Files
create/src/Mod/Sketcher/App/SketchObject.cpp
PaddleStroke 11de6743b0 Sketcher: Fix symmetry of slot redundancy false positive (#26604)
* Sketcher: Fix symmetry of slot redundancy false positive

Perturb symmetric geometries when using the 'create symmetric constraints' option to avoid numerical singularities in the solver (Jacobian Rank).
If geometry is "perfect", the solver cannot distinguish between the derivative of a Symmetry constraint and an Equal constraint, flagging one as redundant.
2026-01-13 13:48:41 -06:00

12309 lines
448 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2008 Jürgen Riegel <juergen.riegel@web.de> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD 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 *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
#include <BRepAdaptor_Curve.hxx>
#include <BRepAdaptor_Surface.hxx>
#include <Mod/Part/App/FCBRepAlgoAPI_Section.h>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakeVertex.hxx>
#include <BRepMesh_IncrementalMesh.hxx>
#include <BRepOffsetAPI_NormalProjection.hxx>
#include <BRepTools_WireExplorer.hxx>
#include <BRep_Tool.hxx>
#include <ElCLib.hxx>
#include <GCPnts_AbscissaPoint.hxx>
#include <GC_MakeArcOfCircle.hxx>
#include <GC_MakeCircle.hxx>
#include <GeomAPI_ProjectPointOnCurve.hxx>
#include <GeomAPI_ProjectPointOnSurf.hxx>
#include <GeomConvert.hxx>
#include <GeomConvert_BSplineCurveKnotSplitting.hxx>
#include <GeomLProp_CLProps.hxx>
#include <Geom_BSplineCurve.hxx>
#include <Geom_Circle.hxx>
#include <Geom_Ellipse.hxx>
#include <Geom_Hyperbola.hxx>
#include <Geom_Line.hxx>
#include <Geom_Parabola.hxx>
#include <Geom_Plane.hxx>
#include <Geom_TrimmedCurve.hxx>
#include <Standard_Version.hxx>
#include <TColStd_Array1OfInteger.hxx>
#include <TopExp.hxx>
#include <TopExp_Explorer.hxx>
#include <TopTools_IndexedMapOfShape.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shape.hxx>
#include <gp_Ax3.hxx>
#include <gp_Circ.hxx>
#include <gp_Elips.hxx>
#include <gp_Hypr.hxx>
#include <gp_Parab.hxx>
#include <gp_Pln.hxx>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/geometry/geometries/register/point.hpp>
#include <boost/iostreams/device/array.hpp>
#include <boost/iostreams/stream.hpp>
#include <boost/range/adaptor/map.hpp>
#include <boost/geometry.hpp>
#include <HLRAlgo_Projector.hxx>
#include <HLRBRep_Algo.hxx>
#include <HLRBRep_HLRToShape.hxx>
#include <App/Application.h>
#include <App/Document.h>
#include <App/ElementNamingUtils.h>
#include <App/Expression.h>
#include <App/ExpressionParser.h>
#include <App/FeaturePythonPyImp.h>
#include <App/IndexedName.h>
#include <App/MappedName.h>
#include <App/ObjectIdentifier.h>
#include <App/Datums.h>
#include <App/Part.h>
#include <Base/Console.h>
#include <Base/Reader.h>
#include <Base/Tools.h>
#include <Base/Vector3D.h>
#include <Base/Writer.h>
#include <Mod/Part/App/BodyBase.h>
#include <Mod/Part/App/PartPyCXX.h>
#include <Mod/Part/App/DatumFeature.h>
#include <Mod/Part/App/GeometryMigrationExtension.h>
#include <Mod/Part/App/TopoShapeOpCode.h>
#include <Mod/Part/App/WireJoiner.h>
#include <memory>
#include "GeoEnum.h"
#include "SketchObject.h"
#include "SketchObjectPy.h"
#include "SolverGeometryExtension.h"
#include "ExternalGeometryFacade.h"
#include <Mod/Part/App/Datums.h>
#undef DEBUG
// #define DEBUG
// clang-format off
using namespace Sketcher;
using namespace Base;
namespace sp = std::placeholders;
namespace bio = boost::iostreams;
FC_LOG_LEVEL_INIT("Sketch", true, true)
PROPERTY_SOURCE(Sketcher::SketchObject, Part::Part2DObject)
SketchObject::SketchObject() : geoLastId(0)
{
ADD_PROPERTY_TYPE(
Geometry, (nullptr), "Sketch", (App::PropertyType)(App::Prop_None), "Sketch geometry");
ADD_PROPERTY_TYPE(Constraints,
(nullptr),
"Sketch",
(App::PropertyType)(App::Prop_None),
"Sketch constraints");
ADD_PROPERTY_TYPE(ExternalGeometry,
(nullptr, nullptr),
"Sketch",
(App::PropertyType)(App::Prop_None | App::Prop_ReadOnly),
"Sketch external geometry");
ADD_PROPERTY_TYPE(ExternalTypes,
({}),
"Sketch",
(App::PropertyType)(App::Prop_None | App::Prop_Hidden),
"Sketch external geometry type: 0 = projection, 1 = intersection, 2 = both.");
ADD_PROPERTY_TYPE(FullyConstrained,
(false),
"Sketch",
(App::PropertyType)(App::Prop_Output | App::Prop_ReadOnly | App::Prop_Hidden),
"Sketch is fully constrained");
ADD_PROPERTY_TYPE(Exports,
(nullptr),
"Sketch",
(App::PropertyType)(App::Prop_Hidden),"Sketch export geometry");
ADD_PROPERTY_TYPE(ExternalGeo,
(nullptr),
"Sketch",
(App::PropertyType)(App::Prop_Hidden),"Sketch external geometry");
ADD_PROPERTY_TYPE(ArcFitTolerance,
(0.0),
"Sketch",
(App::PropertyType)(App::Prop_None),
"Tolerance for fitting arcs of projected external geometry");
ADD_PROPERTY(InternalShape,
(Part::TopoShape()));
ADD_PROPERTY_TYPE(MakeInternals,
(false),
"Internal Geometry",
App::Prop_None,
"Enables selection of closed profiles within a sketch as input for operations");
Geometry.setOrderRelevant(true);
allowOtherBody = true;
allowUnaligned = true;
initExternalGeo();
rebuildVertexIndex();
lastDoF = 0;
lastHasConflict = false;
lastHasRedundancies = false;
lastHasPartialRedundancies = false;
lastHasMalformedConstraints = false;
lastSolverStatus = 0;
lastSolveTime = 0;
solverNeedsUpdate = false;
noRecomputes = false;
//NOLINTBEGIN
ExpressionEngine.setValidator(
std::bind(&Sketcher::SketchObject::validateExpression, this, sp::_1, sp::_2));
constraintsRemovedConn = Constraints.signalConstraintsRemoved.connect(
std::bind(&Sketcher::SketchObject::constraintsRemoved, this, sp::_1));
constraintsRenamedConn = Constraints.signalConstraintsRenamed.connect(
std::bind(&Sketcher::SketchObject::constraintsRenamed, this, sp::_1));
//NOLINTEND
analyser = new SketchAnalysis(this);
internaltransaction = false;
managedoperation = false;
registerElementCache(internalPrefix(), &InternalShape);
}
SketchObject::~SketchObject() {
delete analyser;
}
void SketchObject::setupObject()
{
ParameterGrp::handle hGrpp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Sketcher");
ArcFitTolerance.setValue(hGrpp->GetFloat("ArcFitTolerance", Precision::Confusion()*10.0));
MakeInternals.setValue(hGrpp->GetBool("MakeInternals", false));
inherited::setupObject();
}
void SketchObject::initExternalGeo() {
std::vector<Part::Geometry *> geos;
auto HLine = GeometryTypedFacade<Part::GeomLineSegment>::getTypedFacade();
auto VLine = GeometryTypedFacade<Part::GeomLineSegment>::getTypedFacade();
HLine->getTypedGeometry()->setPoints(Base::Vector3d(0,0,0),Base::Vector3d(1,0,0));
VLine->getTypedGeometry()->setPoints(Base::Vector3d(0,0,0),Base::Vector3d(0,1,0));
HLine->setConstruction(true);
HLine->setId(-1);
VLine->setConstruction(true);
VLine->setId(-2);
geos.push_back(HLine->getGeometry());
geos.push_back(VLine->getGeometry());
HLine->setOwner(false); // we have transferred the ownership to ExternalGeo
VLine->setOwner(false); // we have transferred the ownership to ExternalGeo
ExternalGeo.setValues(std::move(geos));
}
short SketchObject::mustExecute() const
{
if (Geometry.isTouched())
return 1;
if (Constraints.isTouched())
return 1;
if (ExternalGeometry.isTouched())
return 1;
if (ExternalGeo.isTouched())
return 1;
return Part2DObject::mustExecute();
}
App::DocumentObjectExecReturn* SketchObject::execute()
{
try {
App::DocumentObjectExecReturn* rtn = Part2DObject::execute();// to positionBySupport
if (rtn != App::DocumentObject::StdReturn)
// error
return rtn;
}
catch (const Base::Exception& e) {
return new App::DocumentObjectExecReturn(e.what());
}
// setup and diagnose the sketch
try {
rebuildExternalGeometry();
Constraints.acceptGeometry(getCompleteGeometry());
}
catch (const Base::Exception&) {
// 9/16/24: We used to clear the constraints here, but we no longer want to do that
// as missing reference geometry is not considered an error while we sort out sketcher UI.
// Base::Console().error("%s\nClear constraints to external geometry\n", e.what());
// we cannot trust the constraints of external geometries, so remove them
// delConstraintsToExternal();
}
// This includes a regular solve including full geometry update, except when an error
// ensues
int err = this->solve(true);
if (err == -4) {// over-constrained sketch
std::string msg = "Over-constrained sketch\n";
appendConflictMsg(lastConflicting, msg);
return new App::DocumentObjectExecReturn(msg.c_str(), this);
}
else if (err == -3) {// conflicting constraints
std::string msg = "Sketch with conflicting constraints\n";
appendConflictMsg(lastConflicting, msg);
return new App::DocumentObjectExecReturn(msg.c_str(), this);
}
else if (err == -2) {// redundant constraints
std::string msg = "Sketch with redundant constraints\n";
appendRedundantMsg(lastRedundant, msg);
return new App::DocumentObjectExecReturn(msg.c_str(), this);
}
else if (err == -5) {
std::string msg = "Sketch with malformed constraints\n";
appendMalformedConstraintsMsg(lastMalformedConstraints, msg);
return new App::DocumentObjectExecReturn(msg.c_str(), this);
}
else if (err == -1) {// Solver failed
return new App::DocumentObjectExecReturn("Solving the sketch failed", this);
}
// this is not necessary for sketch representation in edit mode, unless we want to trigger an
// update of the objects that depend on this sketch (like pads)
buildShape();
return App::DocumentObject::StdReturn;
}
static bool inline checkSmallEdge(const Part::TopoShape &s) {
if (s.shapeType() != TopAbs_EDGE)
return false;
BRepAdaptor_Curve adapt(TopoDS::Edge(s.getShape()));
return GCPnts_AbscissaPoint::Length(adapt, Precision::Confusion()) <= Precision::Confusion();
}
// clang-format on
void SketchObject::buildShape()
{
// We use the following instead to map element names
std::vector<Part::TopoShape> shapes;
std::vector<Part::TopoShape> vertices;
int geoId = 0;
auto addVertex = [&vertices](auto vertex, auto name) {
if (!vertex.hasElementMap()) {
vertex.resetElementMap(std::make_shared<Data::ElementMap>());
}
vertex.setElementName(
Data::IndexedName::fromConst("Vertex", 1),
Data::MappedName::fromRawData(name.c_str()),
0L
);
vertices.push_back(vertex);
vertices.back().copyElementMap(vertex, Part::OpCodes::Sketch);
};
auto addEdge = [this, &shapes](auto geo, auto indexedName) {
shapes.push_back(getEdge(geo, convertSubName(indexedName, false).c_str()));
if (checkSmallEdge(shapes.back())) {
FC_WARN("Edge too small: " << indexedName);
}
};
// get the geometry after running the solver
auto geometries = solvedSketch.extractGeometry();
for (auto geo : geometries) {
++geoId;
if (GeometryFacade::getConstruction(geo)) {
continue;
}
if (geo->isDerivedFrom<Part::GeomPoint>()) {
int idx = getVertexIndexGeoPos(geoId - 1, Sketcher::PointPos::start);
addVertex(
Part::TopoShape {TopoDS::Vertex(geo->toShape())},
convertSubName(Data::IndexedName::fromConst("Vertex", idx + 1), false)
);
}
else {
auto indexedName = Data::IndexedName::fromConst("Edge", geoId);
addEdge(geo, indexedName);
}
}
for (auto geo : geometries) {
delete geo;
}
for (int i = 2; i < ExternalGeo.getSize(); ++i) {
auto geo = ExternalGeo[i];
auto egf = ExternalGeometryFacade::getFacade(geo);
if (!egf->testFlag(ExternalGeometryExtension::Defining)) {
continue;
}
auto indexedName = Data::IndexedName::fromConst("ExternalEdge", i - 1);
if (geo->isDerivedFrom<Part::GeomPoint>()) {
addVertex(
Part::TopoShape {TopoDS::Vertex(geo->toShape())},
convertSubName(indexedName, false)
);
}
else {
addEdge(geo, indexedName);
}
}
internalElementMap.clear();
if (shapes.empty() && vertices.empty()) {
InternalShape.setValue(Part::TopoShape());
Shape.setValue(Part::TopoShape());
return;
}
Part::TopoShape result(0, getDocument()->getStringHasher());
if (vertices.empty()) {
// Notice here we supply op code Part::OpCodes::Sketch to makeElementWires().
result.makeElementWires(shapes, Part::OpCodes::Sketch);
}
else {
std::vector<Part::TopoShape> results;
if (!shapes.empty()) {
// Note, that we HAVE TO add the Part::OpCodes::Sketch op code to all
// geometry exposed through the Shape property, because
// SketchObject::getElementName() relies on this op code to
// differentiate geometries that are exposed with those in edit
// mode.
auto wires = Part::TopoShape().makeElementWires(shapes, Part::OpCodes::Sketch);
for (const auto& wire : wires.getSubTopoShapes(TopAbs_WIRE)) {
results.push_back(wire);
}
}
results.insert(results.end(), vertices.begin(), vertices.end());
result.makeElementCompound(results);
}
result.Tag = getID();
InternalShape.setValue(buildInternals(result.located(TopLoc_Location())));
// Must set Shape property after InternalShape so that
// GeoFeature::updateElementReference() can run properly on change of Shape
// property, because some reference may pointing to the InternalShape
Shape.setValue(result);
}
// clang-format off
const std::map<std::string,std::string> SketchObject::getInternalElementMap() const
{
if (!internalElementMap.empty() || !MakeInternals.getValue())
return internalElementMap;
const auto& internalShape = InternalShape.getShape();
auto shape = Shape.getShape().located(TopLoc_Location());
if (!internalShape.isNull() && !shape.isNull()) {
std::vector<std::string> names;
std::string prefix;
const std::array<TopAbs_ShapeEnum, 2> types = {TopAbs_VERTEX, TopAbs_EDGE};
for (const auto &type : types) {
prefix = internalPrefix() + Part::TopoShape::shapeName(type);
std::size_t len = prefix.size();
int i=0;
for (const auto &v : internalShape.getSubTopoShapes(type)) {
++i;
shape.findSubShapesWithSharedVertex(v, &names, Data::SearchOption::CheckGeometry
|Data::SearchOption::SingleResult);
if (names.empty())
continue;
prefix += std::to_string(i);
internalElementMap[prefix] = names.front();
internalElementMap[names.front()] = prefix;
prefix.resize(len);
names.clear();
}
}
}
return internalElementMap;
}
Part::TopoShape SketchObject::buildInternals(const Part::TopoShape &edges) const {
if (!MakeInternals.getValue())
return Part::TopoShape();
try {
Part::WireJoiner joiner;
joiner.setTightBound(true);
joiner.setMergeEdges(true);
joiner.addShape(edges);
Part::TopoShape result(getID(), getDocument()->getStringHasher());
if (!joiner.Shape().IsNull()) {
joiner.getResultWires(result, "SKF");
result = result.makeElementFace(result.getSubTopoShapes(TopAbs_WIRE),
/*op*/"",
/*maker*/"Part::FaceMakerRing",
/*pln*/nullptr
);
}
Part::TopoShape openWires(getID(), getDocument()->getStringHasher());
joiner.getOpenWires(openWires, "SKF");
if (openWires.isNull()) {
return result; // No open wires, return either face or empty toposhape
}
if (result.isNull()) {
return openWires; // No face, but we have open wires to return as a shape
}
return result.makeElementCompound({result, openWires}); // Compound and return both
} catch (Base::Exception &e) {
FC_WARN("Failed to make face for sketch: " << e.what());
} catch (Standard_Failure &e) {
FC_WARN("Failed to make face for sketch: " << e.GetMessageString());
}
return Part::TopoShape();
}
static const char *hasSketchMarker(const char *name) {
static std::string marker(Part::TopoShape::elementMapPrefix()+Part::OpCodes::Sketch);
if (!name)
return nullptr;
return strstr(name,marker.c_str());
}
int SketchObject::hasConflicts() const
{
if (lastDoF < 0)// over-constrained sketch
return -2;
if (solvedSketch.hasConflicts())// conflicting constraints
return -1;
return 0;
}
void SketchObject::retrieveSolverDiagnostics()
{
lastHasConflict = solvedSketch.hasConflicts();
lastHasRedundancies = solvedSketch.hasRedundancies();
lastHasPartialRedundancies = solvedSketch.hasPartialRedundancies();
lastHasMalformedConstraints = solvedSketch.hasMalformedConstraints();
lastConflicting = solvedSketch.getConflicting();
lastRedundant = solvedSketch.getRedundant();
lastPartiallyRedundant = solvedSketch.getPartiallyRedundant();
lastMalformedConstraints = solvedSketch.getMalformedConstraints();
}
int SketchObject::solve(bool updateGeoAfterSolving /*=true*/)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
// Reset the initial movement in case of a dragging operation was ongoing on the solver.
solvedSketch.resetInitMove();
// if updateGeoAfterSolving=false, the solver information is updated, but the Sketch is nothing
// updated. It is useful to avoid triggering an OnChange when the goeometry did not change but
// the solver needs to be updated.
// We should have an updated Sketcher (sketchobject) geometry or this solve() should not have
// happened therefore we update our sketch solver geometry with the SketchObject one.
//
// set up a sketch (including dofs counting and diagnosing of conflicts)
lastDoF = solvedSketch.setUpSketch(
getCompleteGeometry(), Constraints.getValues(), getExternalGeometryCount());
// At this point we have the solver information about conflicting/redundant/over-constrained,
// but the sketch is NOT solved. Some examples: Redundant: a vertical line, a horizontal line
// and an angle constraint of 90 degrees between the two lines Conflicting: a 80 degrees angle
// between a vertical line and another line, then adding a horizontal constraint to that other
// line OverConstrained: a conflicting constraint when all other DoF are already constrained (it
// has more constraints than parameters and the extra constraints are not redundant)
solverNeedsUpdate = false;
retrieveSolverDiagnostics();
lastSolveTime = 0.0;
// Failure is default for notifying the user unless otherwise proven
lastSolverStatus = GCS::Failed;
int err = 0;
// redundancy is a lower priority problem than conflict/over-constraint/solver error
// we set it here because we are indeed going to solve, as we can. However, we still want to
// provide the right error code.
if (lastHasRedundancies) {// redundant constraints
err = -2;
}
if (lastDoF < 0) {// over-constrained sketch
err = -4;
}
else if (lastHasConflict) {// conflicting constraints
// The situation is exactly the same as in the over-constrained situation.
err = -3;
}
else if (lastHasMalformedConstraints) {
err = -5;
}
else {
lastSolverStatus = solvedSketch.solve();
if (lastSolverStatus != 0) {// solving
err = -1;
}
}
if (lastHasMalformedConstraints) {
Base::Console().error(
this->getFullLabel(),
QT_TRANSLATE_NOOP("Notifications", "The Sketch has malformed constraints!") "\n");
}
if (lastHasPartialRedundancies) {
QString uniqueName = QString::fromLatin1(this->getNameInDocument());
QString userLabel = QString::fromUtf8(Label.getValue());
QString ref;
if (uniqueName == userLabel) {
ref = uniqueName;
} else {
ref = QStringLiteral("%1 (%2)").arg(uniqueName).arg(userLabel);
}
QString msg = QCoreApplication::translate(
"Notifications",
"\"%1\" has partially redundant constraint(s)."
).arg(ref);
Base::Console().warning(this->getFullLabel(), "%s\n", msg.toUtf8().constData());
}
lastSolveTime = solvedSketch.getSolveTime();
// In uncommon situations, the analysis of QR decomposition leads to full rank, but the result
// does not converge. We avoid marking a sketch as fully constrained when no convergence is
// achieved.
if (err == 0) {
FullyConstrained.setValue(lastDoF == 0);
if (updateGeoAfterSolving) {
// set the newly solved geometry
std::vector<Part::Geometry*> geomlist = solvedSketch.extractGeometry();
Part::PropertyGeometryList tmp;
tmp.setValues(std::move(geomlist));
// Only set values if there is actual changes
if (Constraints.isTouched() || !Geometry.isSame(tmp)) {
Geometry.moveValues(std::move(tmp));
}
}
}
signalSolverUpdate();
return err;
}
namespace bg = boost::geometry;
namespace bgi = boost::geometry::index;
// NOLINTNEXTLINE
BOOST_GEOMETRY_REGISTER_POINT_3D(Base::Vector3d, double, bg::cs::cartesian, x, y, z)
class SketchObject::GeoHistory
{
private:
static constexpr int bgiMaxElements = 16;
using Parameters = bgi::linear<bgiMaxElements>;
using IdSet = std::set<long>;
using IdSets = std::pair<IdSet, IdSet>;
using AdjList = std::list<IdSet>;
// associate a geo with connected ones on both points
using AdjMap = std::map<long, IdSets>;
// maps start/end points to all existing geo to query and update adjacencies
using Value = std::pair<Base::Vector3d, AdjList::iterator>;
AdjList adjlist;
AdjMap adjmap;
bgi::rtree<Value,Parameters> rtree;
public:
AdjList::iterator find(const Base::Vector3d &pt,bool strict=true){
std::vector<Value> ret;
rtree.query(bgi::nearest(pt, 1), std::back_inserter(ret));
if (!ret.empty()) {
// NOTE: we are using square distance here, the 1e-6 threshold is
// very forgiving. We should have used Precision::SquareConfisuion(),
// which is 1e-14. However, there is a problem with current
// commandGeoCreate. They create new geometry with initial point of
// the exact mouse position, instead of the preselected point
// position, and rely on auto constraint to snap in the new
// geometry. So, we cannot use a very strict threshold here.
double tol = strict?Precision::SquareConfusion()*10:1e-6;
double d = Base::DistanceP2(ret[0].first,pt);
if(d<tol) {
return ret[0].second;
}
}
return adjlist.end();
}
void clear() {
rtree.clear();
adjlist.clear();
}
void update(const Base::Vector3d &pt, long id) {
FC_TRACE("update " << id << ", " << FC_xyz(pt));
auto it = find(pt);
if(it==adjlist.end()) {
adjlist.emplace_back();
it = adjlist.end();
--it;
rtree.insert(std::make_pair(pt,it));
}
it->insert(id);
}
void finishUpdate(const std::map<long,int> &geomap) {
IdSet oldset;
for(auto &idset : adjlist) {
oldset.clear();
for(long _id : idset) {
long id = abs(_id);
auto& v = adjmap[id];
auto& adj = _id > 0 ? v.first : v.second;
for (auto it = adj.begin(); it != adj.end(); /* don't advance here */) {
long other = *it;
auto removeId = it++; // grab ID we might erase, and advance
if (geomap.find(other) == geomap.end()) {
// remember those deleted IDs to swap in below
oldset.insert(other);
}
else if (idset.find(other) == idset.end()) {
// delete any existing IDs that are no longer in the adj list
adj.erase(removeId);
}
}
// now merge the current ones
for(long _id2 : idset) {
long id2 = abs(_id2);
if(id!=id2) {
adj.insert(id2);
}
}
}
// now reset the adjacency list with only those deleted id's,
// because the whole purpose of this history is to try to reuse
// deleted id.
idset.swap(oldset);
}
}
AdjList::iterator end() {
return adjlist.end();
}
size_t size() {
return rtree.size();
}
};
void SketchObject::updateGeoHistory() {
if(!geoHistoryLevel) return;
if (!geoHistory) {
geoHistory = std::make_unique<GeoHistory>();
}
FC_TIME_INIT(t);
const auto &geos = getInternalGeometry();
geoHistory->clear();
for (auto geo : geos) {
auto pstart = getPoint(geo, PointPos::start);
auto pend = getPoint(geo, PointPos::end);
int id = GeometryFacade::getId(geo);
geoHistory->update(pstart,id);
if(pstart!=pend)
geoHistory->update(pend,-id);
}
geoHistory->finishUpdate(geoMap);
FC_TIME_LOG(t,"update geometry history (" << geoHistory->size() << ", " << geoMap.size()<<')');
}
// clang-format on
void SketchObject::generateId(const Part::Geometry* geo)
{
auto preReturn = [this, &geo](auto& newId) {
GeometryFacade::setId(geo, newId);
geoMap[Sketcher::GeometryFacade::getId(geo)] = (long)Geometry.getSize();
};
auto isNotInGeoMap = [this](auto& id) {
if (geoMap.find(id) == geoMap.end()) {
return true;
}
FC_TRACE("ignore " << id);
return false;
};
if (geoHistoryLevel == 0) {
preReturn(++geoLastId);
return;
}
if (!geoHistory) {
updateGeoHistory();
}
// Search geo history to see if the start point and end point belongs to
// some deleted geometries. Prefer matching both start and end point. If
// can't then try start and then end. Generate new id if none is found.
auto pstart = getPoint(geo, PointPos::start);
auto it = geoHistory->find(pstart, false);
auto pend = getPoint(geo, PointPos::end);
auto it2 = it;
if (pstart != pend) {
it2 = geoHistory->find(pend, false);
if (it2 == geoHistory->end()) {
it2 = it;
}
}
std::vector<long> found;
if (geoHistoryLevel <= 1 && (it == geoHistory->end() || it2 == it)) {
// level <= 1 means we only reuse id if both start and end matches
preReturn(++geoLastId);
return;
}
if (it != geoHistory->end()) {
// `find_if` avoids checking twice
auto iterOfId = std::ranges::find_if(*it, isNotInGeoMap);
if (iterOfId != it->end() && it2 == it) {
preReturn(*iterOfId);
return;
}
std::copy_if(iterOfId, it->end(), std::back_inserter(found), isNotInGeoMap);
}
if (found.empty()) {
// no candidate exists
if (it2 == it) {
preReturn(++geoLastId);
return;
}
auto iterOfId = std::ranges::find_if(*it, isNotInGeoMap);
if (iterOfId != it->end()) {
preReturn(*iterOfId);
return;
}
preReturn(++geoLastId);
return;
}
auto isInIt2 = [&it2](auto& id) {
if (it2->find(id) != it2->end()) {
return true;
}
FC_TRACE("ignore " << id);
return false;
};
// already some candidate exists, search for matching of both
// points
if (it2 != it) {
auto iterOfId = std::ranges::find_if(found, isInIt2);
if (iterOfId != found.end()) {
preReturn(*iterOfId);
return;
}
}
FC_TRACE("found " << found.front());
preReturn(found.front());
}
// clang-format off
int SketchObject::setDatum(int ConstrId, double Datum)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
// set the changed value for the constraint
if (this->Constraints.hasInvalidGeometry())
return -6;
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
ConstraintType type = vals[ConstrId]->Type;
// for tangent, value==0 is autodecide, value==Pi/2 is external and value==-Pi/2 is internal
if (!vals[ConstrId]->isDimensional() && type != Tangent && type != Perpendicular)
return -1;
if ((type == Radius || type == Diameter || type == Weight) && Datum <= 0)
return (Datum == 0) ? -5 : -4;
if (type == Distance && Datum == 0)
return -5;
// copy the list
std::vector<Constraint*> newVals(vals);
double oldDatum = newVals[ConstrId]->getValue();
newVals[ConstrId] = newVals[ConstrId]->clone();
newVals[ConstrId]->setValue(Datum);
this->Constraints.setValues(std::move(newVals));
int err = solve();
if (err)
this->Constraints.getValues()[ConstrId]->setValue(oldDatum);// newVals is a shell now
return err;
}
double SketchObject::getDatum(int ConstrId) const
{
if (!this->Constraints[ConstrId]->isDimensional()) {
return 0.0;
}
return this->Constraints[ConstrId]->getValue();
}
int SketchObject::setDriving(int ConstrId, bool isdriving)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
int ret = testDrivingChange(ConstrId, isdriving);
if (ret < 0)
return ret;
// copy the list
std::vector<Constraint*> newVals(vals);
newVals[ConstrId] = newVals[ConstrId]->clone();
newVals[ConstrId]->isDriving = isdriving;
this->Constraints.setValues(std::move(newVals));
if (!isdriving)
setExpression(Constraints.createPath(ConstrId), std::shared_ptr<App::Expression>());
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes)
solve();
return 0;
}
int SketchObject::getDriving(int ConstrId, bool& isdriving)
{
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
if (!vals[ConstrId]->isDimensional())
return -1;
isdriving = vals[ConstrId]->isDriving;
return 0;
}
int SketchObject::testDrivingChange(int ConstrId, bool isdriving)
{
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
if (!vals[ConstrId]->isDimensional())
return -2;
if (!(vals[ConstrId]->First >= 0 || vals[ConstrId]->Second >= 0 || vals[ConstrId]->Third >= 0)
&& isdriving) {
// a constraint that does not have at least one element as not-external-geometry can never
// be driving.
return -3;
}
return 0;
}
int SketchObject::setActive(int ConstrId, bool isactive)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
// copy the list
std::vector<Constraint*> newVals(vals);
// clone the changed Constraint
Constraint* constNew = vals[ConstrId]->clone();
constNew->isActive = isactive;
newVals[ConstrId] = constNew;
this->Constraints.setValues(std::move(newVals));
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes)
solve();
return 0;
}
int SketchObject::getActive(int ConstrId, bool& isactive)
{
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
isactive = vals[ConstrId]->isActive;
return 0;
}
int SketchObject::toggleActive(int ConstrId)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
// copy the list
std::vector<Constraint*> newVals(vals);
// clone the changed Constraint
Constraint* constNew = vals[ConstrId]->clone();
constNew->isActive = !constNew->isActive;
newVals[ConstrId] = constNew;
this->Constraints.setValues(std::move(newVals));
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes)
solve();
return 0;
}
int SketchObject::setLabelPosition(int ConstrId, float value)
{
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size())) {
return -1;
}
// copy the list
std::vector<Constraint*> newVals(vals);
// clone the changed Constraint
Constraint* constNew = vals[ConstrId]->clone();
constNew->LabelPosition = value;
newVals[ConstrId] = constNew;
this->Constraints.setValues(std::move(newVals));
return 0;
}
int SketchObject::getLabelPosition(int ConstrId, float& value)
{
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size())) {
return -1;
}
value = vals[ConstrId]->LabelPosition;
return 0;
}
int SketchObject::setLabelDistance(int ConstrId, float value)
{
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size())) {
return -1;
}
// copy the list
std::vector<Constraint*> newVals(vals);
// clone the changed Constraint
Constraint* constNew = vals[ConstrId]->clone();
constNew->LabelDistance = value;
newVals[ConstrId] = constNew;
this->Constraints.setValues(std::move(newVals));
return 0;
}
int SketchObject::getLabelDistance(int ConstrId, float& value)
{
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size())) {
return -1;
}
value = vals[ConstrId]->LabelDistance;
return 0;
}
/// Make all dimensionals Driving/non-Driving
int SketchObject::setDatumsDriving(bool isdriving)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> newVals(vals);
for (size_t i = 0; i < newVals.size(); i++) {
if (!testDrivingChange(i, isdriving)) {
newVals[i] = newVals[i]->clone();
newVals[i]->isDriving = isdriving;
}
}
this->Constraints.setValues(std::move(newVals));
// newVals is a shell now
const std::vector<Constraint*>& uvals = this->Constraints.getValues();
for (size_t i = 0; i < uvals.size(); i++) {
if (!isdriving && uvals[i]->isDimensional())
setExpression(Constraints.createPath(i), std::shared_ptr<App::Expression>());
}
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes)
solve();
return 0;
}
int SketchObject::moveDatumsToEnd()
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> copy(vals);
std::vector<Constraint*> newVals(vals.size());
int addindex = copy.size() - 1;
// add the dimensionals at the end
for (int i = copy.size() - 1; i >= 0; i--) {
if (copy[i]->isDimensional()) {
newVals[addindex] = copy[i];
addindex--;
}
}
// add the non-dimensionals
for (int i = copy.size() - 1; i >= 0; i--) {
if (!copy[i]->isDimensional()) {
newVals[addindex] = copy[i];
addindex--;
}
}
this->Constraints.setValues(std::move(newVals));
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes)
solve();
return 0;
}
void SketchObject::reverseAngleConstraintToSupplementary(Constraint* constr, int constNum)
{
std::swap(constr->First, constr->Second);
std::swap(constr->FirstPos, constr->SecondPos);
constr->FirstPos = (constr->FirstPos == Sketcher::PointPos::start) ? Sketcher::PointPos::end : Sketcher::PointPos::start;
// Edit the expression if any, else modify constraint value directly
if (constraintHasExpression(constNum)) {
std::string expression = getConstraintExpression(constNum);
setConstraintExpression(constNum, std::move(reverseAngleConstraintExpression(expression)));
}
else {
double actAngle = constr->getValue();
constr->setValue(std::numbers::pi - actAngle);
}
}
void SketchObject::inverseAngleConstraint(Constraint* constr)
{
constr->FirstPos = (constr->FirstPos == Sketcher::PointPos::start) ? Sketcher::PointPos::end : Sketcher::PointPos::start;
constr->SecondPos = (constr->SecondPos == Sketcher::PointPos::start) ? Sketcher::PointPos::end : Sketcher::PointPos::start;
}
bool SketchObject::constraintHasExpression(int constNum) const
{
App::ObjectIdentifier path = Constraints.createPath(constNum);
auto info = getExpression(path);
if (info.expression) {
return true;
}
return false;
}
std::string SketchObject::getConstraintExpression(int constNum) const
{
App::ObjectIdentifier path = Constraints.createPath(constNum);
auto info = getExpression(path);
if (info.expression) {
std::string expression = info.expression->toString();
return expression;
}
return {};
}
void SketchObject::setConstraintExpression(int constNum, const std::string& newExpression)
{
App::ObjectIdentifier path = Constraints.createPath(constNum);
auto info = getExpression(path);
if (info.expression) {
try {
std::shared_ptr<App::Expression> expr(App::Expression::parse(this, newExpression));
setExpression(path, std::move(expr));
}
catch (const Base::Exception&) {
Base::Console().error("Failed to set constraint expression.");
}
}
}
std::string SketchObject::reverseAngleConstraintExpression(std::string expression)
{
// Check if expression contains units (°, deg, rad)
if (expression.find("°") != std::string::npos
|| expression.find("deg") != std::string::npos
|| expression.find("rad") != std::string::npos) {
if (expression.substr(0, 9) == "180 ° - ") {
expression = expression.substr(9, expression.size() - 9);
}
else {
expression = "180 ° - (" + expression + ")";
}
}
else {
if (expression.substr(0, 6) == "180 - ") {
expression = expression.substr(6, expression.size() - 6);
}
else {
expression = "180 - (" + expression + ")";
}
}
return expression;
}
int SketchObject::setVirtualSpace(int ConstrId, bool isinvirtualspace)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
// copy the list
std::vector<Constraint*> newVals(vals);
// clone the changed Constraint
Constraint* constNew = vals[ConstrId]->clone();
constNew->isInVirtualSpace = isinvirtualspace;
newVals[ConstrId] = constNew;
this->Constraints.setValues(std::move(newVals));
// Solver didn't actually update, but we need this to inform view provider
// to redraw
signalSolverUpdate();
return 0;
}
int SketchObject::setVirtualSpace(std::vector<int> constrIds, bool isinvirtualspace)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (constrIds.empty())
return 0;
std::sort(constrIds.begin(), constrIds.end());
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (constrIds.front() < 0 || constrIds.back() >= int(vals.size()))
return -1;
std::vector<Constraint*> newVals(vals);
for (auto cid : constrIds) {
// clone the changed Constraint
if (vals[cid]->isInVirtualSpace != isinvirtualspace) {
Constraint* constNew = vals[cid]->clone();
constNew->isInVirtualSpace = isinvirtualspace;
newVals[cid] = constNew;
}
}
this->Constraints.setValues(std::move(newVals));
// Solver didn't actually update, but we need this to inform view provider
// to redraw
signalSolverUpdate();
return 0;
}
int SketchObject::getVirtualSpace(int ConstrId, bool& isinvirtualspace) const
{
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
isinvirtualspace = vals[ConstrId]->isInVirtualSpace;
return 0;
}
int SketchObject::toggleVirtualSpace(int ConstrId)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
// copy the list
std::vector<Constraint*> newVals(vals);
// clone the changed Constraint
Constraint* constNew = vals[ConstrId]->clone();
constNew->isInVirtualSpace = !constNew->isInVirtualSpace;
newVals[ConstrId] = constNew;
this->Constraints.setValues(std::move(newVals));
// Solver didn't actually update, but we need this to inform view provider
// to redraw
signalSolverUpdate();
return 0;
}
int SketchObject::setVisibility(std::vector<int> constrIds, bool isVisible)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (constrIds.empty())
return 0;
std::sort(constrIds.begin(), constrIds.end());
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (constrIds.front() < 0 || constrIds.back() >= int(vals.size()))
return -1;
std::vector<Constraint*> newVals(vals);
for (auto cid : constrIds) {
// clone the changed Constraint
if (vals[cid]->isVisible != isVisible) {
Constraint* constNew = vals[cid]->clone();
constNew->isVisible = isVisible;
newVals[cid] = constNew;
}
}
this->Constraints.setValues(std::move(newVals));
// Solver didn't actually update, but we need this to inform view provider
// to redraw
signalSolverUpdate();
return 0;
}
int SketchObject::setVisibility(int ConstrId, bool isVisible)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return -1;
// copy the list
std::vector<Constraint*> newVals(vals);
// clone the changed Constraint
Constraint* constNew = vals[ConstrId]->clone();
constNew->isVisible = isVisible;
newVals[ConstrId] = constNew;
this->Constraints.setValues(std::move(newVals));
// Solver didn't actually update, but we need this to inform view provider
// to redraw
signalSolverUpdate();
return 0;
}
int SketchObject::setUpSketch()
{
lastDoF = solvedSketch.setUpSketch(
getCompleteGeometry(), Constraints.getValues(), getExternalGeometryCount());
retrieveSolverDiagnostics();
if (lastHasRedundancies || lastDoF < 0 || lastHasConflict || lastHasMalformedConstraints
|| lastHasPartialRedundancies)
Constraints.touch();
return lastDoF;
}
int SketchObject::diagnoseAdditionalConstraints(
std::vector<Sketcher::Constraint*> additionalconstraints)
{
auto objectconstraints = Constraints.getValues();
std::vector<Sketcher::Constraint*> allconstraints;
allconstraints.reserve(objectconstraints.size() + additionalconstraints.size());
std::ranges::copy(objectconstraints, back_inserter(allconstraints));
std::ranges::copy(additionalconstraints, back_inserter(allconstraints));
lastDoF =
solvedSketch.setUpSketch(getCompleteGeometry(), allconstraints, getExternalGeometryCount());
retrieveSolverDiagnostics();
return lastDoF;
}
int SketchObject::moveGeometries(const std::vector<GeoElementId>& geoEltIds, const Base::Vector3d& toPoint, bool relative,
bool updateGeoBeforeMoving)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
// if we are moving a point at SketchObject level, we need to start from a solved sketch
// if we have conflicts we can forget about moving. However, there is the possibility that we
// need to do programmatically moves of new geometry that has not been solved yet and that
// because they were programmatically generated won't generate a conflict. This is the case of
// Fillet for example. This is why exceptionally, it may be required to update the sketch
// geometry to that of of SketchObject upon moving. => use updateGeometry parameter = true then
if (updateGeoBeforeMoving || solverNeedsUpdate) {
lastDoF = solvedSketch.setUpSketch(
getCompleteGeometry(), Constraints.getValues(), getExternalGeometryCount());
retrieveSolverDiagnostics();
solverNeedsUpdate = false;
}
if (lastDoF < 0)// over-constrained sketch
return -1;
if (lastHasConflict)// conflicting constraints
return -1;
// move the point and solve
lastSolverStatus = solvedSketch.moveGeometries(geoEltIds, toPoint, relative);
// moving the point can not result in a conflict that we did not have
// or a redundancy that we did not have before, or a change of DoF
if (lastSolverStatus == 0) {
std::vector<Part::Geometry*> geomlist = solvedSketch.extractGeometry();
Geometry.setValues(geomlist);
for (auto* geo : geomlist) {
delete geo;
}
}
solvedSketch.resetInitMove();// reset solver point moving mechanism
return lastSolverStatus;
}
int SketchObject::moveGeometry(int geoId, PointPos pos, const Base::Vector3d& toPoint, bool relative,
bool updateGeoBeforeMoving)
{
std::vector<GeoElementId> geoEltIds = { GeoElementId(geoId, pos) };
return moveGeometries(geoEltIds, toPoint, relative, updateGeoBeforeMoving);
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomPoint *geomPoint, PointPos PosId)
{
if (PosId == PointPos::start || PosId == PointPos::mid || PosId == PointPos::end)
return geomPoint->getPoint();
return Base::Vector3d();
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomLineSegment *lineSeg, PointPos PosId)
{
switch (PosId) {
case PointPos::start: {
return lineSeg->getStartPoint();
}
case PointPos::end: {
return lineSeg->getEndPoint();
}
default:
break;
}
return Base::Vector3d();
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomCircle *circle, PointPos PosId)
{
auto pt = circle->getCenter();
if(PosId != PointPos::mid)
pt.x += circle->getRadius();
return pt;
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomEllipse *ellipse, PointPos PosId)
{
auto pt = ellipse->getCenter();
if(PosId != PointPos::mid)
pt += ellipse->getMajorAxisDir()*ellipse->getMajorRadius();
return pt;
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomArcOfCircle *aoc, PointPos PosId)
{
switch (PosId) {
case PointPos::start: {
return aoc->getStartPoint(/*emulateCCW=*/true);
}
case PointPos::end: {
return aoc->getEndPoint(/*emulateCCW=*/true);
}
case PointPos::mid: {
return aoc->getCenter();
}
default:
break;
}
return Base::Vector3d();
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomArcOfEllipse *aoe, PointPos PosId)
{
switch (PosId) {
case PointPos::start: {
return aoe->getStartPoint(/*emulateCCW=*/true);
}
case PointPos::end: {
return aoe->getEndPoint(/*emulateCCW=*/true);
}
case PointPos::mid: {
return aoe->getCenter();
}
default:
break;
}
return Base::Vector3d();
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomArcOfHyperbola *aoh, PointPos PosId)
{
switch (PosId) {
case PointPos::start: {
return aoh->getStartPoint();
}
case PointPos::end: {
return aoh->getEndPoint();
}
case PointPos::mid: {
return aoh->getCenter();
}
default:
break;
}
return Base::Vector3d();
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomArcOfParabola *aop, PointPos PosId)
{
switch (PosId) {
case PointPos::start: {
return aop->getStartPoint();
}
case PointPos::end: {
return aop->getEndPoint();
}
case PointPos::mid: {
return aop->getCenter();
}
default:
break;
}
return Base::Vector3d();
}
template <>
Base::Vector3d SketchObject::getPointForGeometry<>(const Part::GeomBSplineCurve *bsp, PointPos PosId)
{
switch (PosId) {
case PointPos::start: {
return bsp->getStartPoint();
}
case PointPos::end: {
return bsp->getEndPoint();
}
default:
break;
}
return Base::Vector3d();
}
Base::Vector3d SketchObject::getPoint(const Part::Geometry *geo, PointPos PosId)
{
if (auto point = freecad_cast<Part::GeomPoint*>(geo)) {
return getPointForGeometry<Part::GeomPoint>(point, PosId);
}
else if (auto lineSegment = freecad_cast<Part::GeomLineSegment*>(geo)) {
return getPointForGeometry<Part::GeomLineSegment>(lineSegment, PosId);
}
else if (auto circle = freecad_cast<Part::GeomCircle*>(geo)) {
return getPointForGeometry<Part::GeomCircle>(circle, PosId);
}
else if (auto ellipse = freecad_cast<Part::GeomEllipse*>(geo)) {
return getPointForGeometry<Part::GeomEllipse>(ellipse, PosId);
}
else if (auto arcOfCircle = freecad_cast<Part::GeomArcOfCircle*>(geo)) {
return getPointForGeometry<Part::GeomArcOfCircle>(arcOfCircle, PosId);
}
else if (auto arcOfEllipse = freecad_cast<Part::GeomArcOfEllipse*>(geo)) {
return getPointForGeometry<Part::GeomArcOfEllipse>(arcOfEllipse, PosId);
}
else if (auto arcOfHyperbola = freecad_cast<Part::GeomArcOfHyperbola*>(geo)) {
return getPointForGeometry<Part::GeomArcOfHyperbola>(arcOfHyperbola, PosId);
}
else if (auto arcOfParabola = freecad_cast<Part::GeomArcOfParabola*>(geo)) {
return getPointForGeometry<Part::GeomArcOfParabola>(arcOfParabola, PosId);
}
else if (auto bSplineCurve = freecad_cast<Part::GeomBSplineCurve*>(geo)) {
return getPointForGeometry<Part::GeomBSplineCurve>(bSplineCurve, PosId);
}
return Base::Vector3d();
}
Base::Vector3d SketchObject::getPoint(int GeoId, PointPos PosId) const
{
if (!(GeoId == H_Axis || GeoId == V_Axis
|| (GeoId <= getHighestCurveIndex() && GeoId >= -getExternalGeometryCount())))
throw Base::ValueError("SketchObject::getPoint. Invalid GeoId was supplied.");
const Part::Geometry* geo = getGeometry(GeoId);
return getPoint(geo,PosId);
}
int SketchObject::getAxisCount() const
{
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
int count = 0;
for (const auto& geo : vals) {
if (geo && GeometryFacade::getConstruction(geo)
&& geo->is<Part::GeomLineSegment>()) {
count++;
}
}
return count;
}
Base::Axis SketchObject::getAxis(int axId) const
{
if (axId == H_Axis || axId == V_Axis || axId == N_Axis)
return Part::Part2DObject::getAxis(axId);
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
int count = 0;
for (const auto& geo : vals) {
if (geo && GeometryFacade::getConstruction(geo)
&& geo->is<Part::GeomLineSegment>()) {
if (count == axId) {
auto* lineSeg = static_cast<Part::GeomLineSegment*>(geo);
Base::Vector3d start = lineSeg->getStartPoint();
Base::Vector3d end = lineSeg->getEndPoint();
return Base::Axis(start, end - start);
}
count++;
}
}
return Base::Axis();
}
void SketchObject::acceptGeometry()
{
Constraints.acceptGeometry(getCompleteGeometry());
rebuildVertexIndex();
signalElementsChanged();
}
bool SketchObject::isSupportedGeometry(const Part::Geometry* geo) const
{
if (geo->is<Part::GeomPoint>()
|| geo->is<Part::GeomCircle>()
|| geo->is<Part::GeomEllipse>()
|| geo->is<Part::GeomArcOfCircle>()
|| geo->is<Part::GeomArcOfEllipse>()
|| geo->is<Part::GeomArcOfHyperbola>()
|| geo->is<Part::GeomArcOfParabola>()
|| geo->is<Part::GeomBSplineCurve>()
|| geo->is<Part::GeomLineSegment>()) {
return true;
}
if (geo->is<Part::GeomTrimmedCurve>()) {
Handle(Geom_TrimmedCurve) trim = Handle(Geom_TrimmedCurve)::DownCast(geo->handle());
Handle(Geom_Circle) circle = Handle(Geom_Circle)::DownCast(trim->BasisCurve());
Handle(Geom_Ellipse) ellipse = Handle(Geom_Ellipse)::DownCast(trim->BasisCurve());
if (!circle.IsNull() || !ellipse.IsNull()) {
return true;
}
}
return false;
}
std::vector<Part::Geometry*>
SketchObject::supportedGeometry(const std::vector<Part::Geometry*>& geoList) const
{
std::vector<Part::Geometry*> supportedGeoList;
supportedGeoList.reserve(geoList.size());
// read-in geometry that the sketcher cannot handle
for (const auto& geo : geoList) {
if (isSupportedGeometry(geo)) {
supportedGeoList.push_back(geo);
}
}
return supportedGeoList;
}
int SketchObject::addGeometry(const std::vector<Part::Geometry*>& geoList,
bool construction /*=false*/)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
std::vector<Part::Geometry*> newVals(vals);
newVals.reserve(newVals.size() + geoList.size());
for (auto& v : geoList) {
Part::Geometry* copy = v->copy();
generateId(copy);
if (construction) {
GeometryFacade::setConstruction(copy, construction);
}
newVals.push_back(copy);
}
// On setting geometry the onChanged method will call acceptGeometry(), thereby updating
// constraint geometry indices and rebuilding the vertex index
Geometry.setValues(std::move(newVals));
return Geometry.getSize() - 1;
}
int SketchObject::addGeometry(const Part::Geometry* geo, bool construction /*=false*/)
{
// this copy has a new random tag (see copy() vs clone())
auto geoNew = std::unique_ptr<Part::Geometry>(geo->copy());
return addGeometry(std::move(geoNew), construction);
}
int SketchObject::addGeometry(std::unique_ptr<Part::Geometry> newgeo, bool construction /*=false*/)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
std::vector<Part::Geometry*> newVals(vals);
auto* geoNew = newgeo.release();
generateId(geoNew);
if (construction) {
GeometryFacade::setConstruction(geoNew, construction);
}
newVals.push_back(geoNew);
// On setting geometry the onChanged method will call acceptGeometry(), thereby updating
// constraint geometry indices and rebuilding the vertex index
Geometry.setValues(std::move(newVals));
return Geometry.getSize() - 1;
}
bool SketchObject::isClosedCurve(const Part::Geometry* geo)
{
return (geo->is<Part::GeomCircle>()
|| geo->is<Part::GeomEllipse>()
|| (geo->is<Part::GeomBSplineCurve>()
&& static_cast<const Part::GeomBSplineCurve*>(geo)->isPeriodic()));
}
bool SketchObject::hasInternalGeometry(const Part::Geometry* geo)
{
return (geo->is<Part::GeomEllipse>()
|| geo->is<Part::GeomArcOfEllipse>()
|| geo->is<Part::GeomArcOfHyperbola>()
|| geo->is<Part::GeomArcOfParabola>()
|| geo->is<Part::GeomBSplineCurve>());
}
int SketchObject::delGeometry(int GeoId, DeleteOptions options)
{
if (GeoId < 0) {
if(GeoId > GeoEnum::RefExt)
return -1;
return delExternal(-GeoId-1);
}
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
if (GeoId >= int(vals.size())) {
return -1;
}
if (options.testFlag(DeleteOption::IncludeInternalGeometry) && hasInternalGeometry(getGeometry(GeoId))) {
// Only for supported types
this->deleteUnusedInternalGeometry(GeoId, true);
return 0;
}
std::vector<Part::Geometry*> newVals(vals);
newVals.erase(newVals.begin() + GeoId);
// Find coincident points to replace the points of the deleted geometry
std::vector<int> GeoIdList;
std::vector<PointPos> PosIdList;
for (PointPos PosId : {PointPos::start, PointPos::end, PointPos::mid}) {
getDirectlyCoincidentPoints(GeoId, PosId, GeoIdList, PosIdList);
if (GeoIdList.size() > 1) {
delConstraintOnPoint(GeoId, PosId, true /* only coincidence */);
transferConstraints(GeoIdList[0], PosIdList[0], GeoIdList[1], PosIdList[1]);
}
}
const std::vector<Constraint*>& constraints = this->Constraints.getValues();
std::vector<Constraint*> newConstraints;
newConstraints.reserve(constraints.size());
for (const auto& constr : constraints) {
if (auto newConstr = getConstraintAfterDeletingGeo(constr, GeoId)) {
newConstraints.push_back(newConstr.release());
}
}
// Block acceptGeometry in OnChanged to avoid unnecessary checks and updates
{
Base::StateLocker preventUpdate(internaltransaction, true);
this->Geometry.setValues(std::move(newVals));
this->Constraints.setValues(std::move(newConstraints));
}
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
Geometry.touch();
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes && !options.testFlag(DeleteOption::NoSolve)) {
solve(options.testFlag(DeleteOption::UpdateGeometry));
}
return 0;
}
int SketchObject::delGeometries(const std::vector<int>& GeoIds, DeleteOptions options)
{
return delGeometries(GeoIds.begin(), GeoIds.end(), options);
}
template <class InputIt>
int SketchObject::delGeometries(InputIt first, InputIt last, DeleteOptions options)
{
std::vector<int> sGeoIds;
std::vector<int> negativeGeoIds;
// Separate GeoIds into negative (external) and non-negative GeoIds
for (auto it = first; it != last; ++it) {
int geoId = *it;
if (geoId < 0 && geoId <= GeoEnum::RefExt) {
negativeGeoIds.push_back(geoId);
}
else if (geoId >= 0){
sGeoIds.push_back(geoId);
}
}
// Handle negative GeoIds by calling delExternal
if (!negativeGeoIds.empty()) {
int result = delExternal(negativeGeoIds);
if (result != 0) {
return result; // Return if deletion of external geometries failed
}
}
// Proceed with non-negative GeoIds
if (sGeoIds.empty()) {
return 0; // No positive GeoIds to delete
}
// if a GeoId has internal geometry, it must delete internal geometries too
for (auto c : Constraints.getValues()) {
if (c->Type == InternalAlignment) {
auto pos = std::ranges::find(sGeoIds, c->Second);
if (pos != sGeoIds.end()) {
sGeoIds.push_back(c->First);
}
}
}
std::ranges::sort(sGeoIds);
// eliminate duplicates
auto newend = std::unique(sGeoIds.begin(), sGeoIds.end());
sGeoIds.resize(std::distance(sGeoIds.begin(), newend));
return delGeometriesExclusiveList(sGeoIds, options);
}
int SketchObject::delGeometriesExclusiveList(const std::vector<int>& GeoIds, DeleteOptions options)
{
std::vector<int> sGeoIds(GeoIds);
std::ranges::sort(sGeoIds);
if (sGeoIds.empty()) {
return 0;
}
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
if (sGeoIds.front() < 0 || sGeoIds.back() >= int(vals.size())) {
return -1;
}
std::vector<Part::Geometry*> newVals(vals);
for (auto it = sGeoIds.rbegin(); it != sGeoIds.rend(); ++it) {
int GeoId = *it;
newVals.erase(newVals.begin() + GeoId);
// Find coincident points to replace the points of the deleted geometry
std::vector<int> GeoIdList;
std::vector<PointPos> PosIdList;
for (PointPos PosId : {PointPos::start, PointPos::end, PointPos::mid}) {
getDirectlyCoincidentPoints(GeoId, PosId, GeoIdList, PosIdList);
if (GeoIdList.size() > 1) {
delConstraintOnPoint(GeoId, PosId, true /* only coincidence */);
transferConstraints(GeoIdList[0], PosIdList[0], GeoIdList[1], PosIdList[1]);
}
}
}
// Copy the original constraints
std::vector<Constraint*> constraints;
for (const auto& ptr : this->Constraints.getValues()) {
constraints.push_back(ptr->clone());
}
for (auto itGeo = sGeoIds.rbegin(); itGeo != sGeoIds.rend(); ++itGeo) {
const int GeoId = *itGeo;
for (auto& constr : constraints) {
changeConstraintAfterDeletingGeo(constr, GeoId);
}
}
constraints.erase(std::remove_if(constraints.begin(),
constraints.end(),
[](const auto& constr) {
return constr->Type == ConstraintType::None;
}),
constraints.end());
// Block acceptGeometry in OnChanged to avoid unnecessary checks and updates
{
Base::StateLocker preventUpdate(internaltransaction, true);
this->Geometry.setValues(newVals);
this->Constraints.setValues(std::move(constraints));
}
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
Geometry.touch();
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes && !options.testFlag(DeleteOption::NoSolve)) {
solve(options.testFlag(DeleteOption::UpdateGeometry));
}
return 0;
}
// clang-format on
void SketchObject::replaceGeometries(std::vector<int> oldGeoIds, std::vector<Part::Geometry*>& newGeos)
{
auto& vals = getInternalGeometry();
auto newVals(vals);
if (std::ranges::any_of(oldGeoIds, [](auto geoId) { return geoId < 0; })) {
THROWM(ValueError, "Cannot replace external geometries and axes.");
}
auto oldGeoIdIter = oldGeoIds.begin();
auto newGeoIter = newGeos.begin();
for (; oldGeoIdIter != oldGeoIds.end() && newGeoIter != newGeos.end();
++oldGeoIdIter, ++newGeoIter) {
GeometryFacade::copyId(getGeometry(*oldGeoIdIter), *newGeoIter);
newVals[*oldGeoIdIter] = *newGeoIter;
}
for (; newGeoIter != newGeos.end(); ++newGeoIter) {
generateId(*newGeoIter);
newVals.push_back(*newGeoIter);
}
// Set geometries first, then delete the old ones. This allows to use `delGeometries`.
Geometry.setValues(std::move(newVals));
delGeometries(oldGeoIdIter, oldGeoIds.end());
}
// clang-format off
int SketchObject::deleteAllGeometry(DeleteOptions options)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
std::vector<Part::Geometry*> newVals(0);
std::vector<Constraint*> newConstraints(0);
// Avoid unnecessary updates and checks as this is a transaction
{
Base::StateLocker preventUpdate(internaltransaction, true);
this->Geometry.setValues(newVals);
this->Constraints.setValues(newConstraints);
}
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
Geometry.touch();
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes && !options.testFlag(DeleteOption::NoSolve)) {
solve(options.testFlag(DeleteOption::UpdateGeometry));
}
return 0;
}
int SketchObject::deleteAllConstraints(DeleteOptions options)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
std::vector<Constraint*> newConstraints(0);
this->Constraints.setValues(newConstraints);
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes && !options.testFlag(DeleteOption::NoSolve)) {
solve(options.testFlag(DeleteOption::UpdateGeometry));
}
return 0;
}
int SketchObject::toggleConstruction(int GeoId)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (GeoId >= 0) {
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
if (GeoId >= int(vals.size())) {
return -1;
}
if (getGeometryFacade(GeoId)->isInternalAligned()) {
return -1;
}
// While it may seem that there is not a need to trigger an update at this time, because the
// solver has its own copy of the geometry, and updateColors of the viewprovider may be
// triggered by the clearselection of the UI command, this won't update the elements widget, in
// the accumulative of actions it is judged that it is worth to trigger an update here.
std::unique_ptr<Part::Geometry> geo(vals[GeoId]->clone());
auto gft = GeometryFacade::getFacade(geo.get());
gft->setConstruction(!gft->getConstruction());
this->Geometry.set1Value(GeoId, std::move(geo));
}
else {
if (GeoId > GeoEnum::RefExt) {
return -1;
}
const std::vector<Part::Geometry*>& extGeos = getExternalGeometry();
std::unique_ptr<Part::Geometry> geo(extGeos[-GeoId - 1]->clone());
auto egf = ExternalGeometryFacade::getFacade(geo.get());
egf->setFlag(ExternalGeometryExtension::Defining, !egf->testFlag(ExternalGeometryExtension::Defining));
this->ExternalGeo.set1Value(-GeoId - 1, std::move(geo));
}
solverNeedsUpdate = true;
signalSolverUpdate(); // FIXME: In theory this is totally redundant, but now seems required
// for UI to update.
return 0;
}
int SketchObject::setConstruction(int GeoId, bool on)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
Part::PropertyGeometryList *prop;
int idx;
if (GeoId >= 0) {
prop = &Geometry;
if (GeoId < Geometry.getSize())
idx = GeoId;
else
return -1;
}else if (GeoId <= GeoEnum::RefExt && -GeoId-1 < ExternalGeo.getSize()) {
prop = &ExternalGeo;
idx = -GeoId-1;
}else
return -1;
// While it may seem that there is not a need to trigger an update at this time, because the
// solver has its own copy of the geometry, and updateColors of the viewprovider may be
// triggered by the clearselection of the UI command, this won't update the elements widget, in
// the accumulative of actions it is judged that it is worth to trigger an update here.
std::unique_ptr<Part::Geometry> geo(prop->getValues()[idx]->clone());
if(prop == &Geometry)
GeometryFacade::setConstruction(geo.get(), on);
else {
auto egf = ExternalGeometryFacade::getFacade(geo.get());
egf->setFlag(ExternalGeometryExtension::Defining, on);
}
prop->set1Value(idx,std::move(geo));
solverNeedsUpdate = true;
return 0;
}
// clang-format on
int SketchObject::toggleExternalGeometryFlag(
const std::vector<int>& geoIds,
const std::vector<ExternalGeometryExtension::Flag>& flags
)
{
if (flags.empty()) {
return 0;
}
auto flag = flags.front();
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
bool update = false;
bool touched = false;
auto geos = ExternalGeo.getValues();
std::set<int> idSet(geoIds.begin(), geoIds.end());
for (auto geoId : geoIds) {
if (geoId > GeoEnum::RefExt || -geoId - 1 >= ExternalGeo.getSize()) {
continue;
}
if (!idSet.contains(geoId)) {
continue;
}
idSet.erase(geoId);
const int idx = -geoId - 1;
auto& geo = geos[idx];
const auto egf = ExternalGeometryFacade::getFacade(geo);
const bool value = !egf->testFlag(flag);
if (!egf->getRef().empty()) {
for (auto relatedGeoId : getRelatedGeometry(geoId)) {
if (relatedGeoId == geoId) {
continue;
}
int relatedIndex = -relatedGeoId - 1;
auto& relatedGeometry = geos[relatedIndex];
relatedGeometry = relatedGeometry->clone();
auto relatedFacade = ExternalGeometryFacade::getFacade(relatedGeometry);
for (auto& _flag : flags) {
relatedFacade->setFlag(_flag, value);
}
idSet.erase(relatedGeoId);
}
}
geo = geo->clone();
egf->setGeometry(geo);
for (auto& _flag : flags) {
egf->setFlag(_flag, value);
}
update = update || (value || flag != ExternalGeometryExtension::Frozen);
touched = true;
}
if (!touched) {
return -1;
}
ExternalGeo.setValues(geos);
if (update) {
rebuildExternalGeometry();
}
return 0;
}
// clang-format off
void SketchObject::addGeometryState(const Constraint* cstr) const
{
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
Sketcher::InternalType::InternalType constraintInternalAlignment = InternalType::None;
bool constraintBlockedState = false;
if (getInternalTypeState(cstr, constraintInternalAlignment)) {
auto gf = GeometryFacade::getFacade(vals[cstr->First]);
gf->setInternalType(constraintInternalAlignment);
}
else if (getBlockedState(cstr, constraintBlockedState)) {
auto gf = GeometryFacade::getFacade(vals[cstr->First]);
gf->setBlocked(constraintBlockedState);
}
}
void SketchObject::removeGeometryState(const Constraint* cstr) const
{
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
// Assign correct Internal Geometry Type (see SketchGeometryExtension)
if (cstr->Type == InternalAlignment) {
auto gf = GeometryFacade::getFacade(vals[cstr->First]);
gf->setInternalType(InternalType::None);
}
// Assign Blocked geometry mode (see SketchGeometryExtension)
if (cstr->Type == Block) {
auto gf = GeometryFacade::getFacade(vals[cstr->First]);
gf->setBlocked(false);
}
}
// ConstraintList is used only to make copies.
int SketchObject::addConstraints(const std::vector<Constraint*>& ConstraintList)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> newVals(vals);
newVals.insert(newVals.end(), ConstraintList.begin(), ConstraintList.end());
for (std::size_t i = newVals.size() - ConstraintList.size(); i < newVals.size(); i++) {
Constraint* cnew = newVals[i]->clone();
newVals[i] = cnew;
if (cnew->Type == Tangent || cnew->Type == Perpendicular) {
AutoLockTangencyAndPerpty(cnew);
}
addGeometryState(cnew);
}
this->Constraints.setValues(std::move(newVals));
return this->Constraints.getSize() - 1;
}
int SketchObject::addCopyOfConstraints(const SketchObject& orig)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
const std::vector<Constraint*>& origvals = orig.Constraints.getValues();
std::vector<Constraint*> newVals(vals);
newVals.reserve(vals.size() + origvals.size());
for (auto& v : origvals)
newVals.push_back(v->copy());
this->Constraints.setValues(std::move(newVals));
auto& uvals = this->Constraints.getValues();
std::size_t uvalssize = uvals.size();
for (std::size_t i = uvalssize, j = 0; i < uvals.size(); i++, j++) {
if (uvals[i]->isDriving && uvals[i]->isDimensional()) {
App::ObjectIdentifier spath = orig.Constraints.createPath(j);
App::PropertyExpressionEngine::ExpressionInfo expr_info = orig.getExpression(spath);
if (expr_info.expression) {// if there is an expression on the source dimensional
App::ObjectIdentifier dpath = this->Constraints.createPath(i);
setExpression(dpath,
std::shared_ptr<App::Expression>(expr_info.expression->copy()));
}
}
}
if (noRecomputes) // if we do not have a recompute, the sketch must be solved to update the DoF of the solver
solve();
return this->Constraints.getSize() - 1;
}
int SketchObject::addConstraint(const Constraint* constraint)
{
auto constraint_ptr = std::unique_ptr<Constraint>(constraint->clone());
return addConstraint(std::move(constraint_ptr));
}
int SketchObject::addConstraint(std::unique_ptr<Constraint> constraint)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> newVals(vals);
Constraint* constNew = constraint.release();
if (constNew->Type == Tangent || constNew->Type == Perpendicular)
AutoLockTangencyAndPerpty(constNew);
addGeometryState(constNew);
newVals.push_back(constNew);// add new constraint at the back
this->Constraints.setValues(std::move(newVals));
return this->Constraints.getSize() - 1;
}
int SketchObject::delConstraint(int ConstrId, DeleteOptions options)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size())) {
return -1;
}
std::vector<Constraint*> newVals(vals);
auto ctriter = newVals.begin() + ConstrId;
removeGeometryState(*ctriter);
newVals.erase(ctriter);
this->Constraints.setValues(std::move(newVals));
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes && !options.testFlag(DeleteOption::NoSolve)) {
solve(options.testFlag(DeleteOption::UpdateGeometry));
}
return 0;
}
int SketchObject::delConstraints(std::vector<int> ConstrIds, DeleteOptions options)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (ConstrIds.empty()) {
return 0;
}
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> newVals(vals);
std::sort(ConstrIds.begin(), ConstrIds.end());
if (ConstrIds.front() < 0 || ConstrIds.back() >= int(vals.size()))
return -1;
for (auto rit = ConstrIds.rbegin(); rit != ConstrIds.rend(); rit++) {
auto ctriter = newVals.begin() + *rit;
removeGeometryState(*ctriter);
newVals.erase(ctriter);
}
this->Constraints.setValues(std::move(newVals));
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes && !options.testFlag(DeleteOption::NoSolve)) {
solve(options.testFlag(DeleteOption::UpdateGeometry));
}
return 0;
}
int SketchObject::delConstraintOnPoint(int VertexId, bool onlyCoincident)
{
int GeoId;
PointPos PosId;
if (VertexId == GeoEnum::RtPnt) {// RootPoint
GeoId = Sketcher::GeoEnum::RtPnt;
PosId = PointPos::start;
}
else
getGeoVertexIndex(VertexId, GeoId, PosId);
return delConstraintOnPoint(GeoId, PosId, onlyCoincident);
}
// clang-format on
int SketchObject::delConstraintOnPoint(int geoId, PointPos posId, bool onlyCoincident)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> newVals;
newVals.reserve(vals.size());
// check if constraints can be redirected to some other point
int replaceGeoId = GeoEnum::GeoUndef;
PointPos replacePosId = Sketcher::PointPos::none;
auto findReplacement = [geoId, posId, &replaceGeoId, &replacePosId, &vals]() {
auto it = std::ranges::find_if(vals, [geoId, posId](auto& constr) {
return constr->Type == Sketcher::Coincident
&& constr->involvesGeoIdAndPosId(geoId, posId);
});
if (it == vals.end()) {
return;
}
if ((*it)->First == geoId && (*it)->FirstPos == posId) {
replaceGeoId = (*it)->Second;
replacePosId = (*it)->SecondPos;
}
else {
replaceGeoId = (*it)->First;
replacePosId = (*it)->FirstPos;
}
};
auto transferToReplacement =
[&geoId, &posId, &replaceGeoId, &replacePosId](int& constrGeoId, PointPos& constrPosId) {
if (replaceGeoId == GeoEnum::GeoUndef) {
return false;
}
if (geoId != constrGeoId || posId != constrPosId) {
return false;
}
constrGeoId = replaceGeoId;
constrPosId = replacePosId;
return true;
};
findReplacement();
auto performCoincidenceChecksOrChanges = [&](auto& constr) -> bool {
if (replaceGeoId == GeoEnum::GeoUndef) {
return false;
}
if (constr->involvesGeoIdAndPosId(replaceGeoId, replacePosId)) {
return false;
}
// Assuming `constr` already involves geoId and posId, all conditions are already met
constr->substituteIndexAndPos(geoId, posId, replaceGeoId, replacePosId);
return true;
};
auto performAllConstraintChecksOrChanges = [&](auto& constr) -> std::optional<bool> {
if (constr->Type != Sketcher::Coincident && onlyCoincident) {
return true;
}
switch (constr->Type) {
case Sketcher::Coincident:
return performCoincidenceChecksOrChanges(constr);
case Sketcher::Distance:
case Sketcher::DistanceX:
case Sketcher::DistanceY: {
return (
transferToReplacement(constr->First, constr->FirstPos)
|| transferToReplacement(constr->Second, constr->SecondPos)
);
}
case Sketcher::PointOnObject: {
return transferToReplacement(constr->First, constr->FirstPos);
}
case Sketcher::Tangent:
case Sketcher::Perpendicular: {
// we could keep this constraint by converting it to a simple one, but that doesn't
// always work (for example if tangent-via-point is necessary), and it is not really
// worth it
return false;
}
case Sketcher::Vertical:
case Sketcher::Horizontal:
case Sketcher::Symmetric: {
return false;
}
default:
return std::nullopt;
}
};
// remove or redirect any constraints associated with the given point
for (auto& constr : vals) {
// keep the constraint if it doesn't involve the point
if (!constr->involvesGeoIdAndPosId(geoId, posId)) {
// for these constraints remove the constraint even if it is not directly associated
// with the given point
const bool isOneOfDistanceTypes = constr->Type == Sketcher::Distance
|| constr->Type == Sketcher::DistanceX || constr->Type == Sketcher::DistanceY;
const bool involvesEntireCurve = constr->First == geoId
&& constr->FirstPos == PointPos::none;
const bool isPosAnEndpoint = posId == PointPos::start || posId == PointPos::end;
if (isOneOfDistanceTypes && involvesEntireCurve && isPosAnEndpoint) {
continue;
}
newVals.push_back(constr);
continue;
}
if (performAllConstraintChecksOrChanges(constr).value_or(true)) {
newVals.push_back(constr);
}
}
if (newVals.size() < vals.size()) {
this->Constraints.setValues(std::move(newVals));
return 0;
}
return -1; // no such constraint
}
// clang-format off
void SketchObject::transferFilletConstraints(int geoId1, PointPos posId1, int geoId2,
PointPos posId2)
{
// If the lines don't intersect, there's no original corner to work with so
// don't try to transfer the constraints. But we should delete line length and equal
// constraints and constraints on the affected endpoints because they're about
// to move unpredictably.
if (!arePointsCoincident(geoId1, posId1, geoId2, posId2)) {
// Delete constraints on the endpoints
delConstraintOnPoint(geoId1, posId1, false);
delConstraintOnPoint(geoId2, posId2, false);
// Delete line length and equal constraints
const std::vector<Constraint*>& constraints = this->Constraints.getValues();
std::vector<int> deleteme;
for (int i = 0; i < int(constraints.size()); i++) {
const Constraint* c = constraints[i];
if (c->Type != Sketcher::Distance && c->Type != Sketcher::Equal) {
continue;
}
bool line1 = c->First == geoId1 && c->FirstPos == PointPos::none;
bool line2 = c->First == geoId2 && c->FirstPos == PointPos::none;
if (line1 || line2) {
deleteme.push_back(i);
}
}
delConstraints(std::move(deleteme), DeleteOption::NoFlag);
return;
}
// If the lines aren't straight, don't try to transfer the constraints.
// TODO: Add support for curved lines.
const Part::Geometry* geo1 = getGeometry(geoId1);
const Part::Geometry* geo2 = getGeometry(geoId2);
if (!geo1->is<Part::GeomLineSegment>() || !geo2->is<Part::GeomLineSegment>()) {
delConstraintOnPoint(geoId1, posId1, false);
delConstraintOnPoint(geoId2, posId2, false);
return;
}
// Add a vertex to preserve the original intersection of the filleted lines
auto* originalCorner = new Part::GeomPoint(getPoint(geoId1, posId1));
int originalCornerId = addGeometry(originalCorner, true);
delete originalCorner;
// Constrain the vertex to the two lines
auto* cornerToLine1 = new Sketcher::Constraint();
cornerToLine1->Type = Sketcher::PointOnObject;
cornerToLine1->First = originalCornerId;
cornerToLine1->FirstPos = PointPos::start;
cornerToLine1->Second = geoId1;
cornerToLine1->SecondPos = PointPos::none;
addConstraint(cornerToLine1);
delete cornerToLine1;
auto* cornerToLine2 = new Sketcher::Constraint();
cornerToLine2->Type = Sketcher::PointOnObject;
cornerToLine2->First = originalCornerId;
cornerToLine2->FirstPos = PointPos::start;
cornerToLine2->Second = geoId2;
cornerToLine2->SecondPos = PointPos::none;
addConstraint(cornerToLine2);
delete cornerToLine2;
Base::StateLocker lock(managedoperation, true);
// Loop through all the constraints and try to do reasonable things with the affected ones
std::vector<Constraint*> newConstraints;
for (auto c : this->Constraints.getValues()) {
// Keep track of whether the affected lines and endpoints appear in this constraint
bool point1First = c->First == geoId1 && c->FirstPos == posId1;
bool point2First = c->First == geoId2 && c->FirstPos == posId2;
bool point1Second = c->Second == geoId1 && c->SecondPos == posId1;
bool point2Second = c->Second == geoId2 && c->SecondPos == posId2;
bool point1Third = c->Third == geoId1 && c->ThirdPos == posId1;
bool point2Third = c->Third == geoId2 && c->ThirdPos == posId2;
bool line1First = c->First == geoId1 && c->FirstPos == PointPos::none;
bool line2First = c->First == geoId2 && c->FirstPos == PointPos::none;
bool line1Second = c->Second == geoId1 && c->SecondPos == PointPos::none;
bool line2Second = c->Second == geoId2 && c->SecondPos == PointPos::none;
if (c->Type == Sketcher::Coincident) {
if ((point1First && point2Second) || (point2First && point1Second)) {
// This is the constraint holding the two edges together that are about to be
// filleted. This constraint goes away because the edges will touch the fillet
// instead.
continue;
}
}
else if (c->Type == Sketcher::Horizontal || c->Type == Sketcher::Vertical) {
// Point-to-point horizontal or vertical constraint, move to new corner point (done towards end of present loop)
}
else if (c->Type == Sketcher::Distance || c->Type == Sketcher::DistanceX
|| c->Type == Sketcher::DistanceY) {
// Point-to-point distance constraint. Move it to the new corner point (done towards end of present loop)
// Distance constraint on the line itself. Change it to point-point between the far end
// of the line and the new corner
if (line1First) {
c->FirstPos = (posId1 == PointPos::start) ? PointPos::end : PointPos::start;
c->Second = originalCornerId;
c->SecondPos = PointPos::start;
}
if (line2First) {
c->FirstPos = (posId2 == PointPos::start) ? PointPos::end : PointPos::start;
c->Second = originalCornerId;
c->SecondPos = PointPos::start;
}
}
else if (c->Type == Sketcher::PointOnObject) {
// The corner to be filleted was touching some other object.
}
else if (c->Type == Sketcher::Equal) {
// Equal length constraints are dicey because the lines are getting shorter. Safer to
// delete them and let the user notice the underconstraint.
if (line1First || line2First || line1Second || line2Second) {
continue;
}
}
else if (c->Type == Sketcher::Symmetric) {
// Symmetries should probably be preserved relative to the original corner
}
else if (c->Type == Sketcher::SnellsLaw) {
// Can't imagine any cases where you'd fillet a vertex going through a lens, so let's
// delete to be safe.
continue;
}
else if (point1First || point2First || point1Second || point2Second || point1Third
|| point2Third) {
// Delete any other point-based constraints on the relevant points
continue;
}
// For any constraint not passing previous conditions, transfer to the new point if relevant
if (point1First || point2First) {
c->First = originalCornerId;
c->FirstPos = PointPos::start;
}
else if (point1Second || point2Second) {
c->Second = originalCornerId;
c->SecondPos = PointPos::start;
}
else if (point1Third || point2Third) {
c->Third = originalCornerId;
c->ThirdPos = PointPos::start;
}
// Default: keep all other constraints
newConstraints.push_back(c->clone());
}
this->Constraints.setValues(std::move(newConstraints));
}
// clang-format on
int SketchObject::transferConstraints(
int fromGeoId,
PointPos fromPosId,
int toGeoId,
PointPos toPosId,
bool doNotTransformTangencies
)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> newVals(vals);
bool changed = false;
for (int i = 0; i < int(newVals.size()); i++) {
if (vals[i]->Type == Sketcher::InternalAlignment) {
// Transferring internal alignment constraint can cause malformed constraints.
// For example a B-spline pole being a point instead of a circle.
continue;
}
else if (vals[i]->involvesGeoIdAndPosId(fromGeoId, fromPosId)
&& !vals[i]->involvesGeoIdAndPosId(toGeoId, toPosId)) {
std::unique_ptr<Constraint> constNew(newVals[i]->clone());
constNew->substituteIndexAndPos(fromGeoId, fromPosId, toGeoId, toPosId);
if (vals[i]->First < 0 && vals[i]->Second < 0) {
// TODO: Can `vals[i]->Third` be involved as well?
// If it is, we need to be sure at most ONE of these is external
continue;
}
switch (vals[i]->Type) {
case Sketcher::Tangent:
case Sketcher::Perpendicular: {
// If not explicitly confirmed, nothing guarantees that a tangent can be freely
// transferred to another coincident point, as the transfer destination edge
// most likely won't be intended to be tangent. However, if it is an end to end
// point tangency, the user expects it to be substituted by a coincidence
// constraint.
if (!doNotTransformTangencies) {
constNew->Type = Sketcher::Coincident;
}
break;
}
case Sketcher::Angle:
// With respect to angle constraints, if it is a DeepSOIC style angle constraint
// (segment+segment+point), then no problem arises as the segments are
// PosId=none. In this case there is no call to this function.
//
// However, other angle constraints are problematic because they are created on
// segments, but internally operate on vertices, PosId=start Such constraint may
// not be successfully transferred on deletion of the segments.
continue;
default:
break;
}
Constraint* constPtr = constNew.release();
newVals[i] = constPtr;
changed = true;
}
}
// assign the new values only if something has changed
if (changed) {
this->Constraints.setValues(std::move(newVals));
}
return 0;
}
// clang-format off
std::vector<int> SketchObject::chooseFilletsEdges(const std::vector<int>& GeoIdList) const
{
if (GeoIdList.size() == 2) {
return GeoIdList;
}
std::vector<int> dst;
for (auto id : GeoIdList) {
if (!GeometryFacade::getFacade(getGeometry(id))->getConstruction()) {
dst.push_back(id);
if (dst.size() > 2) {
return {};
}
}
}
return dst;
}
int SketchObject::fillet(int GeoId, PointPos PosId, double radius, bool trim, bool createCorner, bool chamfer)
{
if (GeoId < 0 || GeoId > getHighestCurveIndex())
return -1;
// Find the other geometry Id associated with the coincident point
std::vector<int> GeoIdList;
std::vector<PointPos> PosIdList;
getDirectlyCoincidentPoints(GeoId, PosId, GeoIdList, PosIdList);
GeoIdList = chooseFilletsEdges(GeoIdList);
// only coincident points between two (non-external) edges can be filleted
if (GeoIdList.size() != 2 || GeoIdList[0] < 0 || GeoIdList[1] < 0) {
return -1;
}
const Part::Geometry* geo1 = getGeometry(GeoIdList[0]);
const Part::Geometry* geo2 = getGeometry(GeoIdList[1]);
if (geo1->is<Part::GeomLineSegment>()
&& geo2->is<Part::GeomLineSegment>()) {
auto* lineSeg1 = static_cast<const Part::GeomLineSegment*>(geo1);
auto* lineSeg2 = static_cast<const Part::GeomLineSegment*>(geo2);
Base::Vector3d midPnt1 = (lineSeg1->getStartPoint() + lineSeg1->getEndPoint()) / 2;
Base::Vector3d midPnt2 = (lineSeg2->getStartPoint() + lineSeg2->getEndPoint()) / 2;
return fillet(GeoIdList[0], GeoIdList[1], midPnt1, midPnt2, radius, trim, createCorner, chamfer);
}
return -1;
}
int SketchObject::fillet(int GeoId1, int GeoId2, const Base::Vector3d& refPnt1,
const Base::Vector3d& refPnt2, double radius, bool trim, bool createCorner, bool chamfer)
{
if (GeoId1 < 0 || GeoId1 > getHighestCurveIndex() || GeoId2 < 0 || GeoId2 > getHighestCurveIndex()) {
return -1;
}
// If either of the two input lines are locked, don't try to trim since it won't work anyway
const Part::Geometry* geo1 = getGeometry(GeoId1);
const Part::Geometry* geo2 = getGeometry(GeoId2);
if (trim && (GeometryFacade::getBlocked(geo1) || GeometryFacade::getBlocked(geo2))) {
trim = false;
}
int pos1 = 0;
int pos2 = 0;
bool reverse = false;
std::unique_ptr<Part::GeomArcOfCircle> arc(createFilletGeometry(geo1, geo2, refPnt1, refPnt2, radius, pos1, pos2, reverse));
if (!arc) {
return -1;
}
int filletId = addGeometry(arc.get());
if (filletId < 0) {
return -1;
}
PointPos PosId1 = static_cast<PointPos>(pos1);
PointPos PosId2= static_cast<PointPos>(pos2);
PointPos filletPosId1 = PointPos::none;
PointPos filletPosId2 = PointPos::none;
Base::Vector3d p1 = arc->getStartPoint(true);
Base::Vector3d p2 = arc->getEndPoint(true);
if (trim) {
if (createCorner && geo1->is<Part::GeomLineSegment>() && geo2->is<Part::GeomLineSegment>()) {
transferFilletConstraints(GeoId1, PosId1, GeoId2, PosId2);
}
else {
delConstraintOnPoint(GeoId1, PosId1, false);
delConstraintOnPoint(GeoId2, PosId2, false);
}
if (reverse) {
filletPosId1 = PointPos::start;
filletPosId2 = PointPos::end;
moveGeometry(GeoId1, PosId1, p1, false, true);
moveGeometry(GeoId2, PosId2, p2, false, true);
}
else {
filletPosId1 = PointPos::end;
filletPosId2 = PointPos::start;
moveGeometry(GeoId1, PosId1, p2, false, true);
moveGeometry(GeoId2, PosId2, p1, false, true);
}
auto tangent1 = std::make_unique<Sketcher::Constraint>();
auto tangent2 = std::make_unique<Sketcher::Constraint>();
tangent1->Type = Sketcher::Tangent;
tangent1->First = GeoId1;
tangent1->FirstPos = PosId1;
tangent1->Second = filletId;
tangent1->SecondPos = filletPosId1;
tangent2->Type = Sketcher::Tangent;
tangent2->First = GeoId2;
tangent2->FirstPos = PosId2;
tangent2->Second = filletId;
tangent2->SecondPos = filletPosId2;
addConstraint(std::move(tangent1));
addConstraint(std::move(tangent2));
}
if (chamfer) {
auto line = std::make_unique<Part::GeomLineSegment>();
line->setPoints(p1, p2);
int lineGeoId = addGeometry(line.get());
auto coinc1 = std::make_unique<Sketcher::Constraint>();
auto coinc2 = std::make_unique<Sketcher::Constraint>();
coinc1->Type = Sketcher::Coincident;
coinc1->First = lineGeoId;
coinc1->FirstPos = filletPosId1;
coinc2->Type = Sketcher::Coincident;
coinc2->First = lineGeoId;
coinc2->FirstPos = filletPosId2;
if (trim) {
coinc1->Second = GeoId1;
coinc1->SecondPos = PosId1;
coinc2->Second = GeoId2;
coinc2->SecondPos = PosId2;
}
else {
coinc1->Second = filletId;
coinc1->SecondPos = PointPos::start;
coinc2->Second = filletId;
coinc2->SecondPos = PointPos::end;
}
addConstraint(std::move(coinc1));
addConstraint(std::move(coinc2));
setConstruction(filletId, true);
}
// if we do not have a recompute after the geometry creation, the sketch must be solved to
// update the DoF of the solver
if (noRecomputes) {
solve();
}
return 0;
}
int SketchObject::extend(int GeoId, double increment, PointPos endpoint)
{
if (GeoId < 0 || GeoId > getHighestCurveIndex())
return -1;
const std::vector<Part::Geometry*>& geomList = getInternalGeometry();
Part::Geometry* geom = geomList[GeoId];
int retcode = -1;
if (geom->is<Part::GeomLineSegment>()) {
auto* seg = static_cast<Part::GeomLineSegment*>(geom);
Base::Vector3d startVec = seg->getStartPoint();
Base::Vector3d endVec = seg->getEndPoint();
if (endpoint == PointPos::start) {
Base::Vector3d newPoint = startVec - endVec;
double scaleFactor = newPoint.Length() + increment;
newPoint.Normalize();
newPoint.Scale(scaleFactor, scaleFactor, scaleFactor);
newPoint = newPoint + endVec;
retcode = moveGeometry(GeoId, Sketcher::PointPos::start, newPoint, false, true);
}
else if (endpoint == PointPos::end) {
Base::Vector3d newPoint = endVec - startVec;
double scaleFactor = newPoint.Length() + increment;
newPoint.Normalize();
newPoint.Scale(scaleFactor, scaleFactor, scaleFactor);
newPoint = newPoint + startVec;
retcode = moveGeometry(GeoId, Sketcher::PointPos::end, newPoint, false, true);
}
}
else if (geom->is<Part::GeomArcOfCircle>()) {
auto* arc = static_cast<Part::GeomArcOfCircle*>(geom);
double startArc, endArc;
arc->getRange(startArc, endArc, true);
if (endpoint == PointPos::start) {
arc->setRange(startArc - increment, endArc, true);
retcode = 0;
}
else if (endpoint == PointPos::end) {
arc->setRange(startArc, endArc + increment, true);
retcode = 0;
}
}
if (retcode == 0 && noRecomputes) {
solve();
}
return retcode;
}
std::unique_ptr<Constraint> SketchObject::createConstraint(
Sketcher::ConstraintType constrType, int firstGeoId, Sketcher::PointPos firstPos,
int secondGeoId, Sketcher::PointPos secondPos, int thirdGeoId, Sketcher::PointPos thirdPos)
{
auto newConstr = std::make_unique<Sketcher::Constraint>();
newConstr->Type = constrType;
newConstr->First = firstGeoId;
newConstr->FirstPos = firstPos;
newConstr->Second = secondGeoId;
newConstr->SecondPos = secondPos;
newConstr->Third = thirdGeoId;
newConstr->ThirdPos = thirdPos;
return newConstr;
}
void SketchObject::addConstraint(Sketcher::ConstraintType constrType, int firstGeoId,
Sketcher::PointPos firstPos, int secondGeoId,
Sketcher::PointPos secondPos, int thirdGeoId,
Sketcher::PointPos thirdPos)
{
auto newConstr = createConstraint(
constrType, firstGeoId, firstPos, secondGeoId, secondPos, thirdGeoId, thirdPos);
this->addConstraint(std::move(newConstr));
}
std::unique_ptr<Constraint>
SketchObject::getConstraintAfterDeletingGeo(const Constraint* constr,
const int deletedGeoId) const
{
if (!constr) {
return nullptr;
}
// TODO: While this is not incorrect, it recreates all constraints regardless of whether or not we need to.
auto newConstr = std::unique_ptr<Constraint>(constr->clone());
changeConstraintAfterDeletingGeo(newConstr.get(), deletedGeoId);
if (newConstr->Type == ConstraintType::None) {
return nullptr;
}
return newConstr;
}
void SketchObject::changeConstraintAfterDeletingGeo(Constraint* constr,
const int deletedGeoId) const
{
if (!constr) {
return;
}
if (constr->involvesGeoId(deletedGeoId)) {
constr->Type = ConstraintType::None;
return;
}
int step = 1;
std::function<bool (const int&)> needsUpdate = [&deletedGeoId](const int& givenId) -> bool {
return givenId > deletedGeoId;
};
if (deletedGeoId < 0) {
step = -1;
needsUpdate = [&deletedGeoId](const int& givenId) -> bool {
return givenId < deletedGeoId && givenId != GeoEnum::GeoUndef;
};
}
if (needsUpdate(constr->First)) {
constr->First -= step;
}
if (needsUpdate(constr->Second)) {
constr->Second -= step;
}
if (needsUpdate(constr->Third)) {
constr->Third -= step;
}
}
// clang-format on
bool SketchObject::seekTrimPoints(
int GeoId,
const Base::Vector3d& point,
int& GeoId1,
Base::Vector3d& intersect1,
int& GeoId2,
Base::Vector3d& intersect2
)
{
if (GeoId < 0 || GeoId > getHighestCurveIndex()) {
return false;
}
auto geos = getCompleteGeometry(); // this includes the axes too
geos.resize(geos.size() - 2); // remove the axes to avoid intersections with the axes
int localindex1, localindex2;
// Not found in will be returned as -1, not as GeoUndef, Part WB is agnostic to the concept of
// GeoUndef
if (!Part2DObject::seekTrimPoints(geos, GeoId, point, localindex1, intersect1, localindex2, intersect2)) {
return false;
}
// invalid complete geometry indices are mapped to GeoUndef
GeoId1 = getGeoIdFromCompleteGeometryIndex(localindex1);
GeoId2 = getGeoIdFromCompleteGeometryIndex(localindex2);
return true;
}
// given a geometry and a point, returns the corresponding parameter of the geometry point
// closest to the point. Wrapped around a try-catch so the calling operation can fail without
// throwing an exception.
bool getIntersectionParameter(const Part::Geometry* geo, const Base::Vector3d point, double& pointParam)
{
const auto* curve = static_cast<const Part::GeomCurve*>(geo);
try {
curve->closestParameter(point, pointParam);
}
catch (Base::CADKernelError& e) {
e.reportException();
return false;
}
return true;
}
bool arePointsWithinPrecision(const Base::Vector3d& point1, const Base::Vector3d& point2)
{
// From testing: 500x (or 0.000050) is needed in order to not falsely distinguish points
// calculated with seekTrimPoints
return ((point1 - point2).Length() < 500 * Precision::Confusion());
}
bool areParamsWithinApproximation(double param1, double param2)
{
// From testing: 500x (or 0.000050) is needed in order to not falsely distinguish points
// calculated with seekTrimPoints
return (std::abs(param1 - param2) < Precision::PApproximation());
}
// returns true if the point defined by (GeoId1, pos1) can be considered to be coincident with
// point.
bool isPointAtPosition(const SketchObject* obj, int GeoId1, PointPos pos1, const Base::Vector3d& point)
{
Base::Vector3d pp = obj->getPoint(GeoId1, pos1);
return arePointsWithinPrecision(point, pp);
}
// Checks whether preexisting constraints must be converted to new constraints.
// Preexisting point on object constraints get converted to coincidents.
// Returns:
// - The constraint that should be used to constraint GeoId and cuttingGeoId
std::unique_ptr<Constraint> transformPreexistingConstraintForTrim(
const SketchObject* obj,
const Constraint* constr,
int GeoId,
int cuttingGeoId,
const Base::Vector3d& cutPointVec,
int newGeoId,
PointPos newPosId
)
{
/* TODO: It is possible that the trimming entity has both a PointOnObject constraint to the
* trimmed entity, and a simple Tangent constraint to the trimmed entity. In this case we
* want to change to a single end-to-end tangency, i.e we want to ensure that constrType1
* is set to Sketcher::Tangent, that the secondPos1 is captured from the PointOnObject,
* and also make sure that the PointOnObject constraint is deleted.
*/
// TODO: Symmetric and distance constraints (sometimes together) can be changed to something
std::unique_ptr<Constraint> newConstr;
if (cuttingGeoId == GeoEnum::GeoUndef || !constr->involvesGeoId(cuttingGeoId)
|| !constr->involvesGeoIdAndPosId(GeoId, PointPos::none)) {
return newConstr;
}
switch (constr->Type) {
case PointOnObject: {
// we might want to transform this (and the new point-on-object constraints) into a
// coincidence At this stage of the check the point has to be an end of `cuttingGeoId`
// on the edge of `GeoId`.
if (isPointAtPosition(obj, constr->First, constr->FirstPos, cutPointVec)) {
// We already know the point-on-object is on the whole of GeoId
newConstr.reset(constr->copy());
newConstr->Type = Sketcher::Coincident;
newConstr->Second = newGeoId;
newConstr->SecondPos = newPosId;
}
break;
}
case Tangent:
case Perpendicular: {
// These may have to be turned into endpoint-to-endpoint or endpoint-to-edge
// TODO: could there be tangent/perpendicular constraints not involving the trim that
// are modified below?
newConstr.reset(constr->copy());
newConstr->substituteIndexAndPos(GeoId, PointPos::none, newGeoId, newPosId);
// make sure the first position is a point
if (newConstr->FirstPos == PointPos::none) {
std::swap(newConstr->First, newConstr->Second);
std::swap(newConstr->FirstPos, newConstr->SecondPos);
}
// there is no need for the third point if it exists
newConstr->Third = GeoEnum::GeoUndef;
newConstr->ThirdPos = PointPos::none;
break;
}
default:
break;
}
return newConstr;
}
std::unique_ptr<Constraint> getNewConstraintAtTrimCut(
const SketchObject* obj,
int cuttingGeoId,
int cutGeoId,
PointPos cutPosId,
const Base::Vector3d& cutPointVec
)
{
auto newConstr = std::make_unique<Sketcher::Constraint>();
newConstr->First = cutGeoId;
newConstr->FirstPos = cutPosId;
newConstr->Second = cuttingGeoId;
if (isPointAtPosition(obj, cuttingGeoId, PointPos::start, cutPointVec)) {
newConstr->Type = Sketcher::Coincident;
newConstr->SecondPos = PointPos::start;
}
else if (isPointAtPosition(obj, cuttingGeoId, PointPos::end, cutPointVec)) {
newConstr->Type = Sketcher::Coincident;
newConstr->SecondPos = PointPos::end;
}
else {
// Points are sufficiently far apart: use point-on-object
newConstr->Type = Sketcher::PointOnObject;
newConstr->SecondPos = PointPos::none;
}
return newConstr;
}
bool isGeoIdAllowedForTrim(const SketchObject* obj, int GeoId)
{
const auto* geo = obj->getGeometry(GeoId);
return GeoId >= 0 && GeoId <= obj->getHighestCurveIndex()
&& GeometryFacade::isInternalType(geo, InternalType::None);
}
bool getParamLimitsOfNewGeosForTrim(
const SketchObject* obj,
int GeoId,
std::array<int, 2>& cuttingGeoIds,
std::array<Base::Vector3d, 2>& cutPoints,
std::vector<std::pair<double, double>>& paramsOfNewGeos
)
{
const auto* geoAsCurve = obj->getGeometry<Part::GeomCurve>(GeoId);
double firstParam = geoAsCurve->getFirstParameter();
double lastParam = geoAsCurve->getLastParameter();
double cut0Param {firstParam}, cut1Param {lastParam};
bool allParamsFound = getIntersectionParameter(geoAsCurve, cutPoints[0], cut0Param)
&& getIntersectionParameter(geoAsCurve, cutPoints[1], cut1Param);
if (!allParamsFound) {
return false;
}
if (!obj->isClosedCurve(geoAsCurve) && areParamsWithinApproximation(firstParam, cut0Param)) {
cuttingGeoIds[0] = GeoEnum::GeoUndef;
}
if (!obj->isClosedCurve(geoAsCurve) && areParamsWithinApproximation(lastParam, cut1Param)) {
cuttingGeoIds[1] = GeoEnum::GeoUndef;
}
size_t numUndefs = std::count(cuttingGeoIds.begin(), cuttingGeoIds.end(), GeoEnum::GeoUndef);
if (numUndefs == 0 && arePointsWithinPrecision(cutPoints[0], cutPoints[1])) {
// If both points are detected and are coincident, deletion is the only option.
paramsOfNewGeos.clear();
return true;
}
paramsOfNewGeos.assign(2 - numUndefs, {firstParam, lastParam});
if (paramsOfNewGeos.empty()) {
return true;
}
if (obj->isClosedCurve(geoAsCurve)) {
paramsOfNewGeos.pop_back();
}
if (cuttingGeoIds[0] != GeoEnum::GeoUndef) {
paramsOfNewGeos.front().second = cut0Param;
}
if (cuttingGeoIds[1] != GeoEnum::GeoUndef) {
paramsOfNewGeos.back().first = cut1Param;
}
return true;
}
void createArcsFromGeoWithLimits(
const Part::GeomCurve* geo,
const std::vector<std::pair<double, double>>& paramsOfNewGeos,
std::vector<Part::Geometry*>& newGeos
)
{
for (auto& [u1, u2] : paramsOfNewGeos) {
auto newGeo = static_cast<const Part::GeomCurve*>(geo)->createArc(u1, u2);
assert(newGeo);
newGeos.emplace_back(newGeo);
}
}
void createNewConstraintsForTrim(
const SketchObject* obj,
const int GeoId,
const std::array<int, 2>& cuttingGeoIds,
const std::array<Base::Vector3d, 2>& cutPoints,
const std::vector<int>& newIds,
const std::vector<const Part::Geometry*> newGeos,
std::vector<int>& idsOfOldConstraints,
std::vector<Constraint*>& newConstraints,
std::set<int, std::greater<>>& geoIdsToBeDeleted
)
{
const auto& allConstraints = obj->Constraints.getValues();
bool isPoint1ConstrainedOnGeoId1 = false;
bool isPoint2ConstrainedOnGeoId2 = false;
for (const auto& oldConstrId : idsOfOldConstraints) {
// trim-specific changes first
const Constraint* con = allConstraints[oldConstrId];
if (con->Type == InternalAlignment) {
geoIdsToBeDeleted.insert(con->First);
continue;
}
if (auto newConstr = transformPreexistingConstraintForTrim(
obj,
con,
GeoId,
cuttingGeoIds[0],
cutPoints[0],
newIds.front(),
PointPos::end
)) {
newConstraints.push_back(newConstr.release());
isPoint1ConstrainedOnGeoId1 = true;
continue;
}
if (auto newConstr = transformPreexistingConstraintForTrim(
obj,
con,
GeoId,
cuttingGeoIds[1],
cutPoints[1],
newIds.back(),
PointPos::start
)) {
newConstraints.push_back(newConstr.release());
isPoint2ConstrainedOnGeoId2 = true;
continue;
}
// We have already transferred all constraints on endpoints to the new pieces.
// If there is still any left, this means one of the remaining pieces was degenerate.
if (!(con->Type == Angle || con->involvesGeoIdAndPosId(GeoId, PointPos::none))) {
continue;
}
// constraint has not yet been changed
obj->deriveConstraintsForPieces(GeoId, newIds, newGeos, con, newConstraints);
}
// Add point-on-object/coincidence constraints with the newly exposed points.
// This will need to account for the constraints that were already converted
// to coincident or end-to-end tangency/perpendicularity.
// TODO: Tangent/perpendicular not yet covered
if (cuttingGeoIds[0] != GeoEnum::GeoUndef && !isPoint1ConstrainedOnGeoId1) {
newConstraints.emplace_back(
getNewConstraintAtTrimCut(obj, cuttingGeoIds[0], newIds.front(), PointPos::end, cutPoints[0])
.release()
);
}
if (cuttingGeoIds[1] != GeoEnum::GeoUndef && !isPoint2ConstrainedOnGeoId2) {
newConstraints.emplace_back(
getNewConstraintAtTrimCut(obj, cuttingGeoIds[1], newIds.back(), PointPos::start, cutPoints[1])
.release()
);
}
}
std::optional<size_t> findPieceContainingPoint(
const SketchObject* obj,
const Part::Geometry* geo,
const Base::Vector3d& point,
const std::vector<int>& newIds,
const std::vector<const Part::Geometry*>& newGeos
)
{
double conParam;
auto* geoAsCurve = static_cast<const Part::GeomCurve*>(geo);
geoAsCurve->closestParameter(point, conParam);
// Choose based on where the closest point lies
// If it's not there, just leave this constraint out
for (size_t i = 0; i < newIds.size(); ++i) {
double newGeoFirstParam = static_cast<const Part::GeomCurve*>(newGeos[i])->getFirstParameter();
double newGeoLastParam = static_cast<const Part::GeomCurve*>(newGeos[i])->getLastParameter();
// For periodic curves the point may need a full revolution
if ((newGeoFirstParam - conParam) > Precision::PApproximation() && obj->isClosedCurve(geo)) {
conParam += (geoAsCurve->getLastParameter() - geoAsCurve->getFirstParameter());
}
if ((newGeoFirstParam - conParam) <= Precision::PApproximation()
&& (conParam - newGeoLastParam) <= Precision::PApproximation()) {
return i;
}
}
return std::nullopt;
}
int SketchObject::trim(int GeoId, const Base::Vector3d& point)
{
if (!isGeoIdAllowedForTrim(this, GeoId)) {
return -1;
}
// Remove internal geometry beforehand for now
// FIXME: we should be able to transfer these to new curves smoothly
// auto geo = getGeometry(GeoId);
const auto* geoAsCurve = getGeometry<Part::GeomCurve>(GeoId);
if (geoAsCurve == nullptr) {
return -1;
}
bool isOriginalCurveConstruction = GeometryFacade::getConstruction(geoAsCurve);
bool isOriginalCurvePeriodic = isClosedCurve(geoAsCurve);
//******************* Step A => Detection of intersection - Common to all Geometries
//****************************************//
// GeoIds intersecting the curve around `point`
std::array<int, 2> cuttingGeoIds {GeoEnum::GeoUndef, GeoEnum::GeoUndef};
// Points at the intersection
std::array<Base::Vector3d, 2> cutPoints;
// Using SketchObject wrapper, as Part2DObject version returns GeoId = -1 when intersection not
// found, which is wrong for a GeoId (axis). seekTrimPoints returns:
// - For a parameter associated with "point" between an intersection and the end point
// (non-periodic case) cuttingGeoIds[0] != GeoUndef and cuttingGeoIds[1] == GeoUndef
// - For a parameter associated with "point" between the start point and an intersection
// (non-periodic case) cuttingGeoIds[1] != GeoUndef and cuttingGeoIds[0] == GeoUndef
// - For a parameter associated with "point" between two intersection points, cuttingGeoIds[0]
// != GeoUndef and cuttingGeoIds[1] != GeoUndef
//
// FirstParam < point1param < point2param < LastParam
if (!SketchObject::seekTrimPoints(
GeoId,
point,
cuttingGeoIds[0],
cutPoints[0],
cuttingGeoIds[1],
cutPoints[1]
)) {
// If no suitable trim points are found, then trim defaults to deleting the geometry
delGeometry(GeoId);
return 0;
}
// TODO: find trim parameters
std::vector<std::pair<double, double>> paramsOfNewGeos;
paramsOfNewGeos.reserve(2);
if (!getParamLimitsOfNewGeosForTrim(this, GeoId, cuttingGeoIds, cutPoints, paramsOfNewGeos)) {
return -1;
}
//******************* Step B => Creation of new geometries
//****************************************//
std::vector<int> newIds;
std::vector<Part::Geometry*> newGeos;
std::vector<const Part::Geometry*> newGeosAsConsts;
switch (paramsOfNewGeos.size()) {
case 0: {
delGeometry(GeoId);
return 0;
}
case 1: {
newIds.push_back(GeoId);
break;
}
case 2: {
newIds.push_back(GeoId);
newIds.push_back(getHighestCurveIndex() + 1);
break;
}
default: {
return -1;
}
}
createArcsFromGeoWithLimits(geoAsCurve, paramsOfNewGeos, newGeos);
for (const auto* geo : newGeos) {
newGeosAsConsts.push_back(geo);
}
//******************* Step C => Creation of new constraints
//****************************************//
// Now that we have the new curves, change constraints as needed
// Some are covered with `deriveConstraintsForPieces`, others are specific to trim
// FIXME: We are using non-smart pointers since that's what's needed in `addConstraints`.
const auto& allConstraints = this->Constraints.getValues();
std::vector<Constraint*> newConstraints;
std::vector<int> idsOfOldConstraints;
std::set<int, std::greater<>> geoIdsToBeDeleted;
getConstraintIndices(GeoId, idsOfOldConstraints);
// remove the constraints that we want to manually transfer
// We could transfer beforehand but in case of exception that transfer is permanent
if (!isOriginalCurvePeriodic) {
std::erase_if(idsOfOldConstraints, [&GeoId, &allConstraints, &cuttingGeoIds](const auto& i) {
auto* constr = allConstraints[i];
bool involvesStart = constr->involvesGeoIdAndPosId(GeoId, PointPos::start);
bool involvesEnd = constr->involvesGeoIdAndPosId(GeoId, PointPos::end);
bool keepStart = cuttingGeoIds[0] != GeoEnum::GeoUndef;
bool keepEnd = cuttingGeoIds[1] != GeoEnum::GeoUndef;
bool involvesBothButNotBothKept = involvesStart && involvesEnd && !(keepStart && keepEnd);
return !involvesBothButNotBothKept
&& ((involvesStart && keepStart) || (involvesEnd && keepEnd));
});
}
std::erase_if(idsOfOldConstraints, [&GeoId, &allConstraints](const auto& i) {
return (allConstraints[i]->involvesGeoIdAndPosId(GeoId, PointPos::mid));
});
createNewConstraintsForTrim(
this,
GeoId,
cuttingGeoIds,
cutPoints,
newIds,
newGeosAsConsts,
idsOfOldConstraints,
newConstraints,
geoIdsToBeDeleted
);
//******************* Step D => Replacing geometries and constraints
//****************************************//
// Constraints related to start/mid/end points of original
[[maybe_unused]] auto constrainAsEqual = [this](int GeoId1, int GeoId2) {
auto newConstr = std::make_unique<Sketcher::Constraint>();
// Build Constraints associated with new pair of arcs
newConstr->Type = Sketcher::Equal;
newConstr->First = GeoId1;
newConstr->FirstPos = Sketcher::PointPos::none;
newConstr->Second = GeoId2;
newConstr->SecondPos = Sketcher::PointPos::none;
addConstraint(std::move(newConstr));
};
delConstraints(std::move(idsOfOldConstraints), DeleteOption::NoFlag);
if (!isOriginalCurvePeriodic) {
transferConstraints(GeoId, PointPos::start, newIds.front(), PointPos::start, true);
transferConstraints(GeoId, PointPos::end, newIds.back(), PointPos::end, true);
}
bool geomHasMid = geoAsCurve->isDerivedFrom<Part::GeomConic>()
|| geoAsCurve->isDerivedFrom<Part::GeomArcOfConic>();
if (geomHasMid) {
transferConstraints(GeoId, PointPos::mid, newIds.front(), PointPos::mid, true);
// Make centers coincident
if (newIds.size() > 1) {
auto* joint = new Constraint();
joint->Type = Coincident;
joint->First = newIds.front();
joint->FirstPos = PointPos::mid;
joint->Second = newIds.back();
joint->SecondPos = PointPos::mid;
newConstraints.push_back(joint);
// Any radius etc. equality constraints here
// TODO: There could be some form of equality between the constraints here. However, it
// may happen that this is imposed by an elaborate set of additional constraints. When
// that happens, this causes redundant constraints, and in worse cases (incorrect)
// complaints of over-constraint and solver failures.
// if (std::ranges::none_of(newConstraints, [](const auto& constr) {
// return constr->Type == ConstraintType::Equal;
// })) {
// constrainAsEqual(newIds.front(), newIds.back());
// }
// TODO: ensure alignment as well?
}
}
replaceGeometries({GeoId}, newGeos);
for (auto newId : newIds) {
setConstruction(newId, isOriginalCurveConstruction);
}
if (noRecomputes) {
solve();
}
for (auto& deletedGeoId : geoIdsToBeDeleted) {
for (auto& cons : newConstraints) {
changeConstraintAfterDeletingGeo(cons, deletedGeoId);
}
}
std::erase_if(newConstraints, [](const auto& constr) {
return constr->Type == ConstraintType::None;
});
delGeometries(geoIdsToBeDeleted.begin(), geoIdsToBeDeleted.end());
addConstraints(newConstraints);
if (noRecomputes) {
solve();
}
//******************* Cleanup
//****************************************//
// Since we used regular "non-smart" pointers, we have to handle cleanup
for (auto& cons : newConstraints) {
delete cons;
}
return 0;
}
bool SketchObject::deriveConstraintsForPieces(
const int oldId,
const std::vector<int>& newIds,
const Constraint* con,
std::vector<Constraint*>& newConstraints
) const
{
std::vector<const Part::Geometry*> newGeos;
for (auto& newId : newIds) {
newGeos.push_back(getGeometry(newId));
}
return deriveConstraintsForPieces(oldId, newIds, newGeos, con, newConstraints);
}
bool SketchObject::deriveConstraintsForPieces(
const int oldId,
const std::vector<int>& newIds,
const std::vector<const Part::Geometry*>& newGeos,
const Constraint* con,
std::vector<Constraint*>& newConstraints
) const
{
const Part::Geometry* geo = getGeometry(oldId);
int conId = con->First;
PointPos conPos = con->FirstPos;
if (conId == oldId) {
conId = con->Second;
conPos = con->SecondPos;
}
bool newGeosLikelyNotCreated = std::ranges::find(newGeos, nullptr) != newGeos.end();
bool transferToAll = false;
switch (con->Type) {
case Horizontal:
case Vertical:
case Parallel: {
transferToAll = geo->is<Part::GeomLineSegment>();
} break;
case Tangent:
case Perpendicular: {
if (geo->is<Part::GeomLineSegment>()) {
transferToAll = true;
break;
}
const Part::Geometry* conGeo = getGeometry(conId);
if (!(conGeo && conGeo->isDerivedFrom<Part::GeomCurve>())) {
return false;
}
// no use going forward if newGeos aren't ready
if (newGeosLikelyNotCreated) {
break;
}
// For now: just transfer to the first intersection
// TODO: Actually check that there was perpendicularity earlier
// TODO: Choose piece based on parameters ("values" of the constraint)
for (size_t i = 0; i < newIds.size(); ++i) {
std::vector<std::pair<Base::Vector3d, Base::Vector3d>> intersections;
bool intersects
= static_cast<const Part::GeomCurve*>(newGeos[i])
->intersect(static_cast<const Part::GeomCurve*>(conGeo), intersections);
if (intersects) {
Constraint* trans = con->copy();
trans->substituteIndex(oldId, newIds[i]);
newConstraints.push_back(trans);
return true;
}
}
} break;
case Angle: {
const auto [thirdGeo, thirdPos] = con->getElement(2);
if (thirdGeo == oldId) {
// TODO: transfer to a coincident point,
// is it possible to do it somewhere else and avoid?
std::vector<int> GeoIdList;
std::vector<PointPos> PosIdList;
getDirectlyCoincidentPoints(thirdGeo, thirdPos, GeoIdList, PosIdList);
if (GeoIdList.size() <= 1) {
// TODO: Even in this case we can add a point
return false;
}
// transfer only to the curve that actually intersects
Base::Vector3d point(getPoint(thirdGeo, thirdPos));
std::optional<size_t> idx = findPieceContainingPoint(this, geo, point, newIds, newGeos);
if (idx.has_value()) {
Constraint* trans = con->copy();
trans->substituteIndexAndPos(GeoIdList[0], PosIdList[0], GeoIdList[1], PosIdList[1]);
trans->substituteIndex(oldId, newIds[idx.value()]);
newConstraints.push_back(trans);
return true;
}
}
else if (thirdGeo != GeoEnum::GeoUndef) {
// Angle via point but the point won't change, can transfer to all or first
// transfer only to the curve that actually intersects
Base::Vector3d point(getPoint(thirdGeo, thirdPos));
std::optional<size_t> idx = findPieceContainingPoint(this, geo, point, newIds, newGeos);
if (idx.has_value()) {
Constraint* trans = con->copy();
trans->substituteIndex(oldId, newIds[idx.value()]);
newConstraints.push_back(trans);
return true;
}
break;
}
else if (std::ranges::any_of(newGeos, [](const Part::Geometry* geo) {
return !geo->is<Part::GeomLineSegment>();
})) {
// Angle without a specific point is only supported when _all_ geometries are lines.
// If the original was a line, we may reach this point, for example, when converting
// it to NURBS.
// NOTE: We may decide to change this logic in the future. Follows
// `Sketch::addConstraint`.
return false;
}
else {
// Straight up angle, can transfer to all or first
transferToAll = true;
break;
}
} break;
case Distance:
case DistanceX:
case DistanceY:
case PointOnObject: {
if (con->FirstPos == PointPos::none && con->SecondPos == PointPos::none
&& newIds.size() > 1) {
Constraint* dist = con->copy();
dist->First = newIds.front();
dist->FirstPos = PointPos::start;
dist->Second = newIds.back();
dist->SecondPos = PointPos::end;
newConstraints.push_back(dist);
return true;
}
if (conId == GeoEnum::GeoUndef || newGeosLikelyNotCreated) {
// nothing further to do
return false;
}
Base::Vector3d conPoint(getPoint(conId, conPos));
double conParam;
auto* geoAsCurve = static_cast<const Part::GeomCurve*>(geo);
geoAsCurve->closestParameter(conPoint, conParam);
// Choose based on where the closest point lies
// If it's not there, just leave this constraint out
for (size_t i = 0; i < newIds.size(); ++i) {
double newGeoFirstParam
= static_cast<const Part::GeomCurve*>(newGeos[i])->getFirstParameter();
double newGeoLastParam
= static_cast<const Part::GeomCurve*>(newGeos[i])->getLastParameter();
// For periodic curves the point may need a full revolution
if ((newGeoFirstParam - conParam) > Precision::PApproximation()
&& isClosedCurve(geo)) {
conParam += (geoAsCurve->getLastParameter() - geoAsCurve->getFirstParameter());
}
if ((newGeoFirstParam - conParam) <= Precision::PApproximation()
&& (conParam - newGeoLastParam) <= Precision::PApproximation()) {
Constraint* trans = con->copy();
trans->First = conId;
trans->FirstPos = conPos;
trans->Second = newIds[i];
trans->SecondPos = PointPos::none;
newConstraints.push_back(trans);
return true;
}
}
} break;
case Radius:
case Diameter:
case Equal: {
// Only transfer to one of them (arbitrarily chosen here as the first) and only if the
// curve is a conic or its arc
// TODO: Some equalities may be transferred, using something along the lines of
// `getDirectlyCoincidentPoints`
if (geo->isDerivedFrom<Part::GeomConic>() || geo->isDerivedFrom<Part::GeomArcOfConic>()) {
Constraint* trans = con->copy();
trans->substituteIndex(oldId, newIds.front());
newConstraints.push_back(trans);
break;
}
} break;
default:
// Release other constraints
break;
}
if (!transferToAll) {
return false;
}
for (auto& newId : newIds) {
Constraint* trans = con->copy();
trans->substituteIndex(oldId, newId);
newConstraints.push_back(trans);
}
return true;
}
int SketchObject::split(int GeoId, const Base::Vector3d& point)
{
// No need to check input data validity as this is an sketchobject managed operation
Base::StateLocker lock(managedoperation, true);
if (GeoId < 0 || GeoId > getHighestCurveIndex()) {
return -1;
}
// FIXME: we should be able to transfer these to new curves smoothly
deleteUnusedInternalGeometryAndUpdateGeoId(GeoId);
const auto* geoAsCurve = getGeometry<Part::GeomCurve>(GeoId);
bool isOriginalCurvePeriodic = isClosedCurve(geoAsCurve);
std::vector<int> newIds;
std::vector<Part::Geometry*> newGeos;
std::vector<Constraint*> newConstraints;
double splitParam;
geoAsCurve->closestParameter(point, splitParam);
// TODO: find trim parameters
std::vector<std::pair<double, double>> paramsOfNewGeos(
isOriginalCurvePeriodic ? 1 : 2,
{geoAsCurve->getFirstParameter(), geoAsCurve->getLastParameter()}
);
paramsOfNewGeos.front().second = isOriginalCurvePeriodic
? (splitParam + geoAsCurve->getLastParameter() - geoAsCurve->getFirstParameter())
: splitParam;
paramsOfNewGeos.back().first = splitParam;
switch (paramsOfNewGeos.size()) {
case 0: {
delGeometry(GeoId);
return 0;
}
case 1: {
newIds.push_back(GeoId);
break;
}
case 2: {
newIds.push_back(GeoId);
newIds.push_back(getHighestCurveIndex() + 1);
break;
}
default: {
return -1;
}
}
createArcsFromGeoWithLimits(geoAsCurve, paramsOfNewGeos, newGeos);
std::vector<int> idsOfOldConstraints;
getConstraintIndices(GeoId, idsOfOldConstraints);
const auto& allConstraints = this->Constraints.getValues();
std::erase_if(idsOfOldConstraints, [&GeoId, &allConstraints](const auto& i) {
return !allConstraints[i]->involvesGeoIdAndPosId(GeoId, PointPos::none);
});
for (const auto& oldConstrId : idsOfOldConstraints) {
Constraint* con = allConstraints[oldConstrId];
deriveConstraintsForPieces(GeoId, newIds, con, newConstraints);
}
// This also seems to reset SketchObject::Geometry.
// TODO: figure out why, and if that check must be used
geoAsCurve = getGeometry<Part::GeomCurve>(GeoId);
if (!isOriginalCurvePeriodic) {
auto* joint = new Constraint();
joint->Type = Coincident;
joint->First = newIds.front();
joint->FirstPos = PointPos::end;
joint->Second = newIds.back();
joint->SecondPos = PointPos::start;
newConstraints.push_back(joint);
transferConstraints(GeoId, PointPos::start, newIds.front(), PointPos::start);
transferConstraints(GeoId, PointPos::end, newIds.back(), PointPos::end);
}
// This additional constraint is there to maintain existing behavior.
// TODO: Decide whether to remove it altogether or also apply to other curves with centers.
if (geoAsCurve->is<Part::GeomArcOfCircle>()) {
auto* joint = new Constraint();
joint->Type = Coincident;
joint->First = newIds.front();
joint->FirstPos = PointPos::mid;
joint->Second = newIds.back();
joint->SecondPos = PointPos::mid;
newConstraints.push_back(joint);
}
if (geoAsCurve->isDerivedFrom<Part::GeomConic>()
|| geoAsCurve->isDerivedFrom<Part::GeomArcOfConic>()) {
transferConstraints(GeoId, PointPos::mid, newIds.front(), PointPos::mid);
}
delConstraints(std::move(idsOfOldConstraints), DeleteOption::NoSolve);
replaceGeometries({GeoId}, newGeos);
addConstraints(newConstraints);
// `if (noRecomputes)` results in a failed test (`testPD_TNPSketchPadSketchSplit(self)`)
// TODO: figure out why, and if that check must be used
solve();
for (auto& cons : newConstraints) {
delete cons;
}
return 0;
}
int SketchObject::join(
int geoId1,
Sketcher::PointPos posId1,
int geoId2,
Sketcher::PointPos posId2,
int continuity
)
{
// No need to check input data validity as this is an sketchobject managed operation
Base::StateLocker lock(managedoperation, true);
if (Sketcher::PointPos::start != posId1 && Sketcher::PointPos::end != posId1
&& Sketcher::PointPos::start != posId2 && Sketcher::PointPos::end != posId2) {
THROWM(ValueError, "Invalid positions: points must be start or end points of a curve.");
return -1;
}
if (geoId1 == geoId2) {
THROWM(ValueError, "Connecting the end points of the same curve is not yet supported.");
return -1;
}
if (geoId1 < 0 || geoId1 > getHighestCurveIndex() || geoId2 < 0
|| geoId2 > getHighestCurveIndex()) {
return -1;
}
// get the old splines
auto* geo1 = dynamic_cast<const Part::GeomCurve*>(getGeometry(geoId1));
auto* geo2 = dynamic_cast<const Part::GeomCurve*>(getGeometry(geoId2));
if (GeometryFacade::getConstruction(geo1) != GeometryFacade::getConstruction(geo2)) {
THROWM(ValueError, "Cannot join construction and non-construction geometries.");
return -1;
}
bool areOriginalCurvesConstruction = GeometryFacade::getConstruction(geo1);
// TODO: make both curves b-splines here itself
if (!geo1 || !geo2) {
return -1;
}
// TODO: is there a cleaner way to get our mutable bsp's?
// we need the splines to be mutable because we may reverse them
// and/or change their degree
std::unique_ptr<Part::GeomBSplineCurve> bsp1(
geo1->toNurbs(geo1->getFirstParameter(), geo1->getLastParameter())
);
std::unique_ptr<Part::GeomBSplineCurve> bsp2(
geo2->toNurbs(geo2->getFirstParameter(), geo2->getLastParameter())
);
if (bsp1->isPeriodic() || bsp2->isPeriodic()) {
THROWM(ValueError, "It is only possible to join non-periodic curves.");
return -1;
}
// reverse the splines if needed: join end of 1st to start of 2nd
if (Sketcher::PointPos::start == posId1) {
bsp1->reverse();
}
if (Sketcher::PointPos::end == posId2) {
bsp2->reverse();
}
// ensure the degrees of both curves are the same
if (bsp1->getDegree() < bsp2->getDegree()) {
bsp1->increaseDegree(bsp2->getDegree());
}
else if (bsp2->getDegree() < bsp1->getDegree()) {
bsp2->increaseDegree(bsp1->getDegree());
}
// TODO: Check for tangent constraint here
bool makeC1Continuous = (continuity >= 1);
// TODO: Rescale one or both sections to fulfill some purpose.
// This could include making param between [0,1], and/or making
// C1 continuity possible.
if (makeC1Continuous) {
// We assume here that there is already G1 continuity.
// Just scale parameters to get C1.
Base::Vector3d slope1 = bsp1->firstDerivativeAtParameter(bsp1->getLastParameter());
Base::Vector3d slope2 = bsp2->firstDerivativeAtParameter(bsp2->getFirstParameter());
// TODO: slope2 can technically be a zero vector
// But that seems not possible unless the spline is trivial.
// Prove or account for the possibility.
double scale = slope2.Length() / slope1.Length();
bsp2->scaleKnotsToBounds(0, scale * (bsp2->getLastParameter() - bsp2->getFirstParameter()));
}
// set up vectors for new poles, knots, mults
std::vector<Base::Vector3d> poles1 = bsp1->getPoles();
std::vector<double> weights1 = bsp1->getWeights();
std::vector<double> knots1 = bsp1->getKnots();
std::vector<int> mults1 = bsp1->getMultiplicities();
std::vector<Base::Vector3d> poles2 = bsp2->getPoles();
std::vector<double> weights2 = bsp2->getWeights();
std::vector<double> knots2 = bsp2->getKnots();
std::vector<int> mults2 = bsp2->getMultiplicities();
std::vector<Base::Vector3d> newPoles(std::move(poles1));
std::vector<double> newWeights(std::move(weights1));
std::vector<double> newKnots(std::move(knots1));
std::vector<int> newMults(std::move(mults1));
poles2.erase(poles2.begin());
if (makeC1Continuous) {
newPoles.erase(newPoles.end() - 1);
}
newPoles.insert(
newPoles.end(),
std::make_move_iterator(poles2.begin()),
std::make_move_iterator(poles2.end())
);
// TODO: Weights might need to be scaled
weights2.erase(weights2.begin());
if (makeC1Continuous) {
newWeights.erase(newWeights.end() - 1);
}
newWeights.insert(
newWeights.end(),
std::make_move_iterator(weights2.begin()),
std::make_move_iterator(weights2.end())
);
// knots of the second spline come after all of the first
double offset = newKnots.back() - knots2.front();
knots2.erase(knots2.begin());
for (auto& knot : knots2) {
knot += offset;
}
newKnots.insert(
newKnots.end(),
std::make_move_iterator(knots2.begin()),
std::make_move_iterator(knots2.end())
);
// end knots can have a multiplicity of (degree + 1)
if (bsp1->getDegree() < newMults.back()) {
newMults.back() = bsp1->getDegree();
if (makeC1Continuous) {
newMults.back() -= 1;
}
}
mults2.erase(mults2.begin());
newMults.insert(
newMults.end(),
std::make_move_iterator(mults2.begin()),
std::make_move_iterator(mults2.end())
);
auto* newSpline = new Part::GeomBSplineCurve(
newPoles,
newWeights,
newKnots,
newMults,
bsp1->getDegree(),
false,
true
);
// int newGeoId = addGeometry(newSpline);
std::vector<Part::Geometry*> newGeos {newSpline};
replaceGeometries({geoId1, geoId2}, newGeos);
exposeInternalGeometry(geoId1);
setConstruction(geoId1, areOriginalCurvesConstruction);
// TODO: transfer constraints on the non-connected ends
auto otherPosId1 = (Sketcher::PointPos::start == posId1) ? Sketcher::PointPos::end
: Sketcher::PointPos::start;
auto otherPosId2 = (Sketcher::PointPos::start == posId2) ? Sketcher::PointPos::end
: Sketcher::PointPos::start;
transferConstraints(geoId1, otherPosId1, geoId1, PointPos::start, true);
transferConstraints(geoId2, otherPosId2, geoId1, PointPos::end, true);
return 0;
}
// clang-format off
bool SketchObject::isExternalAllowed(App::Document* pDoc, App::DocumentObject* pObj,
eReasonList* rsn) const
{
if (rsn)
*rsn = rlAllowed;
// Externals outside of the Document are NOT allowed
if (this->getDocument() != pDoc) {
if (rsn)
*rsn = rlOtherDoc;
return false;
}
// circular reference prevention
try {
if (!(this->testIfLinkDAGCompatible(pObj))) {
if (rsn)
*rsn = rlCircularReference;
return false;
}
}
catch (Base::Exception& e) {
Base::Console().warning(
"Probably, there is a circular reference in the document. Error: %s\n", e.what());
return true;// prohibiting this reference won't remove the problem anyway...
}
// Note: Checking for the body of the support doesn't work when the support are the three base
// planes
Part::BodyBase* body_this = Part::BodyBase::findBodyOf(this);
Part::BodyBase* body_obj = Part::BodyBase::findBodyOf(pObj);
App::Part* part_this = App::Part::getPartOfObject(this);
App::Part* part_obj = App::Part::getPartOfObject(pObj);
if (part_this == part_obj) {// either in the same part, or in the root of document
if (!body_this) {
return true;
}
else if (body_this == body_obj) {
return true;
}
else {
if (rsn)
*rsn = rlOtherBody;
return false;
}
}
else {
// cross-part link. Disallow, should be done via shapebinders only
if (rsn)
*rsn = rlOtherPart;
return false;
}
}
bool SketchObject::isCarbonCopyAllowed(App::Document* pDoc, App::DocumentObject* pObj, bool& xinv,
bool& yinv, eReasonList* rsn) const
{
auto setReason = [&rsn](eReasonList reasonFromList) {
if (rsn)
*rsn = reasonFromList;
};
setReason(rlAllowed);
std::string sketchArchType ("Sketcher::SketchObjectPython");
// Only applicable to sketches
if (!pObj->is<Sketcher::SketchObject>()
&& sketchArchType != pObj->getTypeId().getName()) {
setReason(rlNotASketch);
return false;
}
auto* psObj = static_cast<SketchObject*>(pObj);
// Sketches outside of the Document are NOT allowed
if (this->getDocument() != pDoc) {
setReason(rlOtherDoc);
return false;
}
// circular reference prevention
try {
if (!(this->testIfLinkDAGCompatible(pObj))) {
setReason(rlCircularReference);
return false;
}
}
catch (Base::Exception& e) {
Base::Console().warning(
"Probably, there is a circular reference in the document. Error: %s\n", e.what());
return true;// prohibiting this reference won't remove the problem anyway...
}
// Note: Checking for the body of the support doesn't work when the support are the three base
// planes
Part::BodyBase* body_this = Part::BodyBase::findBodyOf(this);
Part::BodyBase* body_obj = Part::BodyBase::findBodyOf(pObj);
App::Part* part_this = App::Part::getPartOfObject(this);
App::Part* part_obj = App::Part::getPartOfObject(pObj);
if (part_this != part_obj) {
// cross-part relation. Disallow, should be done via shapebinders only
setReason(rlOtherPart);
return false;
}
// Hereafter assuming: either in the same part, or in the root of document
if (body_this && body_this != body_obj) {
if (!this->allowOtherBody) {
setReason(rlOtherBody);
return false;
}
// if the original sketch has external geometry AND it is not in this body prevent
// link
else if (psObj->getExternalGeometryCount() > 2) {
setReason(rlOtherBodyWithLinks);
return false;
}
}
const Rotation& srot = psObj->Placement.getValue().getRotation();
const Rotation& lrot = this->Placement.getValue().getRotation();
Base::Vector3d snormal(0, 0, 1);
Base::Vector3d sx(1, 0, 0);
Base::Vector3d sy(0, 1, 0);
srot.multVec(snormal, snormal);
srot.multVec(sx, sx);
srot.multVec(sy, sy);
Base::Vector3d lnormal(0, 0, 1);
Base::Vector3d lx(1, 0, 0);
Base::Vector3d ly(0, 1, 0);
lrot.multVec(lnormal, lnormal);
lrot.multVec(lx, lx);
lrot.multVec(ly, ly);
double dot = snormal * lnormal;
double dotx = sx * lx;
double doty = sy * ly;
// the planes of the sketches must be parallel
if (!allowUnaligned && fabs(fabs(dot) - 1) > Precision::Confusion()) {
setReason(rlNonParallel);
return false;
}
// the axis must be aligned
if (!allowUnaligned
&& ((fabs(fabs(dotx) - 1) > Precision::Confusion())
|| (fabs(fabs(doty) - 1) > Precision::Confusion()))) {
setReason(rlAxesMisaligned);
return false;
}
// the origins of the sketches must be aligned or be the same
Base::Vector3d ddir =
(psObj->Placement.getValue().getPosition() - this->Placement.getValue().getPosition())
.Normalize();
double alignment = ddir * lnormal;
if (!allowUnaligned && (fabs(fabs(alignment) - 1) > Precision::Confusion())
&& (psObj->Placement.getValue().getPosition()
!= this->Placement.getValue().getPosition())) {
setReason(rlOriginsMisaligned);
return false;
}
xinv = allowUnaligned ? false : (fabs(dotx - 1) > Precision::Confusion());
yinv = allowUnaligned ? false : (fabs(doty - 1) > Precision::Confusion());
return true;
}
// clang-format on
int SketchObject::addSymmetric(
const std::vector<int>& geoIdList,
int refGeoId,
Sketcher::PointPos refPosId,
bool addSymmetryConstraints
)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& constrvals = this->Constraints.getValues();
std::vector<Constraint*> newconstrVals(constrvals);
std::map<int, int> geoIdMap;
std::map<int, bool> isStartEndInverted;
// Find out if reference is aligned with V or H axis,
// if so we can keep Vertical and Horizontal constraints in the mirrored geometry.
bool refIsLine = refPosId == Sketcher::PointPos::none;
bool refIsAxisAligned = refGeoId == Sketcher::GeoEnum::VAxis
|| refGeoId == Sketcher::GeoEnum::HAxis || !refIsLine
|| std::ranges::any_of(constrvals, [&refGeoId](auto* constr) {
return constr->First == refGeoId
&& (constr->Type == Sketcher::Vertical
|| constr->Type == Sketcher::Horizontal);
});
std::vector<Part::Geometry*> symgeos
= getSymmetric(geoIdList, geoIdMap, isStartEndInverted, refGeoId, refPosId);
// Perturb geometry to avoid numerical singularities in the solver (Jacobian Rank).
// If geometry is "perfect", the solver cannot distinguish between the derivative
// of a Symmetry constraint and an Equal constraint, flagging one as redundant.
// see https://github.com/FreeCAD/FreeCAD/issues/13551
// This does not happen with other arcs types.
if (addSymmetryConstraints) {
for (auto* geo : symgeos) {
if (auto* arc = dynamic_cast<Part::GeomArcOfCircle*>(geo)) {
double start, end;
arc->getRange(start, end, true);
arc->setRange(start + Precision::Angular(), end, true);
}
}
}
{
addGeometry(symgeos);
for (auto* constr : constrvals) {
// we look in the map, because we might have skipped internal alignment geometry
auto firstIt = geoIdMap.find(constr->First);
if (firstIt == geoIdMap.end()) {
continue;
}
// if First of constraint is in geoIdList
if (addSymmetryConstraints && constr->Type != Sketcher::InternalAlignment) {
// if we are making symmetric constraints, then we don't want to copy all
// constraints
continue;
}
if (constr->Second == GeoEnum::GeoUndef /*&& constr->Third == GeoEnum::GeoUndef*/) {
if (refIsAxisAligned
&& (constr->Type == Sketcher::DistanceX || constr->Type == Sketcher::DistanceY)) {
// In this case we want to keep the Vertical, Horizontal constraints.
// DistanceX and DistanceY constraints should also be possible to keep in
// this case, but keeping them causes segfault, not sure why.
continue;
}
if (!refIsAxisAligned
&& (constr->Type == Sketcher::DistanceX //
|| constr->Type == Sketcher::DistanceY //
|| constr->Type == Sketcher::Vertical //
|| constr->Type == Sketcher::Horizontal)) {
// this includes all non-directional single GeoId constraints, as radius,
// diameter, weight,...
continue;
}
Constraint* constNew = constr->copy();
constNew->Name = "";
constNew->First = firstIt->second;
newconstrVals.push_back(constNew);
continue;
}
// other geoids intervene in this constraint
auto secondIt = geoIdMap.find(constr->Second);
if (secondIt == geoIdMap.end()) {
continue;
}
// Second is also in the list
auto flipStartEndIfRelevant = [&isStartEndInverted](
int geoId,
const Sketcher::PointPos posId,
Sketcher::PointPos& posIdNew
) {
if (isStartEndInverted[geoId]) {
if (posId == Sketcher::PointPos::start) {
posIdNew = Sketcher::PointPos::end;
}
else if (posId == Sketcher::PointPos::end) {
posIdNew = Sketcher::PointPos::start;
}
}
};
if (constr->Third == GeoEnum::GeoUndef) {
if (!(constr->Type == Sketcher::Coincident //
|| constr->Type == Sketcher::Perpendicular //
|| constr->Type == Sketcher::Parallel //
|| constr->Type == Sketcher::Tangent //
|| constr->Type == Sketcher::Distance //
|| constr->Type == Sketcher::Equal //
|| constr->Type == Sketcher::Angle //
|| constr->Type == Sketcher::PointOnObject //
|| constr->Type == Sketcher::InternalAlignment)) {
continue;
}
Constraint* constNew = constr->copy();
constNew->Name = "";
constNew->First = firstIt->second;
constNew->Second = secondIt->second;
flipStartEndIfRelevant(constr->First, constr->FirstPos, constNew->FirstPos);
flipStartEndIfRelevant(constr->Second, constr->SecondPos, constNew->SecondPos);
if (constNew->Type == Tangent || constNew->Type == Perpendicular) {
AutoLockTangencyAndPerpty(constNew, true);
}
if ((constr->Type == Sketcher::Angle) && (refPosId == Sketcher::PointPos::none)) {
constNew->setValue(-constr->getValue());
}
newconstrVals.push_back(constNew);
continue;
}
// three GeoIds intervene in constraint
auto thirdIt = geoIdMap.find(constr->Third);
if (thirdIt == geoIdMap.end()) {
continue;
}
// Third is also in the list
Constraint* constNew = constr->copy();
constNew->Name = "";
constNew->First = firstIt->second;
constNew->Second = secondIt->second;
constNew->Third = thirdIt->second;
flipStartEndIfRelevant(constr->First, constr->FirstPos, constNew->FirstPos);
flipStartEndIfRelevant(constr->Second, constr->SecondPos, constNew->SecondPos);
flipStartEndIfRelevant(constr->Third, constr->ThirdPos, constNew->ThirdPos);
newconstrVals.push_back(constNew);
}
if (!addSymmetryConstraints) {
if (newconstrVals.size() > constrvals.size()) {
Constraints.setValues(std::move(newconstrVals));
}
// we delayed update, so trigger it now.
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
Geometry.touch();
return Geometry.getSize() - 1;
}
auto createSymConstr =
[&](int first, int second, Sketcher::PointPos firstPos, Sketcher::PointPos secondPos) {
auto* symConstr = new Constraint();
symConstr->Type = Symmetric;
symConstr->First = first;
symConstr->Second = second;
symConstr->Third = refGeoId;
symConstr->FirstPos = firstPos;
symConstr->SecondPos = secondPos;
symConstr->ThirdPos = refPosId;
newconstrVals.push_back(symConstr);
};
auto createEqualityConstr = [&](int first, int second) {
auto* symConstr = new Constraint();
symConstr->Type = Equal;
symConstr->First = first;
symConstr->Second = second;
newconstrVals.push_back(symConstr);
};
for (auto geoIdPair : geoIdMap) {
int geoId1 = geoIdPair.first;
int geoId2 = geoIdPair.second;
const Part::Geometry* geo = getGeometry(geoId1);
if (geo->is<Part::GeomLineSegment>()) {
auto gf = GeometryFacade::getFacade(geo);
if (!gf->isInternalAligned()) {
// Note internal aligned lines (ellipse, parabola, hyperbola) are causing
// redundant constraint.
createSymConstr(
geoId1,
geoId2,
PointPos::start,
isStartEndInverted[geoId1] ? PointPos::end : PointPos::start
);
createSymConstr(
geoId1,
geoId2,
PointPos::end,
isStartEndInverted[geoId1] ? PointPos::start : PointPos::end
);
}
}
else if (geo->is<Part::GeomCircle>() || geo->is<Part::GeomEllipse>()) {
createEqualityConstr(geoId1, geoId2);
createSymConstr(geoId1, geoId2, PointPos::mid, PointPos::mid);
}
else if (geo->is<Part::GeomArcOfCircle>() //
|| geo->is<Part::GeomArcOfEllipse>() //
|| geo->is<Part::GeomArcOfHyperbola>() //
|| geo->is<Part::GeomArcOfParabola>()) {
createEqualityConstr(geoId1, geoId2);
createSymConstr(
geoId1,
geoId2,
PointPos::start,
isStartEndInverted[geoId1] ? PointPos::end : PointPos::start
);
createSymConstr(
geoId1,
geoId2,
PointPos::end,
isStartEndInverted[geoId1] ? PointPos::start : PointPos::end
);
}
else if (geo->is<Part::GeomPoint>()) {
auto gf = GeometryFacade::getFacade(geo);
if (!gf->isInternalAligned()) {
createSymConstr(geoId1, geoId2, PointPos::start, PointPos::start);
}
}
// Note bspline has symmetric by the internal aligned circles.
}
}
// we delayed update, so trigger it now.
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
Geometry.touch();
return Geometry.getSize() - 1;
}
std::vector<Part::Geometry*> SketchObject::getSymmetric(
const std::vector<int>& geoIdList,
std::map<int, int>& geoIdMap,
std::map<int, bool>& isStartEndInverted,
int refGeoId,
Sketcher::PointPos refPosId
)
{
using std::numbers::pi;
std::vector<Part::Geometry*> symmetricVals;
bool refIsLine = refPosId == Sketcher::PointPos::none;
int cgeoid = getHighestCurveIndex() + 1;
auto shouldCopyGeometry = [&](auto* geo, int geoId) -> bool {
auto gf = GeometryFacade::getFacade(geo);
if (!gf->isInternalAligned()) {
// Return true if not internal aligned, indicating it should always be copied
return true;
}
// only add if the corresponding geometry it defines is also in the list.
const auto& constraints = Constraints.getValues();
auto constrIt = std::ranges::find_if(constraints, [&geoId](auto* c) {
return c->Type == Sketcher::InternalAlignment && c->First == geoId;
});
int definedGeo = (constrIt == constraints.end()) ? GeoEnum::GeoUndef : (*constrIt)->Second;
// Return true if definedGeo is in geoIdList, false otherwise
return std::ranges::find(geoIdList, definedGeo) != geoIdList.end();
};
if (refIsLine) {
const Part::Geometry* georef = getGeometry(refGeoId);
if (!georef->is<Part::GeomLineSegment>()) {
return {};
}
auto* refGeoLine = static_cast<const Part::GeomLineSegment*>(georef);
// line
Base::Vector3d refstart = refGeoLine->getStartPoint();
Base::Vector3d vectline = refGeoLine->getEndPoint() - refstart;
for (auto geoId : geoIdList) {
const Part::Geometry* geo = getGeometry(geoId);
Part::Geometry* geosym;
if (!shouldCopyGeometry(geo, geoId)) {
continue;
}
geosym = geo->copy();
// Handle Geometry
if (geosym->is<Part::GeomLineSegment>()) {
auto* geosymline = static_cast<Part::GeomLineSegment*>(geosym);
Base::Vector3d sp = geosymline->getStartPoint();
Base::Vector3d ep = geosymline->getEndPoint();
geosymline->setPoints(
sp + 2.0 * (sp.Perpendicular(refGeoLine->getStartPoint(), vectline) - sp),
ep + 2.0 * (ep.Perpendicular(refGeoLine->getStartPoint(), vectline) - ep)
);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomCircle>()) {
auto* geosymcircle = static_cast<Part::GeomCircle*>(geosym);
Base::Vector3d cp = geosymcircle->getCenter();
geosymcircle->setCenter(
cp + 2.0 * (cp.Perpendicular(refGeoLine->getStartPoint(), vectline) - cp)
);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomArcOfCircle>()) {
auto* geoaoc = static_cast<Part::GeomArcOfCircle*>(geosym);
Base::Vector3d sp = geoaoc->getStartPoint(true);
Base::Vector3d ep = geoaoc->getEndPoint(true);
Base::Vector3d cp = geoaoc->getCenter();
Base::Vector3d ssp = sp
+ 2.0 * (sp.Perpendicular(refGeoLine->getStartPoint(), vectline) - sp);
Base::Vector3d sep = ep
+ 2.0 * (ep.Perpendicular(refGeoLine->getStartPoint(), vectline) - ep);
Base::Vector3d scp = cp
+ 2.0 * (cp.Perpendicular(refGeoLine->getStartPoint(), vectline) - cp);
double theta1 = Base::fmod(atan2(sep.y - scp.y, sep.x - scp.x), 2.f * std::numbers::pi);
double theta2 = Base::fmod(atan2(ssp.y - scp.y, ssp.x - scp.x), 2.f * std::numbers::pi);
geoaoc->setCenter(scp);
geoaoc->setRange(theta1, theta2, true);
isStartEndInverted.insert(std::make_pair(geoId, true));
}
else if (geosym->is<Part::GeomEllipse>()) {
auto* geosymellipse = static_cast<Part::GeomEllipse*>(geosym);
Base::Vector3d cp = geosymellipse->getCenter();
Base::Vector3d majdir = geosymellipse->getMajorAxisDir();
double majord = geosymellipse->getMajorRadius();
double minord = geosymellipse->getMinorRadius();
double df = sqrt(majord * majord - minord * minord);
Base::Vector3d f1 = cp + df * majdir;
Base::Vector3d sf1 = f1
+ 2.0 * (f1.Perpendicular(refGeoLine->getStartPoint(), vectline) - f1);
Base::Vector3d scp = cp
+ 2.0 * (cp.Perpendicular(refGeoLine->getStartPoint(), vectline) - cp);
geosymellipse->setMajorAxisDir(sf1 - scp);
geosymellipse->setCenter(scp);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomArcOfEllipse>()) {
auto* geosymaoe = static_cast<Part::GeomArcOfEllipse*>(geosym);
Base::Vector3d cp = geosymaoe->getCenter();
Base::Vector3d majdir = geosymaoe->getMajorAxisDir();
double majord = geosymaoe->getMajorRadius();
double minord = geosymaoe->getMinorRadius();
double df = sqrt(majord * majord - minord * minord);
Base::Vector3d f1 = cp + df * majdir;
Base::Vector3d sf1 = f1
+ 2.0 * (f1.Perpendicular(refGeoLine->getStartPoint(), vectline) - f1);
Base::Vector3d scp = cp
+ 2.0 * (cp.Perpendicular(refGeoLine->getStartPoint(), vectline) - cp);
geosymaoe->setMajorAxisDir(sf1 - scp);
geosymaoe->setCenter(scp);
double theta1, theta2;
geosymaoe->getRange(theta1, theta2, true);
theta1 = 2.0 * pi - theta1;
theta2 = 2.0 * pi - theta2;
std::swap(theta1, theta2);
if (theta1 < 0) {
theta1 += 2.0 * pi;
theta2 += 2.0 * pi;
}
geosymaoe->setRange(theta1, theta2, true);
isStartEndInverted.insert(std::make_pair(geoId, true));
}
else if (geosym->is<Part::GeomArcOfHyperbola>()) {
auto* geosymaoe = static_cast<Part::GeomArcOfHyperbola*>(geosym);
Base::Vector3d cp = geosymaoe->getCenter();
Base::Vector3d majdir = geosymaoe->getMajorAxisDir();
double majord = geosymaoe->getMajorRadius();
double minord = geosymaoe->getMinorRadius();
double df = sqrt(majord * majord + minord * minord);
Base::Vector3d f1 = cp + df * majdir;
Base::Vector3d sf1 = f1
+ 2.0 * (f1.Perpendicular(refGeoLine->getStartPoint(), vectline) - f1);
Base::Vector3d scp = cp
+ 2.0 * (cp.Perpendicular(refGeoLine->getStartPoint(), vectline) - cp);
geosymaoe->setMajorAxisDir(sf1 - scp);
geosymaoe->setCenter(scp);
double theta1, theta2;
geosymaoe->getRange(theta1, theta2, true);
theta1 = -theta1;
theta2 = -theta2;
std::swap(theta1, theta2);
geosymaoe->setRange(theta1, theta2, true);
isStartEndInverted.insert(std::make_pair(geoId, true));
}
else if (geosym->is<Part::GeomArcOfParabola>()) {
auto* geosymaoe = static_cast<Part::GeomArcOfParabola*>(geosym);
Base::Vector3d cp = geosymaoe->getCenter();
Base::Vector3d f1 = geosymaoe->getFocus();
Base::Vector3d sf1 = f1
+ 2.0 * (f1.Perpendicular(refGeoLine->getStartPoint(), vectline) - f1);
Base::Vector3d scp = cp
+ 2.0 * (cp.Perpendicular(refGeoLine->getStartPoint(), vectline) - cp);
geosymaoe->setXAxisDir(sf1 - scp);
geosymaoe->setCenter(scp);
double theta1, theta2;
geosymaoe->getRange(theta1, theta2, true);
theta1 = -theta1;
theta2 = -theta2;
std::swap(theta1, theta2);
geosymaoe->setRange(theta1, theta2, true);
isStartEndInverted.insert(std::make_pair(geoId, true));
}
else if (geosym->is<Part::GeomBSplineCurve>()) {
auto* geosymbsp = static_cast<Part::GeomBSplineCurve*>(geosym);
std::vector<Base::Vector3d> poles = geosymbsp->getPoles();
for (auto& pole : poles) {
pole = pole
+ 2.0 * (pole.Perpendicular(refGeoLine->getStartPoint(), vectline) - pole);
}
geosymbsp->setPoles(poles);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomPoint>()) {
auto* geosympoint = static_cast<Part::GeomPoint*>(geosym);
Base::Vector3d cp = geosympoint->getPoint();
geosympoint->setPoint(
cp + 2.0 * (cp.Perpendicular(refGeoLine->getStartPoint(), vectline) - cp)
);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else {
Base::Console().error("Unsupported Geometry!! Just copying it.\n");
isStartEndInverted.insert(std::make_pair(geoId, false));
}
symmetricVals.push_back(geosym);
geoIdMap.insert(std::make_pair(geoId, cgeoid));
cgeoid++;
}
return symmetricVals;
}
// reference is a point
Vector3d refpoint;
const Part::Geometry* georef = getGeometry(refGeoId);
if (georef->is<Part::GeomPoint>()) {
refpoint = static_cast<const Part::GeomPoint*>(georef)->getPoint();
}
else if (refGeoId == -1 && refPosId == Sketcher::PointPos::start) {
refpoint = Vector3d(0, 0, 0);
}
else {
if (refPosId == Sketcher::PointPos::none) {
Base::Console().error("Wrong PointPosId.\n");
return {};
}
refpoint = getPoint(georef, refPosId);
}
for (auto geoId : geoIdList) {
const Part::Geometry* geo = getGeometry(geoId);
Part::Geometry* geosym;
if (!shouldCopyGeometry(geo, geoId)) {
continue;
}
geosym = geo->copy();
// Handle Geometry
if (geosym->is<Part::GeomLineSegment>()) {
auto* geosymline = static_cast<Part::GeomLineSegment*>(geosym);
Base::Vector3d sp = geosymline->getStartPoint();
Base::Vector3d ep = geosymline->getEndPoint();
Base::Vector3d ssp = sp + 2.0 * (refpoint - sp);
Base::Vector3d sep = ep + 2.0 * (refpoint - ep);
geosymline->setPoints(ssp, sep);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomCircle>()) {
auto* geosymcircle = static_cast<Part::GeomCircle*>(geosym);
Base::Vector3d cp = geosymcircle->getCenter();
geosymcircle->setCenter(cp + 2.0 * (refpoint - cp));
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomArcOfCircle>()) {
auto* geoaoc = static_cast<Part::GeomArcOfCircle*>(geosym);
Base::Vector3d sp = geoaoc->getStartPoint(true);
Base::Vector3d ep = geoaoc->getEndPoint(true);
Base::Vector3d cp = geoaoc->getCenter();
Base::Vector3d ssp = sp + 2.0 * (refpoint - sp);
Base::Vector3d sep = ep + 2.0 * (refpoint - ep);
Base::Vector3d scp = cp + 2.0 * (refpoint - cp);
double theta1 = Base::fmod(atan2(ssp.y - scp.y, ssp.x - scp.x), 2.f * pi);
double theta2 = Base::fmod(atan2(sep.y - scp.y, sep.x - scp.x), 2.f * pi);
geoaoc->setCenter(scp);
geoaoc->setRange(theta1, theta2, true);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomEllipse>()) {
auto* geosymellipse = static_cast<Part::GeomEllipse*>(geosym);
Base::Vector3d cp = geosymellipse->getCenter();
Base::Vector3d majdir = geosymellipse->getMajorAxisDir();
double majord = geosymellipse->getMajorRadius();
double minord = geosymellipse->getMinorRadius();
double df = sqrt(majord * majord - minord * minord);
Base::Vector3d f1 = cp + df * majdir;
Base::Vector3d sf1 = f1 + 2.0 * (refpoint - f1);
Base::Vector3d scp = cp + 2.0 * (refpoint - cp);
geosymellipse->setMajorAxisDir(sf1 - scp);
geosymellipse->setCenter(scp);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomArcOfEllipse>()) {
auto* geosymaoe = static_cast<Part::GeomArcOfEllipse*>(geosym);
Base::Vector3d cp = geosymaoe->getCenter();
Base::Vector3d majdir = geosymaoe->getMajorAxisDir();
double majord = geosymaoe->getMajorRadius();
double minord = geosymaoe->getMinorRadius();
double df = sqrt(majord * majord - minord * minord);
Base::Vector3d f1 = cp + df * majdir;
Base::Vector3d sf1 = f1 + 2.0 * (refpoint - f1);
Base::Vector3d scp = cp + 2.0 * (refpoint - cp);
geosymaoe->setMajorAxisDir(sf1 - scp);
geosymaoe->setCenter(scp);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomArcOfHyperbola>()) {
auto* geosymaoe = static_cast<Part::GeomArcOfHyperbola*>(geosym);
Base::Vector3d cp = geosymaoe->getCenter();
Base::Vector3d majdir = geosymaoe->getMajorAxisDir();
double majord = geosymaoe->getMajorRadius();
double minord = geosymaoe->getMinorRadius();
double df = sqrt(majord * majord + minord * minord);
Base::Vector3d f1 = cp + df * majdir;
Base::Vector3d sf1 = f1 + 2.0 * (refpoint - f1);
Base::Vector3d scp = cp + 2.0 * (refpoint - cp);
geosymaoe->setMajorAxisDir(sf1 - scp);
geosymaoe->setCenter(scp);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomArcOfParabola>()) {
auto* geosymaoe = static_cast<Part::GeomArcOfParabola*>(geosym);
Base::Vector3d cp = geosymaoe->getCenter();
Base::Vector3d f1 = geosymaoe->getFocus();
Base::Vector3d sf1 = f1 + 2.0 * (refpoint - f1);
Base::Vector3d scp = cp + 2.0 * (refpoint - cp);
geosymaoe->setXAxisDir(sf1 - scp);
geosymaoe->setCenter(scp);
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else if (geosym->is<Part::GeomBSplineCurve>()) {
auto* geosymbsp = static_cast<Part::GeomBSplineCurve*>(geosym);
std::vector<Base::Vector3d> poles = geosymbsp->getPoles();
for (auto& pole : poles) {
pole = pole + 2.0 * (refpoint - pole);
}
geosymbsp->setPoles(poles);
}
else if (geosym->is<Part::GeomPoint>()) {
auto* geosympoint = static_cast<Part::GeomPoint*>(geosym);
Base::Vector3d cp = geosympoint->getPoint();
geosympoint->setPoint(cp + 2.0 * (refpoint - cp));
isStartEndInverted.insert(std::make_pair(geoId, false));
}
else {
Base::Console().error("Unsupported Geometry!! Just copying it.\n");
isStartEndInverted.insert(std::make_pair(geoId, false));
}
symmetricVals.push_back(geosym);
geoIdMap.insert(std::make_pair(geoId, cgeoid));
cgeoid++;
}
return symmetricVals;
}
int SketchObject::addCopy(
const std::vector<int>& geoIdList,
const Base::Vector3d& displacement,
bool moveonly,
bool clone,
int csize,
int rsize,
bool constraindisplacement,
double perpscale
)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Part::Geometry*>& geovals = getInternalGeometry();
std::vector<Part::Geometry*> newgeoVals(geovals);
const std::vector<Constraint*>& constrvals = this->Constraints.getValues();
std::vector<Constraint*> newconstrVals(constrvals);
if (!moveonly) {
newgeoVals.reserve(geovals.size() + geoIdList.size());
}
std::vector<int> newgeoIdList(geoIdList);
if (newgeoIdList.empty()) { // default option to operate on all the geometry
for (int i = 0; i < int(geovals.size()); i++) {
newgeoIdList.push_back(i);
}
}
int cgeoid = getHighestCurveIndex() + 1;
int iterfirstgeoid = -1;
Base::Vector3d iterfirstpoint;
int refgeoid = -1;
int colrefgeoid = 0, rowrefgeoid = 0;
int currentrowfirstgeoid = -1, prevrowstartfirstgeoid = -1, prevfirstgeoid = -1;
Sketcher::PointPos refposId = Sketcher::PointPos::none;
std::map<int, int> geoIdMap;
Base::Vector3d perpendicularDisplacement
= Base::Vector3d(perpscale * displacement.y, perpscale * -displacement.x, 0);
int x, y;
auto makeCopyAtRowColumn = [&](int x, int y) {
// the reference for constraining array elements is the first valid point of the first
// element
if (x == 0 && y == 0) {
const Part::Geometry* geo = getGeometry(*(newgeoIdList.begin()));
auto gf = GeometryFacade::getFacade(geo);
if (gf->isInternalAligned() && !moveonly) {
// only add this geometry if the corresponding geometry it defines is also in
// the list.
auto constrIt = std::ranges::find_if(constrvals, [&newgeoIdList](auto c) {
return (
c->Type == Sketcher::InternalAlignment && c->First == *(newgeoIdList.begin())
);
});
int definedGeo = (constrIt != constrvals.end()) ? (*constrIt)->Second
: GeoEnum::GeoUndef;
if (std::ranges::find(newgeoIdList, definedGeo) == newgeoIdList.end()) {
// the first element setting the reference is an internal alignment
// geometry, wherein the geometry it defines is not part of the copy
// operation.
THROWM(
Base::ValueError,
"A move/copy/array operation on an internal alignment geometry is "
"only possible together with the geometry it defines."
);
}
}
refgeoid = *(newgeoIdList.begin());
currentrowfirstgeoid = refgeoid;
iterfirstgeoid = refgeoid;
if (geo->is<Part::GeomCircle>() || geo->is<Part::GeomEllipse>()) {
refposId = Sketcher::PointPos::mid;
}
else {
refposId = Sketcher::PointPos::start;
}
return; // the first element is already in place
}
prevfirstgeoid = iterfirstgeoid;
iterfirstgeoid = cgeoid;
if (x == 0) { // if first element of second row
prevrowstartfirstgeoid = currentrowfirstgeoid;
currentrowfirstgeoid = cgeoid;
}
int index = 0;
for (auto it = newgeoIdList.cbegin(); it != newgeoIdList.cend(); ++it, ++index) {
const Part::Geometry* geo = getGeometry(*it);
Part::Geometry* geocopy;
auto gf = GeometryFacade::getFacade(geo);
if (gf->isInternalAligned() && !moveonly) {
// only add this geometry if the corresponding geometry it defines is also in
// the list.
int definedGeo = GeoEnum::GeoUndef;
auto constrIt = std::ranges::find_if(constrvals, [&it](auto c) {
return (c->Type == Sketcher::InternalAlignment && c->First == *it);
});
if (constrIt != constrvals.end()) {
definedGeo = (*constrIt)->Second;
}
if (std::ranges::find(newgeoIdList, definedGeo) == newgeoIdList.end()) {
// we should not copy internal alignment geometry, unless the element they
// define is also mirrored
continue;
}
}
// We have already cloned all geometry and constraints, we only need a copy if not
// moving
if (!moveonly) {
geocopy = geo->copy();
generateId(geocopy);
}
else {
geocopy = newgeoVals[*it];
}
// Handle Geometry
if (geocopy->is<Part::GeomLineSegment>()) {
auto* geosymline = static_cast<Part::GeomLineSegment*>(geocopy);
Base::Vector3d ep = geosymline->getEndPoint();
Base::Vector3d ssp = geosymline->getStartPoint() + double(x) * displacement
+ double(y) * perpendicularDisplacement;
geosymline->setPoints(
ssp,
ep + double(x) * displacement + double(y) * perpendicularDisplacement
);
if (it == newgeoIdList.begin()) {
iterfirstpoint = ssp;
}
}
else if (geocopy->is<Part::GeomCircle>()) {
auto* geosymcircle = static_cast<Part::GeomCircle*>(geocopy);
Base::Vector3d cp = geosymcircle->getCenter();
Base::Vector3d scp = cp + double(x) * displacement
+ double(y) * perpendicularDisplacement;
geosymcircle->setCenter(scp);
if (it == newgeoIdList.begin()) {
iterfirstpoint = scp;
}
}
else if (geocopy->is<Part::GeomArcOfCircle>()) {
auto* geoaoc = static_cast<Part::GeomArcOfCircle*>(geocopy);
Base::Vector3d cp = geoaoc->getCenter();
Base::Vector3d scp = cp + double(x) * displacement
+ double(y) * perpendicularDisplacement;
geoaoc->setCenter(scp);
if (it == newgeoIdList.begin()) {
iterfirstpoint = geoaoc->getStartPoint(true);
}
}
else if (geocopy->is<Part::GeomEllipse>()) {
auto* geosymellipse = static_cast<Part::GeomEllipse*>(geocopy);
Base::Vector3d cp = geosymellipse->getCenter();
Base::Vector3d scp = cp + double(x) * displacement
+ double(y) * perpendicularDisplacement;
geosymellipse->setCenter(scp);
if (it == newgeoIdList.begin()) {
iterfirstpoint = scp;
}
}
else if (geocopy->is<Part::GeomArcOfEllipse>()) {
auto* geoaoe = static_cast<Part::GeomArcOfEllipse*>(geocopy);
Base::Vector3d cp = geoaoe->getCenter();
Base::Vector3d scp = cp + double(x) * displacement
+ double(y) * perpendicularDisplacement;
geoaoe->setCenter(scp);
if (it == newgeoIdList.begin()) {
iterfirstpoint = geoaoe->getStartPoint(true);
}
}
else if (geocopy->is<Part::GeomArcOfHyperbola>()) {
auto* geoaoe = static_cast<Part::GeomArcOfHyperbola*>(geocopy);
Base::Vector3d cp = geoaoe->getCenter();
Base::Vector3d scp = cp + double(x) * displacement
+ double(y) * perpendicularDisplacement;
geoaoe->setCenter(scp);
if (it == newgeoIdList.begin()) {
iterfirstpoint = geoaoe->getStartPoint(true);
}
}
else if (geocopy->is<Part::GeomArcOfParabola>()) {
auto* geoaoe = static_cast<Part::GeomArcOfParabola*>(geocopy);
Base::Vector3d cp = geoaoe->getCenter();
Base::Vector3d scp = cp + double(x) * displacement
+ double(y) * perpendicularDisplacement;
geoaoe->setCenter(scp);
if (it == newgeoIdList.begin()) {
iterfirstpoint = geoaoe->getStartPoint(true);
}
}
else if (geocopy->is<Part::GeomBSplineCurve>()) {
auto* geobsp = static_cast<Part::GeomBSplineCurve*>(geocopy);
std::vector<Base::Vector3d> poles = geobsp->getPoles();
for (auto& pole : poles) {
pole = pole + double(x) * displacement + double(y) * perpendicularDisplacement;
}
geobsp->setPoles(poles);
if (it == newgeoIdList.begin()) {
iterfirstpoint = geobsp->getStartPoint();
}
}
else if (geocopy->is<Part::GeomPoint>()) {
auto* geopoint = static_cast<Part::GeomPoint*>(geocopy);
Base::Vector3d cp = geopoint->getPoint();
Base::Vector3d scp = cp + double(x) * displacement
+ double(y) * perpendicularDisplacement;
geopoint->setPoint(scp);
if (it == newgeoIdList.begin()) {
iterfirstpoint = scp;
}
}
else {
Base::Console().error("Unsupported Geometry!! Just skipping it.\n");
continue;
}
if (!moveonly) { // we are copying
newgeoVals.push_back(geocopy);
geoIdMap.insert(std::make_pair(*it, cgeoid));
cgeoid++;
}
}
if (moveonly) {
return;
}
// handle geometry constraints
for (const auto& constr : constrvals) {
auto fit = geoIdMap.find(constr->First);
if (fit == geoIdMap.end()) {
continue;
}
// First of constraint is in geoIdList
if (constr->Second == GeoEnum::GeoUndef /*&& constr->Third == GeoEnum::GeoUndef*/) {
if ((constr->Type == Sketcher::DistanceX || constr->Type == Sketcher::DistanceY)
&& constr->FirstPos != Sketcher::PointPos::none) {
continue;
}
// if it is not a point locking DistanceX/Y
if ((constr->Type == Sketcher::DistanceX || constr->Type == Sketcher::DistanceY
|| constr->Type == Sketcher::Distance || constr->Type == Sketcher::Diameter
|| constr->Type == Sketcher::Weight || constr->Type == Sketcher::Radius)
&& clone) {
// Distances on a single Element are mapped to equality
// constraints in clone mode
Constraint* constNew = constr->copy();
constNew->Type = Sketcher::Equal;
constNew->isDriving = true;
// first is already (constr->First)
constNew->Second = fit->second;
newconstrVals.push_back(constNew);
continue;
}
if (!(constr->Type == Sketcher::Angle && clone)) {
Constraint* constNew = constr->copy();
constNew->First = fit->second;
newconstrVals.push_back(constNew);
continue;
}
if (getGeometry(constr->First)->is<Part::GeomLineSegment>()) {
// Angles on a single Element are mapped to parallel
// constraints in clone mode
Constraint* constNew = constr->copy();
constNew->Type = Sketcher::Parallel;
constNew->isDriving = true;
// first is already (constr->First)
constNew->Second = fit->second;
newconstrVals.push_back(constNew);
}
continue;
}
// other geoids intervene in this constraint
auto sit = geoIdMap.find(constr->Second);
if (sit == geoIdMap.end()) {
continue;
}
// Second is also in the list
if (constr->Third == GeoEnum::GeoUndef) {
if ((constr->Type == Sketcher::DistanceX || constr->Type == Sketcher::DistanceY
|| constr->Type == Sketcher::Distance)
&& (constr->First == constr->Second) && clone) {
// Distances on a two Elements, which must be points of the
// same line are mapped to equality constraints in clone
// mode
Constraint* constNew = constr->copy();
constNew->Type = Sketcher::Equal;
constNew->isDriving = true;
constNew->FirstPos = Sketcher::PointPos::none;
// first is already (constr->First)
constNew->Second = fit->second;
constNew->SecondPos = Sketcher::PointPos::none;
newconstrVals.push_back(constNew);
continue;
}
// remaining, this includes InternalAlignment constraints
Constraint* constNew = constr->copy();
constNew->First = fit->second;
constNew->Second = sit->second;
newconstrVals.push_back(constNew);
continue;
}
auto tit = geoIdMap.find(constr->Third);
if (tit != geoIdMap.end()) {
continue;
}
// Third is also in the list
Constraint* constNew = constr->copy();
constNew->First = fit->second;
constNew->Second = sit->second;
constNew->Third = tit->second;
newconstrVals.push_back(constNew);
}
// handle inter-geometry constraints
if (!constraindisplacement) {
// after each creation reset map so that the key-value is univoque (only for
// operations other than move)
geoIdMap.clear();
}
// add a construction line
auto* constrline = new Part::GeomLineSegment();
// position of the reference point
Base::Vector3d sp = getPoint(refgeoid, refposId)
+ ((x == 0) ? (double(x) * displacement + double(y - 1) * perpendicularDisplacement)
: (double(x - 1) * displacement + double(y) * perpendicularDisplacement));
// position of the current instance corresponding point
Base::Vector3d ep = iterfirstpoint;
constrline->setPoints(sp, ep);
GeometryFacade::setConstruction(constrline, true);
generateId(constrline);
newgeoVals.push_back(constrline);
Constraint* constNew;
if (x == 0) {
// first element of a row
// add coincidents for construction line
constNew = new Constraint();
constNew->Type = Sketcher::Coincident;
constNew->First = prevrowstartfirstgeoid;
constNew->FirstPos = refposId;
constNew->Second = cgeoid;
constNew->SecondPos = Sketcher::PointPos::start;
newconstrVals.push_back(constNew);
constNew = new Constraint();
constNew->Type = Sketcher::Coincident;
constNew->First = iterfirstgeoid;
constNew->FirstPos = refposId;
constNew->Second = cgeoid;
constNew->SecondPos = Sketcher::PointPos::end;
newconstrVals.push_back(constNew);
// it is the first added element of this row in the perpendicular to
// displacementvector direction
if (y == 1) {
rowrefgeoid = cgeoid;
cgeoid++;
// add length (or equal if perpscale==1) and perpendicular
if (perpscale == 1.0) {
constNew = new Constraint();
constNew->Type = Sketcher::Equal;
constNew->First = rowrefgeoid;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->Second = colrefgeoid;
constNew->SecondPos = Sketcher::PointPos::none;
newconstrVals.push_back(constNew);
}
else {
constNew = new Constraint();
constNew->Type = Sketcher::Distance;
constNew->First = rowrefgeoid;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->setValue(perpendicularDisplacement.Length());
newconstrVals.push_back(constNew);
}
constNew = new Constraint();
constNew->Type = Sketcher::Perpendicular;
constNew->First = rowrefgeoid;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->Second = colrefgeoid;
constNew->SecondPos = Sketcher::PointPos::none;
newconstrVals.push_back(constNew);
}
else {
// it is just one more element in the col direction
cgeoid++;
// all other first rowers get an equality and perpendicular constraint
constNew = new Constraint();
constNew->Type = Sketcher::Equal;
constNew->First = rowrefgeoid;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->Second = cgeoid - 1;
constNew->SecondPos = Sketcher::PointPos::none;
newconstrVals.push_back(constNew);
constNew = new Constraint();
constNew->Type = Sketcher::Perpendicular;
constNew->First = cgeoid - 1;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->Second = colrefgeoid;
constNew->SecondPos = Sketcher::PointPos::none;
newconstrVals.push_back(constNew);
}
}
else {
// any element not being the first element of a row
// add coincidents for construction line
constNew = new Constraint();
constNew->Type = Sketcher::Coincident;
constNew->First = prevfirstgeoid;
constNew->FirstPos = refposId;
constNew->Second = cgeoid;
constNew->SecondPos = Sketcher::PointPos::start;
newconstrVals.push_back(constNew);
constNew = new Constraint();
constNew->Type = Sketcher::Coincident;
constNew->First = iterfirstgeoid;
constNew->FirstPos = refposId;
constNew->Second = cgeoid;
constNew->SecondPos = Sketcher::PointPos::end;
newconstrVals.push_back(constNew);
if (y == 0 && x == 1) {
// first element of the first row
colrefgeoid = cgeoid;
cgeoid++;
// add length and Angle
constNew = new Constraint();
constNew->Type = Sketcher::Distance;
constNew->First = colrefgeoid;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->setValue(displacement.Length());
newconstrVals.push_back(constNew);
constNew = new Constraint();
constNew->Type = Sketcher::Angle;
constNew->First = colrefgeoid;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->setValue(atan2(displacement.y, displacement.x));
newconstrVals.push_back(constNew);
}
else {
// any other element
cgeoid++;
// all other elements get an equality and parallel constraint
constNew = new Constraint();
constNew->Type = Sketcher::Equal;
constNew->First = colrefgeoid;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->Second = cgeoid - 1;
constNew->SecondPos = Sketcher::PointPos::none;
newconstrVals.push_back(constNew);
constNew = new Constraint();
constNew->Type = Sketcher::Parallel;
constNew->First = cgeoid - 1;
constNew->FirstPos = Sketcher::PointPos::none;
constNew->Second = colrefgeoid;
constNew->SecondPos = Sketcher::PointPos::none;
newconstrVals.push_back(constNew);
}
}
// after each creation reset map so that the key-value is univoque (only for
// operations other than move)
geoIdMap.clear();
};
for (y = 0; y < rsize; y++) {
for (x = 0; x < csize; x++) {
makeCopyAtRowColumn(x, y);
}
}
// Block acceptGeometry in OnChanged to avoid unnecessary checks and updates
{
Base::StateLocker preventUpdate(internaltransaction, true);
Geometry.setValues(std::move(newgeoVals));
if (newconstrVals.size() > constrvals.size()) {
Constraints.setValues(std::move(newconstrVals));
}
}
// we inhibited update, so we trigger it now
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
Geometry.touch();
return Geometry.getSize() - 1;
}
// clang-format off
int SketchObject::removeAxesAlignment(const std::vector<int>& geoIdList)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& constrvals = this->Constraints.getValues();
std::map<Sketcher::ConstraintType, size_t> numConstrOfType =
{{Sketcher::Horizontal, 0}, {Sketcher::Vertical, 0}};
bool changed = false;
std::vector<std::pair<size_t, Sketcher::ConstraintType>> changeConstraintIndices;
auto chooseActionForConstraint = [&]
(size_t i, const int geoid) {
if (!constrvals[i]->involvesGeoId(geoid)) {
return;
}
switch (constrvals[i]->Type) {
case Sketcher::Horizontal:
case Sketcher::Vertical: {
if (constrvals[i]->FirstPos == Sketcher::PointPos::none
&& constrvals[i]->SecondPos == Sketcher::PointPos::none) {
changeConstraintIndices.emplace_back(i, constrvals[i]->Type);
numConstrOfType[constrvals[i]->Type]++;
}
break;
}
case Sketcher::Symmetric: {
// only remove symmetric to axes
if ((constrvals[i]->Third == GeoEnum::HAxis || constrvals[i]->Third == GeoEnum::VAxis)
&& constrvals[i]->ThirdPos == Sketcher::PointPos::none)
changeConstraintIndices.emplace_back(i, constrvals[i]->Type);
break;
}
case Sketcher::PointOnObject: {
if ((constrvals[i]->Second == GeoEnum::HAxis || constrvals[i]->Second == GeoEnum::VAxis)
&& constrvals[i]->SecondPos == Sketcher::PointPos::none)
changeConstraintIndices.emplace_back(i, constrvals[i]->Type);
break;
}
case Sketcher::DistanceX:
case Sketcher::DistanceY: {
changeConstraintIndices.emplace_back(i, constrvals[i]->Type);
break;
}
default:
break;
}
};
for (size_t i = 0; i < constrvals.size(); i++) {
for (const auto& geoid : geoIdList) {
chooseActionForConstraint(i, geoid);
}
}
if (changeConstraintIndices.empty())
return 0;// nothing to be done
std::vector<Constraint*> newconstrVals;
newconstrVals.reserve(constrvals.size());
std::map<Sketcher::ConstraintType, int> refConstrOfType =
{{Sketcher::Horizontal, GeoEnum::GeoUndef},
{Sketcher::Vertical, GeoEnum::GeoUndef}};
int cindex = 0;
for (size_t i = 0; i < constrvals.size(); i++) {
if (i != changeConstraintIndices[cindex].first) {
newconstrVals.push_back(constrvals[i]);
continue;
}
switch (changeConstraintIndices[cindex].second) {
case Sketcher::Horizontal:
case Sketcher::Vertical: {
if (!(numConstrOfType[changeConstraintIndices[cindex].second] > 0)) {
break;
}
changed = true;
if (refConstrOfType[changeConstraintIndices[cindex].second] == GeoEnum::GeoUndef) {
refConstrOfType[changeConstraintIndices[cindex].second] = constrvals[i]->First;
++cindex;
continue;
}
auto newConstr = new Constraint();
newConstr->Type = Sketcher::Parallel;
newConstr->First = refConstrOfType[changeConstraintIndices[cindex].second];
newConstr->Second = constrvals[i]->First;
newconstrVals.push_back(newConstr);
break;
}
case Sketcher::Symmetric:
case Sketcher::PointOnObject: {
changed = true; // We remove symmetric/point-on-object on axes
break;
}
case Sketcher::DistanceX:
case Sketcher::DistanceY: {
changed = true;
// TODO: Handle pathological cases like DistanceY on horizontal constraint
newconstrVals.push_back(constrvals[i]->clone());
newconstrVals.back()->Type = Sketcher::Distance;
break;
}
default: break;
}
++cindex;
}
if (numConstrOfType[Sketcher::Horizontal] > 0 && numConstrOfType[Sketcher::Vertical] > 0) {
auto newConstr = new Constraint();
newConstr->Type = Sketcher::Perpendicular;
newConstr->First = refConstrOfType[Sketcher::Horizontal];
newConstr->Second = refConstrOfType[Sketcher::Vertical];
newconstrVals.push_back(newConstr);
}
if (changed) {
Constraints.setValues(std::move(newconstrVals));
}
return 0;
}
template <>
int SketchObject::exposeInternalGeometryForType<Part::GeomEllipse>(const int GeoId)
{
const Part::Geometry* geo = getGeometry(GeoId);
// First we search what has to be restored
bool major = false;
bool minor = false;
bool focus1 = false;
bool focus2 = false;
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
for (const auto& constr : vals) {
if (constr->Type != Sketcher::InternalAlignment || constr->Second != GeoId) {
continue;
}
switch (constr->AlignmentType) {
case Sketcher::EllipseMajorDiameter:
major = true;
break;
case Sketcher::EllipseMinorDiameter:
minor = true;
break;
case Sketcher::EllipseFocus1:
focus1 = true;
break;
case Sketcher::EllipseFocus2:
focus2 = true;
break;
default:
return -1;
}
}
int currentgeoid = getHighestCurveIndex();
int incrgeo = 0;
std::vector<Part::Geometry*> igeo;
std::vector<Constraint*> icon;
const auto* ellipse = static_cast<const Part::GeomEllipse*>(geo);
Base::Vector3d center {ellipse->getCenter()};
double majord {ellipse->getMajorRadius()};
double minord {ellipse->getMinorRadius()};
Base::Vector3d majdir {ellipse->getMajorAxisDir()};
Base::Vector3d mindir = Vector3d(-majdir.y, majdir.x);
Base::Vector3d majorpositiveend = center + majord * majdir;
Base::Vector3d majornegativeend = center - majord * majdir;
Base::Vector3d minorpositiveend = center + minord * mindir;
Base::Vector3d minornegativeend = center - minord * mindir;
double df = sqrt(majord * majord - minord * minord);
Base::Vector3d focus1P = center + df * majdir;
Base::Vector3d focus2P = center - df * majdir;
if (!major) {
auto* lmajor = new Part::GeomLineSegment();
lmajor->setPoints(majorpositiveend, majornegativeend);
igeo.push_back(lmajor);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = EllipseMajorDiameter;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!minor) {
auto* lminor = new Part::GeomLineSegment();
lminor->setPoints(minorpositiveend, minornegativeend);
igeo.push_back(lminor);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = EllipseMinorDiameter;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!focus1) {
auto* pf1 = new Part::GeomPoint();
pf1->setPoint(focus1P);
igeo.push_back(pf1);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = EllipseFocus1;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->FirstPos = Sketcher::PointPos::start;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!focus2) {
auto* pf2 = new Part::GeomPoint();
pf2->setPoint(focus2P);
igeo.push_back(pf2);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = EllipseFocus2;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->FirstPos = Sketcher::PointPos::start;
newConstr->Second = GeoId;
icon.push_back(newConstr);
}
addAndCleanup(igeo, icon);
return incrgeo;
}
void SketchObject::addAndCleanup(std::vector<Part::Geometry*> igeo, std::vector<Constraint*> icon)
{
this->addGeometry(igeo, true);
this->addConstraints(icon);
for (auto& geoToDelete : igeo) {
delete geoToDelete;
}
for (auto& constraintToDelete : icon) {
delete constraintToDelete;
}
}
// TODO: This is a repeat of ellipse. Can we do some code reuse?
template <>
int SketchObject::exposeInternalGeometryForType<Part::GeomArcOfEllipse>(const int GeoId)
{
const Part::Geometry* geo = getGeometry(GeoId);
// First we search what has to be restored
bool major = false;
bool minor = false;
bool focus1 = false;
bool focus2 = false;
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
for (const auto& constr : vals) {
if (constr->Type != Sketcher::InternalAlignment || constr->Second != GeoId) {
continue;
}
switch (constr->AlignmentType) {
case Sketcher::EllipseMajorDiameter:
major = true;
break;
case Sketcher::EllipseMinorDiameter:
minor = true;
break;
case Sketcher::EllipseFocus1:
focus1 = true;
break;
case Sketcher::EllipseFocus2:
focus2 = true;
break;
default:
return -1;
}
}
int currentgeoid = getHighestCurveIndex();
int incrgeo = 0;
std::vector<Part::Geometry*> igeo;
std::vector<Constraint*> icon;
const auto* aoe = static_cast<const Part::GeomArcOfEllipse*>(geo);
Base::Vector3d center {aoe->getCenter()};
double majord {aoe->getMajorRadius()};
double minord {aoe->getMinorRadius()};
Base::Vector3d majdir {aoe->getMajorAxisDir()};
Base::Vector3d mindir {-majdir.y, majdir.x};
Base::Vector3d majorpositiveend {center + majord * majdir};
Base::Vector3d majornegativeend {center - majord * majdir};
Base::Vector3d minorpositiveend {center + minord * mindir};
Base::Vector3d minornegativeend {center - minord * mindir};
double df = sqrt(majord * majord - minord * minord);
Base::Vector3d focus1P {center + df * majdir};
Base::Vector3d focus2P {center - df * majdir};
if (!major) {
auto* lmajor = new Part::GeomLineSegment();
lmajor->setPoints(majorpositiveend, majornegativeend);
igeo.push_back(lmajor);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = EllipseMajorDiameter;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!minor) {
auto* lminor = new Part::GeomLineSegment();
lminor->setPoints(minorpositiveend, minornegativeend);
igeo.push_back(lminor);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = EllipseMinorDiameter;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!focus1) {
auto* pf1 = new Part::GeomPoint();
pf1->setPoint(focus1P);
igeo.push_back(pf1);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = EllipseFocus1;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->FirstPos = Sketcher::PointPos::start;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!focus2) {
auto* pf2 = new Part::GeomPoint();
pf2->setPoint(focus2P);
igeo.push_back(pf2);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = EllipseFocus2;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->FirstPos = Sketcher::PointPos::start;
newConstr->Second = GeoId;
icon.push_back(newConstr);
}
addAndCleanup(igeo, icon);
return incrgeo; // number of added elements
}
template <>
int SketchObject::exposeInternalGeometryForType<Part::GeomArcOfHyperbola>(const int GeoId)
{
const Part::Geometry* geo = getGeometry(GeoId);
// First we search what has to be restored
bool major = false;
bool minor = false;
bool focus = false;
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
for (auto const& constr : vals) {
if (constr->Type != Sketcher::InternalAlignment || constr->Second != GeoId) {
continue;
}
switch (constr->AlignmentType) {
case Sketcher::HyperbolaMajor:
major = true;
break;
case Sketcher::HyperbolaMinor:
minor = true;
break;
case Sketcher::HyperbolaFocus:
focus = true;
break;
default:
return -1;
}
}
int currentgeoid = getHighestCurveIndex();
int incrgeo = 0;
const auto* aoh = static_cast<const Part::GeomArcOfHyperbola*>(geo);
Base::Vector3d center {aoh->getCenter()};
double majord {aoh->getMajorRadius()};
double minord {aoh->getMinorRadius()};
Base::Vector3d majdir {aoh->getMajorAxisDir()};
std::vector<Part::Geometry*> igeo;
std::vector<Constraint*> icon;
Base::Vector3d mindir = Vector3d(-majdir.y, majdir.x);
Base::Vector3d majorpositiveend = center + majord * majdir;
Base::Vector3d majornegativeend = center - majord * majdir;
Base::Vector3d minorpositiveend = majorpositiveend + minord * mindir;
Base::Vector3d minornegativeend = majorpositiveend - minord * mindir;
double df = sqrt(majord * majord + minord * minord);
Base::Vector3d focus1P = center + df * majdir;
if (!major) {
auto* lmajor = new Part::GeomLineSegment();
lmajor->setPoints(majorpositiveend, majornegativeend);
igeo.push_back(lmajor);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = Sketcher::HyperbolaMajor;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!minor) {
auto* lminor = new Part::GeomLineSegment();
lminor->setPoints(minorpositiveend, minornegativeend);
igeo.push_back(lminor);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = Sketcher::HyperbolaMinor;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!focus) {
auto* pf1 = new Part::GeomPoint();
pf1->setPoint(focus1P);
igeo.push_back(pf1);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = Sketcher::HyperbolaFocus;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->FirstPos = Sketcher::PointPos::start;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
addAndCleanup(igeo, icon);
return incrgeo; // number of added elements
}
template <>
int SketchObject::exposeInternalGeometryForType<Part::GeomArcOfParabola>(const int GeoId)
{
const Part::Geometry* geo = getGeometry(GeoId);
// First we search what has to be restored
bool focus = false;
bool focus_to_vertex = false;
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
for (auto const& constr : vals) {
if (constr->Type != Sketcher::InternalAlignment || constr->Second != GeoId) {
continue;
}
switch (constr->AlignmentType) {
case Sketcher::ParabolaFocus:
focus = true;
break;
case Sketcher::ParabolaFocalAxis:
focus_to_vertex = true;
break;
default:
return -1;
}
}
int currentgeoid = getHighestCurveIndex();
int incrgeo = 0;
const auto* aop = static_cast<const Part::GeomArcOfParabola*>(geo);
Base::Vector3d center {aop->getCenter()};
Base::Vector3d focusp {aop->getFocus()};
std::vector<Part::Geometry*> igeo;
std::vector<Constraint*> icon;
if (!focus) {
auto* pf1 = new Part::GeomPoint();
pf1->setPoint(focusp);
igeo.push_back(pf1);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = Sketcher::ParabolaFocus;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->FirstPos = Sketcher::PointPos::start;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
if (!focus_to_vertex) {
auto* paxis = new Part::GeomLineSegment();
paxis->setPoints(center, focusp);
igeo.push_back(paxis);
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = Sketcher::ParabolaFocalAxis;
newConstr->First = currentgeoid + incrgeo + 1;
newConstr->FirstPos = Sketcher::PointPos::none;
newConstr->Second = GeoId;
icon.push_back(newConstr);
incrgeo++;
}
addAndCleanup(igeo, icon);
return incrgeo; // number of added elements
}
template <>
int SketchObject::exposeInternalGeometryForType<Part::GeomBSplineCurve>(const int GeoId)
{
const Part::Geometry* geo = getGeometry(GeoId);
const auto* bsp = static_cast<const Part::GeomBSplineCurve*>(geo);
// First we search what has to be restored
std::vector<int> controlpointgeoids(bsp->countPoles(), GeoEnum::GeoUndef);
std::vector<int> knotgeoids(bsp->countKnots(), GeoEnum::GeoUndef);
bool isfirstweightconstrained = false;
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
// search for existing poles
for (auto const& constr : vals) {
if (constr->Type != Sketcher::InternalAlignment || constr->Second != GeoId) {
continue;
}
switch (constr->AlignmentType) {
case Sketcher::BSplineControlPoint:
controlpointgeoids[constr->InternalAlignmentIndex] = constr->First;
break;
case Sketcher::BSplineKnotPoint:
knotgeoids[constr->InternalAlignmentIndex] = constr->First;
break;
default:
return -1;
}
}
if (controlpointgeoids[0] != GeoEnum::GeoUndef) {
isfirstweightconstrained =
std::ranges::any_of(vals, [&controlpointgeoids](const auto& constr) {
return (constr->Type == Sketcher::Weight && constr->First == controlpointgeoids[0]);
});
}
int currentgeoid = getHighestCurveIndex();
int incrgeo = 0;
std::vector<Part::Geometry*> igeo;
std::vector<Constraint*> icon;
std::vector<Base::Vector3d> poles = bsp->getPoles();
std::vector<double> weights = bsp->getWeights();
std::vector<double> knots = bsp->getKnots();
double distance_p0_p1 = (poles[1] - poles[0]).Length();// for visual purposes only
for (size_t index = 0; index < controlpointgeoids.size(); ++index) {
auto& cpGeoId = controlpointgeoids.at(index);
if (cpGeoId != GeoEnum::GeoUndef) {
continue;
}
// if controlpoint not existing
auto* pc = new Part::GeomCircle();
pc->setCenter(poles[index]);
pc->setRadius(distance_p0_p1 / 6);
igeo.push_back(pc);
incrgeo++;
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = Sketcher::BSplineControlPoint;
newConstr->First = currentgeoid + incrgeo;
newConstr->FirstPos = Sketcher::PointPos::mid;
newConstr->Second = GeoId;
newConstr->InternalAlignmentIndex = index;
icon.push_back(newConstr);
if (index == 0) {
controlpointgeoids[0] = currentgeoid + incrgeo;
if (weights[0] == 1.0) {
// if the first weight is 1.0 it's probably going to be non-rational
auto* newConstr3 = new Sketcher::Constraint();
newConstr3->Type = Sketcher::Weight;
newConstr3->First = controlpointgeoids[0];
newConstr3->setValue(weights[0]);
icon.push_back(newConstr3);
isfirstweightconstrained = true;
}
continue;
}
if (isfirstweightconstrained && weights[0] == weights[index]) {
// if pole-weight newly created AND first weight is radius-constrained,
// AND these weights are equal, constrain them to be equal
auto* newConstr2 = new Sketcher::Constraint();
newConstr2->Type = Sketcher::Equal;
newConstr2->First = currentgeoid + incrgeo;
newConstr2->FirstPos = Sketcher::PointPos::none;
newConstr2->Second = controlpointgeoids[0];
newConstr2->SecondPos = Sketcher::PointPos::none;
icon.push_back(newConstr2);
}
}
for (size_t index = 0; index < knotgeoids.size(); ++index) {
auto& kGeoId = knotgeoids.at(index);
if (kGeoId != GeoEnum::GeoUndef) {
continue;
}
// if knot point not existing
auto* kp = new Part::GeomPoint();
kp->setPoint(bsp->pointAtParameter(knots[index]));
igeo.push_back(kp);
incrgeo++;
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = Sketcher::BSplineKnotPoint;
newConstr->First = currentgeoid + incrgeo;
newConstr->FirstPos = Sketcher::PointPos::start;
newConstr->Second = GeoId;
newConstr->InternalAlignmentIndex = index;
icon.push_back(newConstr);
}
Q_UNUSED(isfirstweightconstrained);
addAndCleanup(igeo, icon);
return incrgeo; // number of added elements
}
int SketchObject::exposeInternalGeometry(int GeoId)
{
if (GeoId < 0 || GeoId > getHighestCurveIndex())
return -1;
const Part::Geometry* geo = getGeometry(GeoId);
// Only for supported types
if (geo->is<Part::GeomEllipse>()) {
return exposeInternalGeometryForType<Part::GeomEllipse>(GeoId);
}
else if (geo->is<Part::GeomArcOfEllipse>()) {
return exposeInternalGeometryForType<Part::GeomArcOfEllipse>(GeoId);
}
else if (geo->is<Part::GeomArcOfHyperbola>()) {
return exposeInternalGeometryForType<Part::GeomArcOfHyperbola>(GeoId);
}
else if (geo->is<Part::GeomArcOfParabola>()) {
return exposeInternalGeometryForType<Part::GeomArcOfParabola>(GeoId);
}
else if (geo->is<Part::GeomBSplineCurve>()) {
return exposeInternalGeometryForType<Part::GeomBSplineCurve>(GeoId);
}
else
return -1;// not supported type
}
int SketchObject::deleteUnusedInternalGeometry(int GeoId, bool delgeoid)
{
if (GeoId < 0 || GeoId > getHighestCurveIndex())
return -1;
const Part::Geometry* geo = getGeometry(GeoId);
// Only for supported types
if (geo->is<Part::GeomEllipse>()
|| geo->is<Part::GeomArcOfEllipse>()
|| geo->is<Part::GeomArcOfHyperbola>()) {
return deleteUnusedInternalGeometryWhenTwoFoci(GeoId, delgeoid);
}
if (geo->is<Part::GeomArcOfParabola>()) {
return deleteUnusedInternalGeometryWhenOneFocus(GeoId, delgeoid);
}
if (geo->is<Part::GeomBSplineCurve>()) {
return deleteUnusedInternalGeometryWhenBSpline(GeoId, delgeoid);
}
// Default case: type not supported
return -1;
}
int SketchObject::deleteUnusedInternalGeometryWhenTwoFoci(int GeoId, bool delgeoid)
{
int majorelementindex = -1;
int minorelementindex = -1;
int focus1elementindex = -1;
int focus2elementindex = -1;
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
for (auto const& constr : vals) {
if (constr->Type != Sketcher::InternalAlignment || constr->Second != GeoId) {
continue;
}
switch (constr->AlignmentType) {
case Sketcher::EllipseMajorDiameter:
case Sketcher::HyperbolaMajor:
majorelementindex = constr->First;
break;
case Sketcher::EllipseMinorDiameter:
case Sketcher::HyperbolaMinor:
minorelementindex = constr->First;
break;
case Sketcher::EllipseFocus1:
case Sketcher::HyperbolaFocus:
focus1elementindex = constr->First;
break;
case Sketcher::EllipseFocus2:
focus2elementindex = constr->First;
break;
default:
return -1;
}
}
// Hide unused geometry here
int majorconstraints = 0;// number of constraints associated to the geoid of the major axis
int minorconstraints = 0;
int focus1constraints = 0;
int focus2constraints = 0;
for (const auto& constr : vals) {
if (constr->involvesGeoId(majorelementindex))
majorconstraints++;
else if (constr->involvesGeoId(minorelementindex))
minorconstraints++;
else if (constr->involvesGeoId(focus1elementindex))
focus1constraints++;
else if (constr->involvesGeoId(focus2elementindex))
focus2constraints++;
}
std::vector<int> delgeometries;
// those with less than 2 constraints must be removed
if (focus2constraints < 2)
delgeometries.push_back(focus2elementindex);
if (focus1constraints < 2)
delgeometries.push_back(focus1elementindex);
if (minorconstraints < 2)
delgeometries.push_back(minorelementindex);
if (majorconstraints < 2)
delgeometries.push_back(majorelementindex);
if (delgeoid)
delgeometries.push_back(GeoId);
// indices over an erased element get automatically updated!!
std::sort(delgeometries.begin(), delgeometries.end(), std::greater<>());
for (auto& dGeoId : delgeometries) {
delGeometry(dGeoId, DeleteOption::UpdateGeometry);
}
int ndeleted = delgeometries.size();
return ndeleted;// number of deleted elements
}
int SketchObject::deleteUnusedInternalGeometryWhenOneFocus(int GeoId, bool delgeoid)
{
// if the focus-to-vertex line is constrained, then never delete the focus
// if the line is unconstrained, then the line may be deleted,
// in this case the focus may be deleted if unconstrained.
int majorelementindex = -1;
int focus1elementindex = -1;
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
for (auto const& constr : vals) {
if (constr->Type != Sketcher::InternalAlignment || constr->Second != GeoId) {
continue;
}
switch (constr->AlignmentType) {
case Sketcher::ParabolaFocus:
focus1elementindex = constr->First;
break;
case Sketcher::ParabolaFocalAxis:
majorelementindex = constr->First;
break;
default:
return -1;
}
}
// Hide unused geometry here
// number of constraints associated to the geoid of the major axis other than the coincident
// ones
int majorconstraints = 0;
int focus1constraints = 0;
for (const auto& constr : vals) {
if (constr->involvesGeoId(majorelementindex)) {
majorconstraints++;
}
else if (constr->involvesGeoId(focus1elementindex)) {
focus1constraints++;
}
}
std::vector<int> delgeometries;
// major has minimum one constraint, the specific internal alignment constraint
if (majorelementindex != -1 && majorconstraints < 2)
delgeometries.push_back(majorelementindex);
// focus has minimum one constraint now, the specific internal alignment constraint
if (focus1elementindex != -1 && focus1constraints < 2)
delgeometries.push_back(focus1elementindex);
if (delgeoid)
delgeometries.push_back(GeoId);
// indices over an erased element get automatically updated!!
std::sort(delgeometries.begin(), delgeometries.end(), std::greater<>());
for (auto& dGeoId : delgeometries) {
delGeometry(dGeoId, DeleteOption::UpdateGeometry);
}
int ndeleted = delgeometries.size();
delgeometries.clear();
return ndeleted;// number of deleted elements
}
int SketchObject::deleteUnusedInternalGeometryWhenBSpline(int GeoId, bool delgeoid)
{
// First we search existing IA
std::map<int, int> poleGeoIdsAndConstraints;
std::map<int, int> knotGeoIdsAndConstraints;
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
// search for existing poles
for (auto const& constr : vals) {
if (constr->Type != Sketcher::InternalAlignment || constr->Second != GeoId) {
continue;
}
switch (constr->AlignmentType) {
case Sketcher::BSplineControlPoint:
poleGeoIdsAndConstraints[constr->First] = 0;
break;
case Sketcher::BSplineKnotPoint:
knotGeoIdsAndConstraints[constr->First] = 0;
break;
default:
return -1;
}
}
std::vector<int> delgeometries;
// Update all control point constraint counts.
// EXCLUDES internal alignment and related constraints.
for (auto const& constr : vals) {
// We do not ignore weight constraints as we did with radius constraints,
// because the radius magnitude no longer makes sense without the B-Spline.
if (constr->Type == Sketcher::InternalAlignment
|| constr->Type == Sketcher::Weight) {
continue;
}
bool firstIsInCPGeoIds = poleGeoIdsAndConstraints.count(constr->First) == 1;
bool secondIsInCPGeoIds = poleGeoIdsAndConstraints.count(constr->Second) == 1;
if (constr->Type == Sketcher::Equal && firstIsInCPGeoIds == secondIsInCPGeoIds) {
continue;
}
// any equality constraint constraining a pole is not interpole
if (firstIsInCPGeoIds) {
++poleGeoIdsAndConstraints[constr->First];
}
if (secondIsInCPGeoIds) {
++poleGeoIdsAndConstraints[constr->Second];
}
}
for (auto& [cpGeoId, numConstr] : poleGeoIdsAndConstraints) {
if (numConstr < 1) { // IA
delgeometries.push_back(cpGeoId);
}
}
for (auto& [kGeoId, numConstr] : knotGeoIdsAndConstraints) {
// Update all control point constraint counts.
// INCLUDES internal alignment and related constraints.
auto tempGeoID = kGeoId; // C++17 and earlier do not support captured structured bindings
numConstr = std::count_if(vals.begin(), vals.end(), [&tempGeoID](const auto& constr) {
return constr->involvesGeoId(tempGeoID);
});
if (numConstr < 2) { // IA
delgeometries.push_back(kGeoId);
}
}
if (delgeoid) {
delgeometries.push_back(GeoId);
}
int ndeleted = delGeometriesExclusiveList(delgeometries);
return ndeleted;// number of deleted elements
}
int SketchObject::deleteUnusedInternalGeometryAndUpdateGeoId(int& GeoId, bool delgeoid)
{
const Part::Geometry* geo = getGeometry(GeoId);
if (!hasInternalGeometry(geo)) {
return -1;
}
// We need to remove the internal geometry of the BSpline, as BSplines change in number
// of poles and knots We save the tags of the relevant geometry to retrieve the new
// GeoIds later on.
boost::uuids::uuid GeoIdTag;
GeoIdTag = geo->getTag();
int returnValue = deleteUnusedInternalGeometry(GeoId, delgeoid);
if (delgeoid) {
GeoId = GeoEnum::GeoUndef;
return returnValue;
}
auto vals = getCompleteGeometry();
for (size_t i = 0; i < vals.size(); i++) {
if (vals[i]->getTag() == GeoIdTag) {
GeoId = getGeoIdFromCompleteGeometryIndex(i);
break;
}
}
return returnValue;
}
bool SketchObject::convertToNURBS(int GeoId)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (GeoId > getHighestCurveIndex()
|| (GeoId < 0 && -GeoId > static_cast<int>(ExternalGeo.getSize())) || GeoId == -1
|| GeoId == -2)
return false;
const Part::Geometry* geo = getGeometry(GeoId);
if (geo->is<Part::GeomPoint>())
return false;
const auto* geo1 = static_cast<const Part::GeomCurve*>(geo);
Part::GeomBSplineCurve* bspline;
try {
bspline = geo1->toNurbs(geo1->getFirstParameter(), geo1->getLastParameter());
if (geo1->isDerivedFrom<Part::GeomArcOfConic>()) {
const auto* geoaoc = static_cast<const Part::GeomArcOfConic*>(geo1);
if (geoaoc->isReversed())
bspline->reverse();
}
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
// revert to original values
return false;
}
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
std::vector<Part::Geometry*> newVals(vals);
// Block checks and updates in OnChanged to avoid unnecessary checks and updates
{
Base::StateLocker preventUpdate(internaltransaction, true);
if (GeoId < 0) {// external geometry
newVals.push_back(bspline);
generateId(bspline);
}
else {// normal geometry
newVals[GeoId] = bspline;
GeometryFacade::copyId(geo, bspline);
const std::vector<Sketcher::Constraint*>& cvals = Constraints.getValues();
std::vector<Constraint*> newcVals(cvals);
int index = cvals.size() - 1;
// delete constraints on this elements other than coincident constraints (bspline does
// not support them currently), except for coincidents on mid point of the
// to-be-converted curve.
for (; index >= 0; index--) {
auto otherthancoincident = cvals[index]->Type != Sketcher::Coincident
&& cvals[index]->involvesGeoId(GeoId);
auto coincidentonmidpoint = cvals[index]->Type == Sketcher::Coincident
&& cvals[index]->involvesGeoIdAndPosId(GeoId, Sketcher::PointPos::mid);
if (otherthancoincident || coincidentonmidpoint)
newcVals.erase(newcVals.begin() + index);
}
this->Constraints.setValues(std::move(newcVals));
}
Geometry.setValues(std::move(newVals));
}
// trigger update now
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
Geometry.touch();
return true;
}
bool SketchObject::increaseBSplineDegree(int GeoId, int degreeincrement /*= 1*/)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (GeoId < 0 || GeoId > getHighestCurveIndex()) {
return false;
}
const Part::Geometry* geo = getGeometry(GeoId);
if (!geo->is<Part::GeomBSplineCurve>()) {
return false;
}
const auto* bsp = static_cast<const Part::GeomBSplineCurve*>(geo);
const Handle(Geom_BSplineCurve) curve = Handle(Geom_BSplineCurve)::DownCast(bsp->handle());
std::unique_ptr<Part::GeomBSplineCurve> bspline(new Part::GeomBSplineCurve(curve));
try {
int cdegree = bspline->getDegree();
bspline->increaseDegree(cdegree + degreeincrement);
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
return false;
}
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
std::vector<Part::Geometry*> newVals(vals);
GeometryFacade::copyId(geo, bspline.get());
newVals[GeoId] = bspline.release();
// AcceptGeometry called from onChanged
Geometry.setValues(std::move(newVals));
return true;
}
bool SketchObject::decreaseBSplineDegree(int GeoId, int degreedecrement /*= 1*/)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (GeoId < 0 || GeoId > getHighestCurveIndex())
return false;
const Part::Geometry* geo = getGeometry(GeoId);
if (!geo->is<Part::GeomBSplineCurve>())
return false;
const auto* bsp = static_cast<const Part::GeomBSplineCurve*>(geo);
const Handle(Geom_BSplineCurve) curve = Handle(Geom_BSplineCurve)::DownCast(bsp->handle());
std::unique_ptr<Part::GeomBSplineCurve> bspline(new Part::GeomBSplineCurve(curve));
try {
int cdegree = bspline->getDegree();
// degree must be >= 1
int maxdegree = cdegree - degreedecrement;
if (maxdegree == 0)
return false;
bspline->approximate(Precision::Confusion(), 20, maxdegree, GeomAbs_C0);
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
return false;
}
// FIXME: Avoid to delete the whole geometry but only delete invalid constraints
// and unused construction geometries
#if 0
const std::vector< Part::Geometry * > &vals = getInternalGeometry();
std::vector< Part::Geometry * > newVals(vals);
newVals[GeoId] = bspline.release();
// AcceptGeometry called from onChanged
Geometry.setValues(newVals);
#else
delGeometry(GeoId);
int newId = addGeometry(bspline.release());
exposeInternalGeometry(newId);
#endif
return true;
}
// clang-format on
bool SketchObject::modifyBSplineKnotMultiplicity(int GeoId, int knotIndex, int multiplicityincr)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (GeoId < 0 || GeoId > getHighestCurveIndex()) {
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP("Exceptions", "B-spline Geometry Index (GeoID) is out of bounds.")
);
}
if (multiplicityincr == 0) {
// no change in multiplicity
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP("Exceptions", "You are requesting no change in knot multiplicity.")
);
}
const Part::Geometry* geo = getGeometry(GeoId);
if (!geo->is<Part::GeomBSplineCurve>()) {
THROWMT(
Base::TypeError,
QT_TRANSLATE_NOOP("Exceptions", "The Geometry Index (GeoId) provided is not a B-spline.")
);
}
const auto* bsp = static_cast<const Part::GeomBSplineCurve*>(geo);
int degree = bsp->getDegree();
if (knotIndex > bsp->countKnots() || knotIndex < 1) {
// knotindex in OCC 1 -> countKnots
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP(
"Exceptions",
"The knot index is out of bounds. Note that in accordance with "
"OCC notation, the first knot has index 1 and not zero."
)
);
}
std::unique_ptr<Part::GeomBSplineCurve> bspline;
int curmult = bsp->getMultiplicity(knotIndex);
// zero is removing the knot, degree is just positional continuity
if ((curmult + multiplicityincr) > degree) {
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP(
"Exceptions",
"The multiplicity cannot be increased beyond the degree of the B-spline."
)
);
}
// zero is removing the knot, degree is just positional continuity
if ((curmult + multiplicityincr) < 0) {
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP("Exceptions", "The multiplicity cannot be decreased beyond zero.")
);
}
try {
bspline.reset(static_cast<Part::GeomBSplineCurve*>(bsp->clone()));
if (multiplicityincr > 0) { // increase multiplicity
bspline->increaseMultiplicity(knotIndex, curmult + multiplicityincr);
}
else { // decrease multiplicity
bool result = bspline->removeKnot(knotIndex, curmult + multiplicityincr, 1E6);
if (!result) {
THROWMT(
Base::CADKernelError,
QT_TRANSLATE_NOOP(
"Exceptions",
"OCC is unable to decrease the multiplicity within the "
"maximum tolerance."
)
);
}
}
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
return false;
}
// we succeeded with the multiplicity modification, so alignment geometry may be
// invalid/inconsistent for the new bspline
std::vector<int> delGeoId;
std::vector<Base::Vector3d> poles = bsp->getPoles();
std::vector<Base::Vector3d> newPoles = bspline->getPoles();
// on fully removing a knot the knot geometry changes
std::vector<double> knots = bsp->getKnots();
std::vector<double> newKnots = bspline->getKnots();
std::map<Sketcher::InternalAlignmentType, std::vector<int>> indexInNew {
{Sketcher::BSplineControlPoint, {}},
{Sketcher::BSplineKnotPoint, {}}
};
indexInNew[Sketcher::BSplineControlPoint].reserve(poles.size());
indexInNew[Sketcher::BSplineKnotPoint].reserve(knots.size());
for (const auto& pole : poles) {
const auto it = std::ranges::find(newPoles, pole);
indexInNew[Sketcher::BSplineControlPoint].emplace_back(it - newPoles.begin());
}
std::ranges::replace(indexInNew[Sketcher::BSplineControlPoint], int(newPoles.size()), -1);
for (const auto& knot : knots) {
const auto it = std::ranges::find(newKnots, knot);
indexInNew[Sketcher::BSplineKnotPoint].emplace_back(it - newKnots.begin());
}
std::ranges::replace(indexInNew[Sketcher::BSplineKnotPoint], int(newKnots.size()), -1);
const std::vector<Sketcher::Constraint*>& cvals = Constraints.getValues();
std::vector<Constraint*> newcVals(0);
// modify pole and knot constraints
for (const auto& constr : cvals) {
if (!(constr->Type == Sketcher::InternalAlignment && constr->Second == GeoId)) {
newcVals.push_back(constr);
continue;
}
int index = indexInNew.at(constr->AlignmentType).at(constr->InternalAlignmentIndex);
if (index == -1) {
// it is an internal alignment geometry that is no longer valid
// => delete it and the geometry
delGeoId.push_back(constr->First);
continue;
}
Constraint* newConstr = constr->clone();
newConstr->InternalAlignmentIndex = index;
newcVals.push_back(newConstr);
}
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
std::vector<Part::Geometry*> newVals(vals);
GeometryFacade::copyId(geo, bspline.get());
newVals[GeoId] = bspline.release();
// Block acceptGeometry in OnChanged to avoid unnecessary checks and updates
{
Base::StateLocker preventUpdate(internaltransaction, true);
Geometry.setValues(std::move(newVals));
this->Constraints.setValues(std::move(newcVals));
}
// Trigger update now
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
if (!delGeoId.empty()) {
delGeometriesExclusiveList(delGeoId);
}
else {
Geometry.touch();
}
return true;
}
bool SketchObject::insertBSplineKnot(int GeoId, double param, int multiplicity)
{
// TODO: Check if this is still valid: no need to check input data validity as this is an
// sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
// handling unacceptable cases
if (GeoId < 0 || GeoId > getHighestCurveIndex()) {
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP("Exceptions", "B-spline Geometry Index (GeoID) is out of bounds.")
);
}
if (multiplicity == 0) {
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP("Exceptions", "Knot cannot have zero multiplicity.")
);
}
const Part::Geometry* geo = getGeometry(GeoId);
if (!geo->is<Part::GeomBSplineCurve>()) {
THROWMT(
Base::TypeError,
QT_TRANSLATE_NOOP("Exceptions", "The Geometry Index (GeoId) provided is not a B-spline.")
);
}
const auto* bsp = static_cast<const Part::GeomBSplineCurve*>(geo);
int degree = bsp->getDegree();
double firstParam = bsp->getFirstParameter();
double lastParam = bsp->getLastParameter();
if (multiplicity > degree) {
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP(
"Exceptions",
"Knot multiplicity cannot be higher than the degree of the B-spline."
)
);
}
if (param > lastParam || param < firstParam) {
THROWMT(
Base::ValueError,
QT_TRANSLATE_NOOP("Exceptions", "Knot cannot be inserted outside the B-spline parameter range.")
);
}
std::unique_ptr<Part::GeomBSplineCurve> bspline;
// run the command
try {
bspline.reset(static_cast<Part::GeomBSplineCurve*>(bsp->clone()));
bspline->insertKnot(param, multiplicity);
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
return false;
}
// once command is run update the internal geometries
std::vector<int> delGeoId;
std::vector<Base::Vector3d> poles = bsp->getPoles();
std::vector<Base::Vector3d> newPoles = bspline->getPoles();
std::vector<int> poleIndexInNew(poles.size(), -1);
for (size_t j = 0; j < poles.size(); j++) {
const auto it = std::ranges::find(newPoles, poles[j]);
poleIndexInNew[j] = it - newPoles.begin();
}
std::ranges::replace(poleIndexInNew, int(newPoles.size()), -1);
std::vector<double> knots = bsp->getKnots();
std::vector<double> newKnots = bspline->getKnots();
std::vector<int> knotIndexInNew(knots.size(), -1);
for (size_t j = 0; j < knots.size(); j++) {
const auto it = std::ranges::find(newKnots, knots[j]);
knotIndexInNew[j] = it - newKnots.begin();
}
std::ranges::replace(knotIndexInNew, int(newKnots.size()), -1);
const std::vector<Sketcher::Constraint*>& cvals = Constraints.getValues();
std::vector<Constraint*> newcVals(0);
// modify pole and knot constraints
for (const auto& constr : cvals) {
if (!(constr->Type == Sketcher::InternalAlignment && constr->Second == GeoId)) {
newcVals.push_back(constr);
continue;
}
std::vector<int>* indexInNew = nullptr;
if (constr->AlignmentType == Sketcher::BSplineControlPoint) {
indexInNew = &poleIndexInNew;
}
else if (constr->AlignmentType == Sketcher::BSplineKnotPoint) {
indexInNew = &knotIndexInNew;
}
else {
// it is a bspline geometry, but not a controlpoint or knot
newcVals.push_back(constr);
continue;
}
if (indexInNew && indexInNew->at(constr->InternalAlignmentIndex) == -1) {
// it is an internal alignment geometry that is no longer valid
// => delete it and the pole circle
delGeoId.push_back(constr->First);
continue;
}
Constraint* newConstr = constr->clone();
newConstr->InternalAlignmentIndex = indexInNew->at(constr->InternalAlignmentIndex);
newcVals.push_back(newConstr);
}
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
std::vector<Part::Geometry*> newVals(vals);
GeometryFacade::copyId(geo, bspline.get());
newVals[GeoId] = bspline.release();
// Block acceptGeometry in OnChanged to avoid unnecessary checks and updates
{
Base::StateLocker preventUpdate(internaltransaction, true);
Geometry.setValues(std::move(newVals));
this->Constraints.setValues(std::move(newcVals));
}
// Trigger update now
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
if (!delGeoId.empty()) {
// NOTE: There have been a couple of instances when knot insertion has
// led to a segmentation fault: see
// https://forum.freecad.org/viewtopic.php?f=19&t=64962&sid=10272db50a635c633260517b14ecad37.
// If a segfault happens again and a `Geometry.touch()` here fixes it,
// it is possible that `delGeometriesExclusiveList` is causing an update
// in constraint GUI features during an intermediate step.
// See 247a9f0876a00e08c25b07d1f8802479d8623e87 for suggestions.
// Geometry.touch();
delGeometriesExclusiveList(delGeoId);
return true;
}
Geometry.touch();
return true;
}
int SketchObject::carbonCopy(App::DocumentObject* pObj, bool construction)
{
using std::numbers::pi;
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
// so far only externals to the support of the sketch and datum features
bool xinv = false, yinv = false;
if (!isCarbonCopyAllowed(pObj->getDocument(), pObj, xinv, yinv)) {
return -1;
}
auto* psObj = static_cast<SketchObject*>(pObj);
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
const std::vector<Sketcher::Constraint*>& cvals = Constraints.getValues();
std::vector<Part::Geometry*> newVals(vals);
std::vector<Constraint*> newcVals(cvals);
int nextgeoid = vals.size();
int nextextgeoid = getExternalGeometryCount();
int nextcid = cvals.size();
const std::vector<Part::Geometry*>& svals = psObj->getInternalGeometry();
const std::vector<Sketcher::Constraint*>& scvals = psObj->Constraints.getValues();
newVals.reserve(vals.size() + svals.size());
newcVals.reserve(cvals.size() + scvals.size());
const Base::Vector3d& origin = this->Placement.getValue().getPosition();
const Base::Rotation& rotation = this->Placement.getValue().getRotation();
const Base::Vector3d axisH = rotation.multVec(Base::Vector3d::UnitX);
const Base::Vector3d axisV = rotation.multVec(Base::Vector3d::UnitY);
std::map<int, int> extMap;
if (psObj->ExternalGeo.getSize() > 1) {
int i = -1;
auto geos = this->ExternalGeo.getValues();
std::string myName(this->getNameInDocument());
myName += ".";
for (const auto& geo : psObj->ExternalGeo.getValues()) {
if (++i < 2) { // skip h/v axes
continue;
}
else {
auto egf = ExternalGeometryFacade::getFacade(geo);
const auto& ref = egf->getRef();
if (boost::starts_with(ref, myName)) {
int geoId;
PointPos posId;
if (this->geoIdFromShapeType(ref.c_str() + myName.size(), geoId, posId)) {
extMap[-i - 1] = geoId;
continue;
}
}
}
auto copy = geo->copy();
auto egf = ExternalGeometryFacade::getFacade(copy);
egf->setId(++geoLastId);
if (!egf->getRef().empty()) {
auto& refs = this->externalGeoRefMap[egf->getRef()];
refs.push_back(geoLastId);
}
this->externalGeoMap[geoLastId] = (int)geos.size();
geos.push_back(copy);
extMap[-i - 1] = -(int)geos.size();
}
Base::ObjectStatusLocker<App::Property::Status, App::Property> guard(
App::Property::User3,
&this->ExternalGeo
);
this->ExternalGeo.setValues(std::move(geos));
}
if (psObj->ExternalGeometry.getSize() > 0) {
std::vector<DocumentObject*> Objects = ExternalGeometry.getValues();
std::vector<std::string> SubElements = ExternalGeometry.getSubValues();
const std::vector<DocumentObject*> originalObjects = Objects;
const std::vector<std::string> originalSubElements = SubElements;
std::vector<DocumentObject*> sObjects = psObj->ExternalGeometry.getValues();
std::vector<std::string> sSubElements = psObj->ExternalGeometry.getSubValues();
if (Objects.size() != SubElements.size() || sObjects.size() != sSubElements.size()) {
assert(0 /*counts of objects and subelements in external geometry links do not match*/);
Base::Console().error(
"Internal error: counts of objects and subelements in external "
"geometry links do not match\n"
);
return -1;
}
int si = 0;
for (auto& sobj : sObjects) {
int i = 0;
for (auto& obj : Objects) {
if (obj == sobj && SubElements[i] == sSubElements[si]) {
Base::Console().error(
"Link to %s already exists in this sketch. Delete the link and try again\n",
sSubElements[si].c_str()
);
return -1;
}
i++;
}
Objects.push_back(sobj);
SubElements.push_back(sSubElements[si]);
si++;
}
ExternalGeometry.setValues(Objects, SubElements);
try {
rebuildExternalGeometry();
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
// revert to original values
ExternalGeometry.setValues(originalObjects, originalSubElements);
return -1;
}
solverNeedsUpdate = true;
}
auto applyGeometryFlipCorrection = [xinv, yinv, origin, axisV, axisH](Part::Geometry* geoNew) {
if (!xinv && !yinv) {
return;
}
if (xinv) {
geoNew->mirror(origin, axisV);
}
if (yinv) {
geoNew->mirror(origin, axisH);
}
};
for (const auto& geoOld : svals) {
Part::Geometry* geoNew = geoOld->copy();
if (xinv || yinv) {
// corrections for flipped geometry
applyGeometryFlipCorrection(geoNew);
}
generateId(geoNew);
if (construction && !geoNew->is<Part::GeomPoint>()) {
GeometryFacade::setConstruction(geoNew, true);
}
newVals.push_back(geoNew);
}
auto applyConstraintFlipCorrection = [xinv, yinv](Sketcher::Constraint* newConstr) {
if (!xinv && !yinv) {
return;
}
// DistanceX, DistanceY
if ((xinv && newConstr->Type == Sketcher::DistanceX)
|| (yinv && newConstr->Type == Sketcher::DistanceY)) {
if (newConstr->First == newConstr->Second) {
std::swap(newConstr->FirstPos, newConstr->SecondPos);
}
else {
newConstr->setValue(-newConstr->getValue());
}
}
// Angle
if (newConstr->Type == Sketcher::Angle) {
auto normalizeAngle = [](double angleDeg) {
while (angleDeg > pi) {
angleDeg -= pi * 2.0;
}
while (angleDeg <= -pi) {
angleDeg += pi * 2.0;
}
return angleDeg;
};
if (xinv && yinv) { // rotation 180 degrees around normal axis
if (newConstr->First == -1 || newConstr->Second == -1 || newConstr->First == -2
|| newConstr->Second == -2 || newConstr->Second == GeoEnum::GeoUndef) {
// angle to horizontal or vertical axis
newConstr->setValue(normalizeAngle(newConstr->getValue() + pi));
}
else {
// angle between two sketch entities
// do nothing
}
}
else if (xinv) { // rotation 180 degrees around vertical axis
if (newConstr->First == -1 || newConstr->Second == -1
|| newConstr->Second == GeoEnum::GeoUndef) {
// angle to horizontal axis
newConstr->setValue(normalizeAngle(pi - newConstr->getValue()));
}
else {
// angle between two sketch entities or angle to vertical axis
newConstr->setValue(normalizeAngle(-newConstr->getValue()));
}
}
else if (yinv) { // rotation 180 degrees around horizontal axis
if (newConstr->First == -2 || newConstr->Second == -2) {
// angle to vertical axis
newConstr->setValue(normalizeAngle(pi - newConstr->getValue()));
}
else {
// angle between two sketch entities or angle to horizontal axis
newConstr->setValue(normalizeAngle(-newConstr->getValue()));
}
}
}
};
for (const auto& constr : scvals) {
Sketcher::Constraint* newConstr = constr->copy();
if (constr->First >= 0) {
newConstr->First += nextgeoid;
}
if (constr->Second >= 0) {
newConstr->Second += nextgeoid;
}
if (constr->Third >= 0) {
newConstr->Third += nextgeoid;
}
if (constr->First < -2 && constr->First != GeoEnum::GeoUndef) {
newConstr->First -= (nextextgeoid - 2);
}
if (constr->Second < -2 && constr->Second != GeoEnum::GeoUndef) {
newConstr->Second -= (nextextgeoid - 2);
}
if (constr->Third < -2 && constr->Third != GeoEnum::GeoUndef) {
newConstr->Third -= (nextextgeoid - 2);
}
if (xinv || yinv) {
// corrections for flipped constraints
applyConstraintFlipCorrection(newConstr);
}
newcVals.push_back(newConstr);
}
// Block acceptGeometry in OnChanged to avoid unnecessary checks and updates
{
Base::StateLocker preventUpdate(internaltransaction, true);
Geometry.setValues(std::move(newVals));
this->Constraints.setValues(std::move(newcVals));
}
// we trigger now the update (before dealing with expressions)
// Update geometry indices and rebuild vertexindex now via onChanged, so that
// ViewProvider::UpdateData is triggered.
Geometry.touch();
auto makeCorrectedExpressionString =
[xinv, yinv](const Sketcher::Constraint* constr, const std::string expr) -> std::string {
if (!xinv && !yinv) {
return expr;
}
// DistanceX, DistanceY
if ((xinv && constr->Type == Sketcher::DistanceX)
|| (yinv && constr->Type == Sketcher::DistanceY)) {
if (constr->First == constr->Second) {
return expr;
}
else {
return "-(" + expr + ")";
}
}
// Angle
if (constr->Type == Sketcher::Angle) {
if (xinv && yinv) { // rotation 180 degrees around normal axis
if (constr->First == -1 || constr->Second == -1 || constr->First == -2
|| constr->Second == -2 || constr->Second == GeoEnum::GeoUndef) {
// angle to horizontal or vertical axis
return "(" + expr + ") + 180 deg";
}
else {
// angle between two sketch entities
// do nothing
return expr;
}
}
else if (xinv) { // rotation 180 degrees around vertical axis
if (constr->First == -1 || constr->Second == -1
|| constr->Second == GeoEnum::GeoUndef) {
// angle to horizontal axis
return "180 deg - (" + expr + ")";
}
else {
// angle between two sketch entities or angle to vertical axis
return "-(" + expr + ")";
}
}
else if (yinv) { // rotation 180 degrees around horizontal axis
if (constr->First == -2 || constr->Second == -2) {
// angle to vertical axis
return "180 deg - (" + expr + ")";
}
else {
// angle between two sketch entities or angle to horizontal axis
return "-(" + expr + ")";
}
}
}
return expr;
};
int sourceid = 0;
for (auto it = scvals.cbegin(); it != scvals.cend(); ++it, ++nextcid, ++sourceid) {
if (!((*it)->isDimensional() && (*it)->isDriving)) {
continue;
}
App::ObjectIdentifier spath;
std::shared_ptr<App::Expression> expr;
std::string scname = (*it)->Name;
std::string sref;
if (App::ExpressionParser::isTokenAnIndentifier(scname)) {
spath = App::ObjectIdentifier(psObj->Constraints)
<< App::ObjectIdentifier::SimpleComponent(scname);
sref = spath.getDocumentObjectName().getString() + spath.toString();
}
else {
spath = psObj->Constraints.createPath(sourceid);
sref = spath.getDocumentObjectName().getString() + std::string(1, '.') + spath.toString();
}
if (xinv || yinv) {
// corrections for flipped expressions
sref = makeCorrectedExpressionString((*it), sref);
}
expr = std::shared_ptr<App::Expression>(App::Expression::parse(this, sref));
setExpression(Constraints.createPath(nextcid), std::move(expr));
}
// Solve even if `noRecomputes==false`, because recompute may fail, and leave the
// sketch in an inconsistent state. A concrete example. If the copied sketch
// has broken external geometry, its recomputation will fail. And because we
// use expression for copied constraint to add dependency to the copied
// sketch, this sketch will not be recomputed (because its dependency fails
// to recompute).
solve();
return svals.size();
}
int SketchObject::addExternal(App::DocumentObject* Obj, const char* SubName, bool defining, bool intersection)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
// so far only externals to the support of the sketch and datum features
if (!isExternalAllowed(Obj->getDocument(), Obj)) {
return -1;
}
auto wholeShape = Part::Feature::getTopoShape(
Obj,
Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform
);
auto shape = wholeShape.getSubTopoShape(SubName, /*silent*/ true);
TopAbs_ShapeEnum shapeType = TopAbs_SHAPE;
if (shape.shapeType(/*silent*/ true) != TopAbs_FACE) {
if (shape.hasSubShape(TopAbs_FACE)) {
shapeType = TopAbs_FACE;
}
else if (shape.shapeType(/*silent*/ true) != TopAbs_EDGE && shape.hasSubShape(TopAbs_EDGE)) {
shapeType = TopAbs_EDGE;
}
}
if (shapeType != TopAbs_SHAPE) {
std::string element = Part::TopoShape::shapeName(shapeType);
std::size_t elementNameSize = element.size();
int geometryCount = ExternalGeometry.getSize();
gp_Pln sketchPlane;
if (intersection) {
Base::Placement Plm = Placement.getValue();
Base::Vector3d Pos = Plm.getPosition();
Base::Rotation Rot = Plm.getRotation();
Base::Vector3d dN(0, 0, 1);
Rot.multVec(dN, dN);
Base::Vector3d dX(1, 0, 0);
Rot.multVec(dX, dX);
gp_Ax3 sketchAx3(
gp_Pnt(Pos.x, Pos.y, Pos.z),
gp_Dir(dN.x, dN.y, dN.z),
gp_Dir(dX.x, dX.y, dX.z)
);
sketchPlane.SetPosition(sketchAx3);
}
for (const auto& subShape : shape.getSubShapes(shapeType)) {
int idx = wholeShape.findShape(subShape);
if (idx == 0) {
continue;
}
if (intersection) {
try {
FCBRepAlgoAPI_Section maker(subShape, sketchPlane);
if (!maker.IsDone() || maker.Shape().IsNull()) {
continue;
}
}
catch (Standard_Failure&) {
continue;
}
}
element += std::to_string(idx);
addExternal(Obj, element.c_str(), defining, intersection);
element.resize(elementNameSize);
}
if (ExternalGeometry.getSize() == geometryCount) {
return -1;
}
return geometryCount;
}
// get the actual lists of the externals
std::vector<long> Types = ExternalTypes.getValues();
std::vector<DocumentObject*> Objects = ExternalGeometry.getValues();
std::vector<std::string> SubElements = ExternalGeometry.getSubValues();
Types.resize(Objects.size(), static_cast<long>(ExtType::Projection));
const std::vector<DocumentObject*> originalObjects = Objects;
const std::vector<std::string> originalSubElements = SubElements;
if (Objects.size() != SubElements.size()) {
assert(0 /*counts of objects and subelements in external geometry links do not match*/);
Base::Console().error(
"Internal error: counts of objects and subelements in external "
"geometry links do not match\n"
);
return -1;
}
bool add = true;
for (size_t i = 0; i < Objects.size(); ++i) {
if (!(Objects[i] == Obj && std::string(SubName) == SubElements[i])) {
continue;
}
if (Types[i] == static_cast<int>(ExtType::Both)
|| (Types[i] == static_cast<int>(ExtType::Projection) && !intersection)
|| (Types[i] == static_cast<int>(ExtType::Intersection) && intersection)) {
Base::Console().error("Link to %s already exists in this sketch.\n", SubName);
return -1;
}
// Case where projections are already there when adding intersections.
add = false;
Types[i] = static_cast<int>(ExtType::Both);
}
if (add) {
// add the new ones
Objects.push_back(Obj);
SubElements.emplace_back(SubName);
Types.push_back(static_cast<int>(intersection ? ExtType::Intersection : ExtType::Projection));
if (intersection) {
}
// set the Link list.
ExternalGeometry.setValues(Objects, SubElements);
}
ExternalTypes.setValues(Types);
try {
ExternalToAdd ext {Obj, std::string(SubName), defining, intersection};
rebuildExternalGeometry(ext);
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
// revert to original values
ExternalGeometry.setValues(originalObjects, originalSubElements);
return -1;
}
acceptGeometry(); // This may need to be refactored into onChanged for ExternalGeometry
solverNeedsUpdate = true;
return ExternalGeometry.getValues().size() - 1;
}
// clang-format off
int SketchObject::delExternal(int ExtGeoId)
{
return delExternal(std::vector<int>{ExtGeoId});
}
int SketchObject::delExternal(const std::vector<int>& ExtGeoIds)
{
std::set<long> geoIds;
for (int ExtGeoId : ExtGeoIds) {
int GeoId = ExtGeoId >= 0 ? GeoEnum::RefExt - ExtGeoId : ExtGeoId;
if (GeoId > GeoEnum::RefExt || -GeoId - 1 >= ExternalGeo.getSize())
return -1;
auto geo = getGeometry(GeoId);
if (!geo)
return -1;
auto egf = ExternalGeometryFacade::getFacade(geo);
geoIds.insert(egf->getId());
if (egf->getRef().size()) {
auto& refs = externalGeoRefMap[egf->getRef()];
geoIds.insert(refs.begin(), refs.end());
}
}
delExternalPrivate(geoIds, true);
return 0;
}
// clang-format on
void SketchObject::delExternalPrivate(const std::set<long>& ids, bool removeRef)
{
Base::StateLocker lock(managedoperation, true); // no need to check input data validity as this
// is an sketchobject managed operation.
std::set<std::string> refs;
// Must sort in reverse order so as to delete geo from back to front to
// avoid index change
std::set<int, std::greater<int>> geoIds;
for (auto id : ids) {
auto it = externalGeoMap.find(id);
if (it == externalGeoMap.end()) {
continue;
}
auto egf = ExternalGeometryFacade::getFacade(ExternalGeo[it->second]);
if (removeRef && egf->getRef().size()) {
refs.insert(egf->getRef());
}
geoIds.insert(-it->second - 1);
}
if (geoIds.empty()) {
return;
}
std::vector<Constraint*> newConstraints;
for (const auto& cstr : Constraints.getValues()) {
if (geoIds.count(cstr->First)
|| (cstr->Second != GeoEnum::GeoUndef && geoIds.count(cstr->Second))
|| (cstr->Third != GeoEnum::GeoUndef && geoIds.count(cstr->Third))) {
continue;
}
int offset = 0;
std::unique_ptr<Constraint> newCstr(cstr->clone());
for (auto GeoId : geoIds) {
GeoId += offset++;
if (newCstr->First >= GeoId && newCstr->Second >= GeoId && newCstr->Third >= GeoId) {
break;
}
changeConstraintAfterDeletingGeo(newCstr.get(), GeoId);
}
// need to provide raw pointer because that's the only one supported by `setValues`
newConstraints.push_back(newCstr.release());
}
auto geos = ExternalGeo.getValues();
int offset = 0;
for (auto geoId : geoIds) {
int idx = -geoId - 1;
geos.erase(geos.begin() + idx - offset);
++offset;
}
if (refs.empty()) {
ExternalGeo.setValues(std::move(geos));
solverNeedsUpdate = true;
Constraints.setValues(std::move(newConstraints));
acceptGeometry(); // This may need to be refactored into OnChanged for ExternalGeometry.
}
std::vector<std::string> newSubs;
std::vector<App::DocumentObject*> newObjs;
const auto& subs = ExternalGeometry.getSubValues();
auto itSub = subs.begin();
const auto& objs = ExternalGeometry.getValues();
auto itObj = objs.begin();
bool touched = false;
assert(externalGeoRef.size() == objs.size());
assert(externalGeoRef.size() == subs.size());
for (auto it = externalGeoRef.begin(); it != externalGeoRef.end(); ++it, ++itObj, ++itSub) {
if (refs.count(*it) == 0) {
touched = true;
newObjs.push_back(*itObj);
newSubs.push_back(*itSub);
}
}
if (touched) {
ExternalGeometry.setValues(newObjs, newSubs);
}
ExternalGeo.setValues(std::move(geos));
solverNeedsUpdate = true;
Constraints.setValues(std::move(newConstraints));
acceptGeometry(); // This may need to be refactored into OnChanged for ExternalGeometry.
}
// clang-format off
// clang-format on
int SketchObject::delAllExternal()
{
int count = 0; // the remaining count of the detached external geometry
std::map<int, int> indexMap; // the index map of the remain external geometry
std::vector<Part::Geometry*> geos; // the remaining external geometry
for (int i = 0; i < ExternalGeo.getSize(); ++i) {
auto geo = ExternalGeo[i];
auto egf = ExternalGeometryFacade::getFacade(geo);
if (egf->getRef().empty()) {
indexMap[i] = count++;
}
geos.push_back(geo);
}
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
// get the actual lists of the externals
std::vector<DocumentObject*> Objects = ExternalGeometry.getValues();
std::vector<std::string> SubElements = ExternalGeometry.getSubValues();
const std::vector<DocumentObject*> originalObjects = Objects;
const std::vector<std::string> originalSubElements = SubElements;
Objects.clear();
SubElements.clear();
const std::vector<Constraint*>& constraints = Constraints.getValues();
std::vector<Constraint*> newConstraints(0);
for (const auto& constr : constraints) {
if (constr->First > GeoEnum::RefExt
&& (constr->Second > GeoEnum::RefExt || constr->Second == GeoEnum::GeoUndef)
&& (constr->Third > GeoEnum::RefExt || constr->Third == GeoEnum::GeoUndef)) {
Constraint* copiedConstr = constr->clone();
newConstraints.push_back(copiedConstr);
}
}
ExternalGeometry.setValues(Objects, SubElements);
try {
rebuildExternalGeometry();
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
// revert to original values
ExternalGeometry.setValues(originalObjects, originalSubElements);
for (Constraint* it : newConstraints) {
delete it;
}
return -1;
}
ExternalGeometry.setValue(0);
ExternalGeo.setValues(std::move(geos));
solverNeedsUpdate = true;
Constraints.setValues(std::move(newConstraints));
acceptGeometry(); // This may need to be refactored into OnChanged for ExternalGeometry
return 0;
}
// clang-format off
int SketchObject::delConstraintsToExternal(DeleteOptions options)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
const std::vector<Constraint*>& constraints = Constraints.getValuesForce();
std::vector<Constraint*> newConstraints(0);
int GeoId = GeoEnum::RefExt, NullId = GeoEnum::GeoUndef;
for (const auto& constr : constraints) {
if (constr->First > GeoId && (constr->Second > GeoId || constr->Second == NullId)
&& (constr->Third > GeoId || constr->Third == NullId)) {
newConstraints.push_back(constr);
}
}
Constraints.setValues(std::move(newConstraints));
Constraints.acceptGeometry(getCompleteGeometry());
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver
if (noRecomputes && !options.testFlag(DeleteOption::NoFlag)) {
solve(options.testFlag(DeleteOption::UpdateGeometry));
}
return 0;
}
int SketchObject::attachExternal(
const std::vector<int> &geoIds, App::DocumentObject *Obj, const char* SubName)
{
if (!isExternalAllowed(Obj->getDocument(), Obj))
return -1;
std::set<std::string> detached;
std::set<int> idSet;
for (int geoId : geoIds) {
if (geoId > GeoEnum::RefExt || -geoId - 1 >= ExternalGeo.getSize())
continue;
auto geo = getGeometry(geoId);
if(!geo)
continue;
auto egf = ExternalGeometryFacade::getFacade(geo);
if(egf->getRef().size())
detached.insert(egf->getRef());
for(int id : getRelatedGeometry(geoId))
idSet.insert(id);
}
auto geos = ExternalGeo.getValues();
std::vector<DocumentObject*> Objects = ExternalGeometry.getValues();
auto itObj = Objects.begin();
std::vector<std::string> SubElements = ExternalGeometry.getSubValues();
auto itSub = SubElements.begin();
assert(Objects.size()==SubElements.size());
assert(externalGeoRef.size() == Objects.size());
for(auto &key : externalGeoRef) {
if (*itObj == Obj && *itSub == SubName){
FC_ERR("Duplicate external element reference in " << getFullName() << ": " << key);
return -1;
}
// detach old reference
if(detached.count(key)) {
itObj = Objects.erase(itObj);
itSub = SubElements.erase(itSub);
}else{
++itObj;
++itSub;
}
}
// add the new ones
Objects.push_back(Obj);
SubElements.push_back(std::string(SubName));
ExternalGeometry.setValues(Objects,SubElements);
if(externalGeoRef.size()!=Objects.size())
return -1;
std::string ref = externalGeoRef.back();
for(auto geoId : idSet) {
auto &geo = geos[-geoId-1];
geo = geo->clone();
ExternalGeometryFacade::getFacade(geo)->setRef(ref);
}
ExternalGeo.setValues(std::move(geos));
rebuildExternalGeometry();
return ExternalGeometry.getSize()-1;
}
std::vector<int> SketchObject::getRelatedGeometry(int GeoId) const {
std::vector<int> res;
if(GeoId>GeoEnum::RefExt || -GeoId-1>=ExternalGeo.getSize())
return res;
auto geo = getGeometry(GeoId);
if(!geo)
return res;
const std::string &ref = ExternalGeometryFacade::getFacade(geo)->getRef();
if(!ref.size())
return {GeoId};
auto iter = externalGeoRefMap.find(ref);
if(iter == externalGeoRefMap.end())
return {GeoId};
for(auto id : iter->second) {
auto it = externalGeoMap.find(id);
if(it!=externalGeoMap.end())
res.push_back(-it->second-1);
}
return res;
}
int SketchObject::syncGeometry(const std::vector<int> &geoIds) {
bool touched = false;
auto geos = ExternalGeo.getValues();
std::set<int> idSet;
for(int geoId : geoIds) {
auto geo = getGeometry(geoId);
if(!geo || !ExternalGeometryFacade::getFacade(geo)->testFlag(ExternalGeometryExtension::Frozen))
continue;
for(int gid : getRelatedGeometry(geoId))
idSet.insert(gid);
}
for(int geoId : idSet) {
if(geoId <= GeoEnum::RefExt && -geoId-1 < ExternalGeo.getSize()) {
auto &geo = geos[-geoId-1];
geo = geo->clone();
ExternalGeometryFacade::getFacade(geo)->setFlag(ExternalGeometryExtension::Sync);
touched = true;
}
}
if(touched)
ExternalGeo.setValues(std::move(geos));
return 0;
}
const Part::Geometry* SketchObject::_getGeometry(int GeoId) const
{
if (GeoId >= 0) {
const std::vector<Part::Geometry *> &geomlist = getInternalGeometry();
if (GeoId < int(geomlist.size()))
return geomlist[GeoId];
}
else if (-GeoId-1 < ExternalGeo.getSize()) {
return ExternalGeo[-GeoId-1];
}
return nullptr;
}
int SketchObject::getCompleteGeometryIndex(int GeoId) const
{
if (GeoId >= 0) {
if (GeoId < int(Geometry.getSize()))
return GeoId;
}
else if (-GeoId <= int(ExternalGeo.getSize()))
return -GeoId - 1;
return GeoEnum::GeoUndef;
}
int SketchObject::getGeoIdFromCompleteGeometryIndex(int completeGeometryIndex) const
{
int completeGeometryCount = int(Geometry.getSize() + ExternalGeo.getSize());
if (completeGeometryIndex < 0 || completeGeometryIndex >= completeGeometryCount)
return GeoEnum::GeoUndef;
if (completeGeometryIndex < Geometry.getSize())
return completeGeometryIndex;
else
return (completeGeometryIndex - completeGeometryCount);
}
int SketchObject::getSingleScaleDefiningConstraint() const
{
const std::vector<Constraint*>& vals = this->Constraints.getValues();
int found = -1;
for (size_t i = 0; i < vals.size(); ++i) {
// An angle does not define scale
if (vals[i]->isDimensional() && vals[i]->Type != Angle) {
if (found != -1) { // More than one scale defining constraint
return -1;
}
found = i;
}
}
return found;
}
std::unique_ptr<const GeometryFacade> SketchObject::getGeometryFacade(int GeoId) const
{
return GeometryFacade::getFacade(getGeometry(GeoId));
}
int SketchObject::setGeometry(int GeoId, const Part::Geometry *geo) {
std::unique_ptr<Part::Geometry> g(geo->clone());
if(GeoId>=0 && GeoId <Geometry.getSize()) {
Geometry.set1Value(GeoId,std::move(g));
} else if(GeoId <= GeoEnum::RefExt && -GeoId-1 < ExternalGeo.getSize()) {
ExternalGeo.set1Value(-GeoId-1,std::move(g));
} else
return -1;
return 0;
}
namespace {
// Auxiliary Method: returns vector projection in UV space of plane
static gp_Vec2d ProjVecOnPlane_UV(const gp_Vec& V, const gp_Pln& Pl)
{
return gp_Vec2d(V.Dot(Pl.Position().XDirection()), V.Dot(Pl.Position().YDirection()));
}
// Auxiliary Method: returns vector projection in UVN space of plane
static gp_Vec ProjVecOnPlane_UVN(const gp_Vec& V, const gp_Pln& Pl)
{
gp_Vec2d vector = ProjVecOnPlane_UV(V, Pl);
return gp_Vec(vector.X(), vector.Y(), 0.0);
}
// Auxiliary Method: returns point projection in UV space of plane
static gp_Vec2d ProjPointOnPlane_UV(const gp_Pnt& P, const gp_Pln& Pl)
{
gp_Vec OP = gp_Vec(Pl.Location(), P);
return ProjVecOnPlane_UV(OP, Pl);
}
// Auxiliary Method: returns point projection in UVN space of plane
static gp_Vec ProjPointOnPlane_UVN(const gp_Pnt& P, const gp_Pln& Pl)
{
gp_Vec2d vec2 = ProjPointOnPlane_UV(P, Pl);
return gp_Vec(vec2.X(), vec2.Y(), 0.0);
}
// Auxiliary Method: returns point projection in XYZ space
static gp_Pnt ProjPointOnPlane_XYZ(const gp_Pnt& P, const gp_Pln& Pl)
{
gp_Vec positionUVN = ProjPointOnPlane_UVN(P, Pl);
return gp_Pnt((positionUVN.X() * Pl.Position().XDirection()
+ positionUVN.Y() * Pl.Position().YDirection() + gp_Vec(Pl.Location().XYZ()))
.XYZ());
}
// Auxiliary method
Part::Geometry* projectLine(const BRepAdaptor_Curve& curve, const Handle(Geom_Plane) & gPlane,
const Base::Placement& invPlm)
{
double first = curve.FirstParameter();
if (fabs(first) > 1E99) {
// TODO: What is OCE's definition of Infinite?
// TODO: The clean way to do this is to handle a new sketch geometry Geom::Line
// but its a lot of work to implement...
first = -10000;
}
double last = curve.LastParameter();
if (fabs(last) > 1E99) {
last = +10000;
}
gp_Pnt P1 = curve.Value(first);
gp_Pnt P2 = curve.Value(last);
GeomAPI_ProjectPointOnSurf proj1(P1, gPlane);
P1 = proj1.NearestPoint();
GeomAPI_ProjectPointOnSurf proj2(P2, gPlane);
P2 = proj2.NearestPoint();
Base::Vector3d p1(P1.X(), P1.Y(), P1.Z());
Base::Vector3d p2(P2.X(), P2.Y(), P2.Z());
invPlm.multVec(p1, p1);
invPlm.multVec(p2, p2);
if (Base::Distance(p1, p2) < Precision::Confusion()) {
Base::Vector3d p = (p1 + p2) / 2;
auto* point = new Part::GeomPoint(p);
GeometryFacade::setConstruction(point, true);
return point;
}
else {
auto* line = new Part::GeomLineSegment();
line->setPoints(p1, p2);
GeometryFacade::setConstruction(line, true);
return line;
}
}
} // anonymous namespace
bool SketchObject::evaluateSupport()
{
// returns false if the shape is broken, null or non-planar
App::DocumentObject* link = AttachmentSupport.getValue();
if (!link || !link->isDerivedFrom<Part::Feature>())
return false;
return true;
}
static Part::Geometry *fitArcs(std::vector<std::unique_ptr<Part::Geometry> > &arcs,
const gp_Pnt &P1,
const gp_Pnt &P2,
double tol)
{
double radius = 0.0;
double m = 0.0;
Base::Vector3d center;
for (auto &geo : arcs) {
if (auto arc = freecad_cast<Part::GeomArcOfCircle*>(geo.get())) {
if (radius == 0.0) {
radius = arc->getRadius();
center = arc->getCenter();
double f = arc->getFirstParameter();
double l = arc->getLastParameter();
m = (l-f)*0.5 + f; // middle parameter
} else if (std::abs(radius - arc->getRadius()) > tol)
return nullptr;
} else
return nullptr;
}
if (radius == 0.0) {
return nullptr;
}
if (P1.SquareDistance(P2) < Precision::Confusion()) {
auto* circle = new Part::GeomCircle();
circle->setCenter(center);
circle->setRadius(radius);
return circle;
}
if (arcs.size() == 1) {
auto res = arcs.front().release();
arcs.clear();
return res;
}
GeomLProp_CLProps prop(Handle(Geom_Curve)::DownCast(arcs.front()->handle()),m,0,Precision::Confusion());
gp_Pnt midPoint = prop.Value();
GC_MakeArcOfCircle arc(P1, midPoint, P2);
auto* geo = new Part::GeomArcOfCircle();
geo->setHandle(arc.Value());
return geo;
}
void SketchObject::validateExternalLinks()
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
std::vector<DocumentObject*> Objects = ExternalGeometry.getValues();
std::vector<std::string> SubElements = ExternalGeometry.getSubValues();
bool rebuild = false;
for (int i = 0; i < int(Objects.size()); i++) {
const App::DocumentObject* Obj = Objects[i];
const std::string SubElement = SubElements[i];
TopoDS_Shape refSubShape;
try {
if (Obj->isDerivedFrom<Part::Datum>()) {
const auto* datum = static_cast<const Part::Datum*>(Obj);
refSubShape = datum->getShape();
}
else if (Obj->isDerivedFrom<App::DatumElement>()) {
// do nothing - shape will be calculated later during rebuild
}
else {
const auto* refObj = static_cast<const Part::Feature*>(Obj);
const Part::TopoShape& refShape = refObj->Shape.getShape();
refSubShape = refShape.getSubShape(SubElement.c_str());
}
continue; // no bad link needs to be removed
}
catch (Base::IndexError& indexError) {
Base::Console().warning(
this->getFullLabel(), (indexError.getMessage() + "\n").c_str());
}
catch (Base::ValueError& valueError) {
Base::Console().warning(
this->getFullLabel(), (valueError.getMessage() + "\n").c_str());
}
catch (Standard_Failure&) {
}
rebuild = true;
Objects.erase(Objects.begin() + i);
SubElements.erase(SubElements.begin() + i);
const std::vector<Constraint*>& constraints = Constraints.getValues();
std::vector<Constraint*> newConstraints;
newConstraints.reserve(constraints.size());
int GeoId = GeoEnum::RefExt - i;
for (const auto& constr : constraints) {
auto newConstr = getConstraintAfterDeletingGeo(constr, GeoId);
if (newConstr) {
newConstraints.push_back(newConstr.release());
}
}
Constraints.setValues(std::move(newConstraints));
i--;// we deleted an item, so the next one took its place
}
if (rebuild) {
ExternalGeometry.setValues(Objects, SubElements);
rebuildExternalGeometry();
acceptGeometry();// This may need to be refactor to OnChanged for ExternalGeo
solve(true); // we have to update this sketch and everything depending on it.
}
}
namespace {
void adjustParameterRange(const TopoDS_Edge &edge,
Handle(Geom_Plane) gPlane,
const gp_Trsf &mov,
Handle(Geom_Curve) curve,
double &firstParameter,
double &lastParameter)
{
// This function is to deal with the ambiguity of trimming a periodic
// curve, e.g. given two points on a circle, whether to get the upper or
// lower arc. Because projection orientation may swap the first and last
// parameter of the original curve.
//
// We project the middle point of the original curve to the projected curve
// to decide whether to flip the parameters.
Handle(Geom_Curve) origCurve = BRepAdaptor_Curve(edge).Curve().Curve();
// GeomAPI_ProjectPointOnCurve will project a point to an untransformed
// curve, so make sure to obtain the point on an untransformed edge.
auto e = edge.Located(TopLoc_Location());
gp_Pnt firstPoint = BRep_Tool::Pnt(TopExp::FirstVertex(TopoDS::Edge(e)));
double f = GeomAPI_ProjectPointOnCurve(firstPoint, origCurve).LowerDistanceParameter();
gp_Pnt lastPoint = BRep_Tool::Pnt(TopExp::LastVertex(TopoDS::Edge(e)));
double l = GeomAPI_ProjectPointOnCurve(lastPoint, origCurve).LowerDistanceParameter();
auto adjustPeriodic = [](Handle(Geom_Curve) curve, double &f, double &l) {
// Copied from Geom_TrimmedCurve::setTrim()
if (curve->IsPeriodic()) {
Standard_Real Udeb = curve->FirstParameter();
Standard_Real Ufin = curve->LastParameter();
// set f in the range Udeb , Ufin
// set l in the range f , f + Period()
ElCLib::AdjustPeriodic(Udeb, Ufin,
std::min(std::abs(f-l)/2,Precision::PConfusion()),
f, l);
}
};
// Adjust for periodic curve to deal with orientation
adjustPeriodic(origCurve, f, l);
// Obtain the middle parameter in order to get the mid point of the arc
double m = (l - f) * 0.5 + f;
GeomLProp_CLProps prop(origCurve,m,0,Precision::Confusion());
gp_Pnt midPoint = prop.Value();
// Transform all three points to the world coordinate
auto trsf = edge.Location().Transformation();
midPoint.Transform(trsf);
firstPoint.Transform(trsf);
lastPoint.Transform(trsf);
// Project the points to the sketch plane. Note the coordinates are still
// in world coordinate system.
gp_Pnt pm = GeomAPI_ProjectPointOnSurf(midPoint, gPlane).NearestPoint();
gp_Pnt pf = GeomAPI_ProjectPointOnSurf(firstPoint, gPlane).NearestPoint();
gp_Pnt pl = GeomAPI_ProjectPointOnSurf(lastPoint, gPlane).NearestPoint();
// Transform the projected points to sketch plane local coordinates
pm.Transform(mov);
pf.Transform(mov);
pl.Transform(mov);
// Obtain the corresponding parameters for those points in the projected curve
double f2 = GeomAPI_ProjectPointOnCurve(pf, curve).LowerDistanceParameter();
double l2 = GeomAPI_ProjectPointOnCurve(pl, curve).LowerDistanceParameter();
double m2 = GeomAPI_ProjectPointOnCurve(pm, curve).LowerDistanceParameter();
firstParameter = f2;
lastParameter = l2;
adjustPeriodic(curve, f2, l2);
adjustPeriodic(curve, f2, m2);
// If the middle point is out of range, it means we need to choose the
// other half of the arc.
if (m2 > l2){
std::swap(firstParameter, lastParameter);
}
}
void processEdge2(TopoDS_Edge& projEdge, std::vector<std::unique_ptr<Part::Geometry>>& geos)
{
BRepAdaptor_Curve projCurve(projEdge);
if (projCurve.GetType() == GeomAbs_Line) {
gp_Pnt P1 = projCurve.Value(projCurve.FirstParameter());
gp_Pnt P2 = projCurve.Value(projCurve.LastParameter());
Base::Vector3d p1(P1.X(), P1.Y(), P1.Z());
Base::Vector3d p2(P2.X(), P2.Y(), P2.Z());
if (Base::Distance(p1, p2) < Precision::Confusion()) {
Base::Vector3d p = (p1 + p2) / 2;
auto* point = new Part::GeomPoint(p);
GeometryFacade::setConstruction(point, true);
geos.emplace_back(point);
}
else {
auto* line = new Part::GeomLineSegment();
line->setPoints(p1, p2);
GeometryFacade::setConstruction(line, true);
geos.emplace_back(line);
}
}
else if (projCurve.GetType() == GeomAbs_Circle) {
gp_Circ c = projCurve.Circle();
gp_Pnt p = c.Location();
gp_Pnt P1 = projCurve.Value(projCurve.FirstParameter());
gp_Pnt P2 = projCurve.Value(projCurve.LastParameter());
if (P1.SquareDistance(P2) < Precision::Confusion()) {
auto* circle = new Part::GeomCircle();
circle->setRadius(c.Radius());
circle->setCenter(Base::Vector3d(p.X(), p.Y(), p.Z()));
GeometryFacade::setConstruction(circle, true);
geos.emplace_back(circle);
}
else {
auto* arc = new Part::GeomArcOfCircle();
Handle(Geom_Curve) curve = new Geom_Circle(c);
Handle(Geom_TrimmedCurve) tCurve = new Geom_TrimmedCurve(curve,
projCurve.FirstParameter(),
projCurve.LastParameter());
arc->setHandle(tCurve);
GeometryFacade::setConstruction(arc, true);
geos.emplace_back(arc);
}
}
else if (projCurve.GetType() == GeomAbs_BSplineCurve) {
// Unfortunately, a normal projection of a circle can also give
// a Bspline Split the spline into arcs
GeomConvert_BSplineCurveKnotSplitting bSplineSplitter(projCurve.BSpline(), 2);
auto* bspline = new Part::GeomBSplineCurve(projCurve.BSpline());
GeometryFacade::setConstruction(bspline, true);
geos.emplace_back(bspline);
}
else if (projCurve.GetType() == GeomAbs_Hyperbola) {
gp_Hypr e = projCurve.Hyperbola();
gp_Pnt p = e.Location();
gp_Pnt P1 = projCurve.Value(projCurve.FirstParameter());
gp_Pnt P2 = projCurve.Value(projCurve.LastParameter());
gp_Dir normal = e.Axis().Direction();
gp_Dir xdir = e.XAxis().Direction();
gp_Ax2 xdirref(p, normal);
if (P1.SquareDistance(P2) < Precision::Confusion()) {
auto* hyperbola = new Part::GeomHyperbola();
hyperbola->setMajorRadius(e.MajorRadius());
hyperbola->setMinorRadius(e.MinorRadius());
hyperbola->setCenter(Base::Vector3d(p.X(), p.Y(), p.Z()));
hyperbola->setAngleXU(-xdir.AngleWithRef(xdirref.XDirection(), normal));
GeometryFacade::setConstruction(hyperbola, true);
geos.emplace_back(hyperbola);
}
else {
auto* aoh = new Part::GeomArcOfHyperbola();
Handle(Geom_Curve) curve = new Geom_Hyperbola(e);
Handle(Geom_TrimmedCurve) tCurve = new Geom_TrimmedCurve(curve,
projCurve.FirstParameter(),
projCurve.LastParameter());
aoh->setHandle(tCurve);
GeometryFacade::setConstruction(aoh, true);
geos.emplace_back(aoh);
}
}
else if (projCurve.GetType() == GeomAbs_Parabola) {
gp_Parab e = projCurve.Parabola();
gp_Pnt p = e.Location();
gp_Pnt P1 = projCurve.Value(projCurve.FirstParameter());
gp_Pnt P2 = projCurve.Value(projCurve.LastParameter());
gp_Dir normal = e.Axis().Direction();
gp_Dir xdir = e.XAxis().Direction();
gp_Ax2 xdirref(p, normal);
if (P1.SquareDistance(P2) < Precision::Confusion()) {
auto* parabola = new Part::GeomParabola();
parabola->setFocal(e.Focal());
parabola->setCenter(Base::Vector3d(p.X(), p.Y(), p.Z()));
parabola->setAngleXU(-xdir.AngleWithRef(xdirref.XDirection(), normal));
GeometryFacade::setConstruction(parabola, true);
geos.emplace_back(parabola);
}
else {
auto* aop = new Part::GeomArcOfParabola();
Handle(Geom_Curve) curve = new Geom_Parabola(e);
Handle(Geom_TrimmedCurve) tCurve = new Geom_TrimmedCurve(curve,
projCurve.FirstParameter(),
projCurve.LastParameter());
aop->setHandle(tCurve);
GeometryFacade::setConstruction(aop, true);
geos.emplace_back(aop);
}
}
else if (projCurve.GetType() == GeomAbs_Ellipse) {
gp_Elips e = projCurve.Ellipse();
gp_Pnt p = e.Location();
gp_Pnt P1 = projCurve.Value(projCurve.FirstParameter());
gp_Pnt P2 = projCurve.Value(projCurve.LastParameter());
gp_Dir normal = gp_Dir(0, 0, 1);
gp_Ax2 xdirref(p, normal);
if (P1.SquareDistance(P2) < Precision::Confusion()) {
auto* ellipse = new Part::GeomEllipse();
Handle(Geom_Ellipse) curve = new Geom_Ellipse(e);
ellipse->setHandle(curve);
GeometryFacade::setConstruction(ellipse, true);
geos.emplace_back(ellipse);
}
else {
auto* aoe = new Part::GeomArcOfEllipse();
Handle(Geom_Curve) curve = new Geom_Ellipse(e);
Handle(Geom_TrimmedCurve) tCurve = new Geom_TrimmedCurve(curve,
projCurve.FirstParameter(),
projCurve.LastParameter());
aoe->setHandle(tCurve);
GeometryFacade::setConstruction(aoe, true);
geos.emplace_back(aoe);
}
}
else if (projCurve.GetType() == GeomAbs_BezierCurve) {
Handle(Geom_BSplineCurve) hBSpline = GeomConvert::CurveToBSplineCurve(projCurve.Bezier());
auto* bspline = new Part::GeomBSplineCurve(hBSpline);
GeometryFacade::setConstruction(bspline, true);
geos.emplace_back(bspline);
}
else if (projCurve.GetType() == GeomAbs_OffsetCurve) {
Handle(Geom_BSplineCurve) hBSpline = GeomConvert::CurveToBSplineCurve(projCurve.OffsetCurve());
auto* bspline = new Part::GeomBSplineCurve(hBSpline);
GeometryFacade::setConstruction(bspline, true);
geos.emplace_back(bspline);
}
else {
throw Base::NotImplementedError("Not yet supported geometry for external geometry");
}
}
void processEdge(const TopoDS_Edge& edge,
std::vector<std::unique_ptr<Part::Geometry>>& geos,
const Handle(Geom_Plane)& gPlane,
const Base::Placement& invPlm,
const gp_Trsf& mov,
const gp_Pln& sketchPlane,
const Base::Rotation& invRot,
gp_Ax3& sketchAx3,
TopoDS_Shape& aProjFace)
{
using std::numbers::pi;
BRepAdaptor_Curve curve(edge);
if (curve.GetType() == GeomAbs_Line) {
geos.emplace_back(projectLine(curve, gPlane, invPlm));
}
else if (curve.GetType() == GeomAbs_Circle) {
auto isFullCircle = [](const BRepAdaptor_Curve& curve) {
double f = curve.FirstParameter();
double l = curve.LastParameter();
gp_Circ c;
c.SetRadius(1.0);
// for a full circle this is ~ 0.0
double diff = std::abs(l - f - c.Length());
return diff < gp::Resolution();
};
gp_Dir vec1 = sketchPlane.Axis().Direction();
gp_Dir vec2 = curve.Circle().Axis().Direction();
// start point of arc of circle
gp_Pnt beg = curve.Value(curve.FirstParameter());
// end point of arc of circle
gp_Pnt end = curve.Value(curve.LastParameter());
if (vec1.IsParallel(vec2, Precision::Confusion())) {
gp_Circ circle = curve.Circle();
gp_Pnt cnt = circle.Location();
GeomAPI_ProjectPointOnSurf proj(cnt, gPlane);
cnt = proj.NearestPoint();
circle.SetLocation(cnt);
cnt.Transform(mov);
circle.Transform(mov);
if (beg.SquareDistance(end) < Precision::Confusion()) {
auto* gCircle = new Part::GeomCircle();
gCircle->setRadius(circle.Radius());
gCircle->setCenter(Base::Vector3d(cnt.X(), cnt.Y(), cnt.Z()));
GeometryFacade::setConstruction(gCircle, true);
geos.emplace_back(gCircle);
}
else {
auto* gArc = new Part::GeomArcOfCircle();
Handle(Geom_Curve) hCircle = new Geom_Circle(circle);
Handle(Geom_TrimmedCurve) tCurve = new Geom_TrimmedCurve(
hCircle, curve.FirstParameter(), curve.LastParameter());
gArc->setHandle(tCurve);
GeometryFacade::setConstruction(gArc, true);
geos.emplace_back(gArc);
}
}
else {
// creates an ellipse or a segment
gp_Circ origCircle = curve.Circle();
if (vec1.IsNormal(vec2, Precision::Angular())) {
// circle's normal vector in plane:
// projection is a line
// define center by projection
gp_Pnt cnt = origCircle.Location();
GeomAPI_ProjectPointOnSurf proj(cnt, gPlane);
cnt = proj.NearestPoint();
gp_Dir dirOrientation {vec1 ^ vec2};
gp_Dir dirLine(dirOrientation);
auto* projectedSegment = new Part::GeomLineSegment();
Geom_Line ligne(cnt, dirLine);// helper object to compute end points
gp_Pnt P1, P2; // end points of the segment, OCC style
ligne.D0(-origCircle.Radius(), P1);
ligne.D0(origCircle.Radius(), P2);
if (!curve.IsClosed()) {// arc of circle
double alpha =
dirOrientation.AngleWithRef(curve.Circle().XAxis().Direction(),
curve.Circle().Axis().Direction());
double baseAngle = curve.FirstParameter();
int tours = 0;
double startAngle = baseAngle + alpha;
// bring startAngle back in [-pi/2 , 3pi/2[
while (startAngle < -pi / 2.0 && tours < 10) {
startAngle = baseAngle + ++tours * 2.0 * pi + alpha;
}
while (startAngle >= 3.0 * pi / 2.0 && tours > -10) {
startAngle = baseAngle + --tours * 2.0 * pi + alpha;
}
// apply same offset to end angle
double endAngle = curve.LastParameter() + startAngle - baseAngle;
if (startAngle <= 0.0) {
if (endAngle <= 0.0) {
P1 = ProjPointOnPlane_XYZ(beg, sketchPlane);
P2 = ProjPointOnPlane_XYZ(end, sketchPlane);
}
else {
if (endAngle <= fabs(startAngle)) {
// P2 = P2 already defined
P1 = ProjPointOnPlane_XYZ(beg, sketchPlane);
}
else if (endAngle < pi) {
// P2 = P2, already defined
P1 = ProjPointOnPlane_XYZ(end, sketchPlane);
}
else {
// P1 = P1, already defined
// P2 = P2, already defined
}
}
}
else if (startAngle < pi) {
if (endAngle < pi) {
P1 = ProjPointOnPlane_XYZ(beg, sketchPlane);
P2 = ProjPointOnPlane_XYZ(end, sketchPlane);
}
else if (endAngle < 2.0 * pi - startAngle) {
P2 = ProjPointOnPlane_XYZ(beg, sketchPlane);
// P1 = P1, already defined
}
else if (endAngle < 2.0 * pi) {
P2 = ProjPointOnPlane_XYZ(end, sketchPlane);
// P1 = P1, already defined
}
else {
// P1 = P1, already defined
// P2 = P2, already defined
}
}
else {
if (endAngle < 2 * pi) {
P1 = ProjPointOnPlane_XYZ(beg, sketchPlane);
P2 = ProjPointOnPlane_XYZ(end, sketchPlane);
}
else if (endAngle < 4 * pi - startAngle) {
P1 = ProjPointOnPlane_XYZ(beg, sketchPlane);
// P2 = P2, already defined
}
else if (endAngle < 3 * pi) {
// P1 = P1, already defined
P2 = ProjPointOnPlane_XYZ(end, sketchPlane);
}
else {
// P1 = P1, already defined
// P2 = P2, already defined
}
}
}
Base::Vector3d p1(P1.X(), P1.Y(), P1.Z());// ends of segment FCAD style
Base::Vector3d p2(P2.X(), P2.Y(), P2.Z());
invPlm.multVec(p1, p1);
invPlm.multVec(p2, p2);
projectedSegment->setPoints(p1, p2);
GeometryFacade::setConstruction(projectedSegment, true);
geos.emplace_back(projectedSegment);
}
else {// general case, full circle or arc of circle
gp_Pnt cnt = origCircle.Location();
GeomAPI_ProjectPointOnSurf proj(cnt, gPlane);
// projection of circle center on sketch plane, 3D space
cnt = proj.NearestPoint();
// converting to FCAD style vector
Base::Vector3d p(cnt.X(), cnt.Y(), cnt.Z());
// transforming towards sketch's (x,y) coordinates
invPlm.multVec(p, p);
gp_Vec vecMajorAxis = vec1 ^ vec2;// major axis in 3D space
double minorRadius;// TODO use data type of vectors around...
double cosTheta;
// cos of angle between the two planes, assuming vectirs are normalized
// to 1
cosTheta = fabs(vec1.Dot(vec2));
minorRadius = origCircle.Radius() * cosTheta;
// maj axis into FCAD style vector
Base::Vector3d vectorMajorAxis(
vecMajorAxis.X(), vecMajorAxis.Y(), vecMajorAxis.Z());
// transforming to sketch's (x,y) coordinates
invRot.multVec(vectorMajorAxis, vectorMajorAxis);
// back to OCC
vecMajorAxis.SetXYZ(
gp_XYZ(vectorMajorAxis[0], vectorMajorAxis[1], vectorMajorAxis[2]));
// NB: force normal of ellipse to be normal of sketch's plane.
gp_Ax2 refFrameEllipse(
gp_Pnt(gp_XYZ(p[0], p[1], p[2])), gp_Vec(0, 0, 1), vecMajorAxis);
gp_Elips elipsDest;
elipsDest.SetPosition(refFrameEllipse);
elipsDest.SetMajorRadius(origCircle.Radius());
elipsDest.SetMinorRadius(minorRadius);
Handle(Geom_Ellipse) projCurve = new Geom_Ellipse(elipsDest);
if (isFullCircle(curve)) {
// projection is an ellipse
auto* ellipse = new Part::GeomEllipse();
ellipse->setHandle(projCurve);
GeometryFacade::setConstruction(ellipse, true);
geos.emplace_back(ellipse);
}
else {
// projection is an arc of ellipse
auto* aoe = new Part::GeomArcOfEllipse();
double firstParam, lastParam;
// adjust the parameter range to get the correct arc
adjustParameterRange(edge, gPlane, mov, projCurve, firstParam, lastParam);
Handle(Geom_TrimmedCurve) trimmedCurve = new Geom_TrimmedCurve(projCurve, firstParam, lastParam);
aoe->setHandle(trimmedCurve);
GeometryFacade::setConstruction(aoe, true);
geos.emplace_back(aoe);
}
}
}
}
else if (curve.GetType() == GeomAbs_Ellipse) {
gp_Pnt P1 = curve.Value(curve.FirstParameter());
gp_Pnt P2 = curve.Value(curve.LastParameter());
gp_Elips elipsOrig = curve.Ellipse();
gp_Elips elipsDest;
gp_Pnt origCenter = elipsOrig.Location();
gp_Pnt destCenter = ProjPointOnPlane_UVN(origCenter, sketchPlane).XYZ();
gp_Dir origAxisMajorDir = elipsOrig.XAxis().Direction();
gp_Vec origAxisMajor = elipsOrig.MajorRadius() * gp_Vec(origAxisMajorDir);
gp_Dir origAxisMinorDir = elipsOrig.YAxis().Direction();
gp_Vec origAxisMinor = elipsOrig.MinorRadius() * gp_Vec(origAxisMinorDir);
// Here, it used to be a test for parallel direction between the sketchplane and
// the elipsOrig, in which the original ellipse would be copied and translated
// to the new position. The problem with that approach is that for the sketcher
// the normal vector is always (0,0,1). If the original ellipse was not on the
// XY plane, the copy will not be either. Then, the dimensions would be wrong
// because of the different major axis direction (which is not projected on the
// XY plane). So here, we default to the more general ellipse construction
// algorithm.
//
// Doing that solves:
// https://forum.freecad.org/viewtopic.php?f=3&t=55284#p477522
// GENERAL ELLIPSE CONSTRUCTION ALGORITHM
//
// look for major axis of projected ellipse
//
// t is the parameter along the origin ellipse
// OM(t) = origCenter
// + majorRadius * cos(t) * origAxisMajorDir
// + minorRadius * sin(t) * origAxisMinorDir
gp_Vec2d PA = ProjVecOnPlane_UV(origAxisMajor, sketchPlane);
gp_Vec2d PB = ProjVecOnPlane_UV(origAxisMinor, sketchPlane);
double t_max = 2.0 * PA.Dot(PB) / (PA.SquareMagnitude() - PB.SquareMagnitude());
t_max = 0.5 * atan(t_max);// gives new major axis is most cases, but not all
double t_min = t_max + 0.5 * pi;
// ON_max = OM(t_max) gives the point, which projected on the sketch plane,
// becomes the apoapse of the projected ellipse.
gp_Vec ON_max = origAxisMajor * cos(t_max) + origAxisMinor * sin(t_max);
gp_Vec ON_min = origAxisMajor * cos(t_min) + origAxisMinor * sin(t_min);
gp_Vec destAxisMajor = ProjVecOnPlane_UVN(ON_max, sketchPlane);
gp_Vec destAxisMinor = ProjVecOnPlane_UVN(ON_min, sketchPlane);
double RDest = destAxisMajor.Magnitude();
double rDest = destAxisMinor.Magnitude();
if (RDest < rDest) {
double rTmp = rDest;
rDest = RDest;
RDest = rTmp;
gp_Vec axisTmp = destAxisMajor;
destAxisMajor = destAxisMinor;
destAxisMinor = axisTmp;
}
double sens = sketchAx3.Direction().Dot(elipsOrig.Position().Direction());
int flip = sens > 0.0 ? 1.0 : -1.0;
gp_Ax2 destCurveAx2(destCenter, gp_Dir(0, 0, flip), gp_Dir(destAxisMajor));
// projection is a circle
if ((RDest - rDest) < (double)Precision::Confusion()) {
Handle(Geom_Circle) projCurve = new Geom_Circle(destCurveAx2, 0.5 * (rDest + RDest));
if (P1.SquareDistance(P2) < Precision::Confusion()) {
auto* circle = new Part::GeomCircle();
circle->setHandle(projCurve);
GeometryFacade::setConstruction(circle, true);
geos.emplace_back(circle);
}
else {
auto* arc = new Part::GeomArcOfCircle();
double firstParam, lastParam;
adjustParameterRange(edge, gPlane, mov, projCurve, firstParam, lastParam);
Handle(Geom_TrimmedCurve) tCurve = new Geom_TrimmedCurve(projCurve, firstParam, lastParam);
arc->setHandle(tCurve);
GeometryFacade::setConstruction(arc, true);
geos.emplace_back(arc);
}
}
else {
if (sketchPlane.Position().Direction().IsNormal(
elipsOrig.Position().Direction(), Precision::Angular())) {
gp_Vec start = gp_Vec(destCenter.XYZ()) + destAxisMajor;
gp_Vec end = gp_Vec(destCenter.XYZ()) - destAxisMajor;
auto* projectedSegment = new Part::GeomLineSegment();
projectedSegment->setPoints(
Base::Vector3d(start.X(), start.Y(), start.Z()),
Base::Vector3d(end.X(), end.Y(), end.Z()));
GeometryFacade::setConstruction(projectedSegment, true);
geos.emplace_back(projectedSegment);
}
else {
elipsDest.SetPosition(destCurveAx2);
elipsDest.SetMajorRadius(destAxisMajor.Magnitude());
elipsDest.SetMinorRadius(destAxisMinor.Magnitude());
Handle(Geom_Ellipse) projCurve = new Geom_Ellipse(elipsDest);
if (P1.SquareDistance(P2) < Precision::Confusion()) {
auto* ellipse = new Part::GeomEllipse();
ellipse->setHandle(projCurve);
GeometryFacade::setConstruction(ellipse, true);
geos.emplace_back(ellipse);
}
else {
auto* aoe = new Part::GeomArcOfEllipse();
double firstParam, lastParam;
adjustParameterRange(edge, gPlane, mov, projCurve, firstParam, lastParam);
Handle(Geom_TrimmedCurve) tCurve = new Geom_TrimmedCurve(projCurve, firstParam, lastParam);
aoe->setHandle(tCurve);
GeometryFacade::setConstruction(aoe, true);
geos.emplace_back(aoe);
}
}
}
}
else {
gp_Pln plane;
auto shape = Part::TopoShape(edge);
bool planar = shape.findPlane(plane);
// Check if the edge is planar and plane is perpendicular to the projection plane
if (planar && plane.Axis().IsNormal(sketchPlane.Axis(), Precision::Angular())) {
// Project an edge to a line. Only works if the edge is planar and its plane is
// perpendicular to the projection plane. OCC has trouble handling
// BSpline projection to a straight line. Although it does correctly projects
// the line including extreme bounds (not always a case), it will produce a BSpline with degree
// more than one.
//
// The work around here is to use an aligned bounding box of the edge to get
// the projection of the extremum points to construct the projected line.
// First, transform the shape to the projection plane local coordinates.
shape.setPlacement(invPlm * shape.getPlacement());
// Align the z axis of the edge plane to the y axis of the projection
// plane, so that the extreme bound will be a line in the x axis direction
// of the projection plane.
double angle = plane.Axis().Direction().Angle(sketchPlane.YAxis().Direction());
gp_Trsf trsf;
if (fabs(angle) > Precision::Angular()) {
trsf.SetRotation(gp_Ax1(gp_Pnt(), gp_Dir(0, 0, 1)), angle);
shape.move(trsf);
}
// Make a copy to work around OCC circular edge transformation bug
shape = shape.makeElementCopy();
// Obtain the bounding box (precise version!) and move the extreme points back
// to the original location
auto bbox = shape.getBoundBoxOptimal();
if (!bbox.IsValid()){
throw Base::CADKernelError("Invalid bounding box");
}
gp_Pnt p1(bbox.MinX, bbox.MinY, 0);
gp_Pnt p2(bbox.MaxX, bbox.MaxY, 0);
if (fabs(angle) > Precision::Angular()) {
trsf.SetRotation(gp_Ax1(gp_Pnt(), gp_Dir(0, 0, 1)), -angle);
p1.Transform(trsf);
p2.Transform(trsf);
}
Base::Vector3d P1(p1.X(), p1.Y(), 0);
Base::Vector3d P2(p2.X(), p2.Y(), 0);
// check for degenerated case when the line is collapsed to a point
if (p1.SquareDistance(p2) < Precision::SquareConfusion()) {
auto* point = new Part::GeomPoint((P1 + P2) / 2);
GeometryFacade::setConstruction(point, true);
geos.emplace_back(point);
}
else {
auto* projectedSegment = new Part::GeomLineSegment();
projectedSegment->setPoints(P1, P2);
GeometryFacade::setConstruction(projectedSegment, true);
geos.emplace_back(projectedSegment);
}
}
else {
try {
Part::TopoShape projShape;
// Projection of the edge on parallel plane to the sketch plane is edge itself
// all we need to do is match coordinate systems
// for some reason OCC doesn't like to project a planar B-Spline to a plane parallel to it
if (planar && plane.Axis().Direction().IsParallel(sketchPlane.Axis().Direction(), Precision::Confusion())) {
TopoDS_Edge projEdge = edge;
// We need to trim the curve in case we are projecting a B-Spline segment
if(curve.GetType() == GeomAbs_BSplineCurve){
double Param1 = curve.FirstParameter();
double Param2 = curve.LastParameter();
if (Param1 > Param2){
std::swap(Param1, Param2);
}
// trim curve in case we are projecting a segment
auto bsplineCurve = curve.BSpline();
if(Param2 - Param1 > Precision::Confusion()){
bsplineCurve->Segment(Param1, Param2);
projEdge = BRepBuilderAPI_MakeEdge(bsplineCurve).Edge();
}
}
projShape.setShape(projEdge);
// We can't use gp_Pln::Distance() because we need to
// know which side the plane is regarding the sketch
const gp_Pnt& aP = sketchPlane.Location();
const gp_Pnt& aLoc = plane.Location ();
const gp_Dir& aDir = plane.Axis().Direction();
double d = (aDir.X() * (aP.X() - aLoc.X()) +
aDir.Y() * (aP.Y() - aLoc.Y()) +
aDir.Z() * (aP.Z() - aLoc.Z()));
gp_Trsf trsf;
trsf.SetTranslation(gp_Vec(aDir) * d);
projShape.transformShape(Part::TopoShape::convert(trsf), /*copy*/false);
} else {
// When planes not parallel or perpendicular, or edge is not planar
// normal projection is working just fine
BRepOffsetAPI_NormalProjection mkProj(aProjFace);
mkProj.Add(edge);
mkProj.Build();
projShape.setShape(mkProj.Projection());
}
if (!projShape.isNull() && projShape.hasSubShape(TopAbs_EDGE)) {
for (auto &e : projShape.getSubTopoShapes(TopAbs_EDGE)) {
// Transform copy of the edge to the sketch plane local coordinates
e.transformShape(invPlm.toMatrix(), /*copy*/true, /*checkScale*/true);
TopoDS_Edge projEdge = TopoDS::Edge(e.getShape());
processEdge2(projEdge, geos);
}
}
}
catch (Standard_Failure& e) {
throw Base::CADKernelError(e.GetMessageString());
}
}
}
}
std::vector<TopoDS_Shape> projectShape(const TopoDS_Shape& inShape, const gp_Ax3& viewAxis)
{
std::vector<TopoDS_Shape> res;
Handle(HLRBRep_Algo) brep_hlr;
try {
brep_hlr = new HLRBRep_Algo();
brep_hlr->Add(inShape);
gp_Trsf aTrsf;
aTrsf.SetTransformation(viewAxis);
HLRAlgo_Projector projector(aTrsf, false, 1);
brep_hlr->Projector(projector);
brep_hlr->Update();
brep_hlr->Hide();
}
catch (const Standard_Failure& e) {
Base::Console().error("GO::projectShape - OCC error - %s - while projecting shape\n",
e.GetMessageString());
throw Base::RuntimeError("SketchObject::projectShape - OCC error");
}
catch (...) {
throw Base::RuntimeError("SketchObject::projectShape - unknown error");
}
try {
HLRBRep_HLRToShape hlrToShape(brep_hlr);
if (!hlrToShape.VCompound().IsNull()) {
//TopAbs_COMPOUND to TopAbs_EDGE
res.push_back(hlrToShape.VCompound());
}
if (!hlrToShape.Rg1LineVCompound().IsNull()) {
res.push_back(hlrToShape.Rg1LineVCompound());
}
if (!hlrToShape.OutLineVCompound().IsNull()) {
res.push_back(hlrToShape.OutLineVCompound());
}
if (!hlrToShape.IsoLineVCompound().IsNull()) {
res.push_back(hlrToShape.IsoLineVCompound());
}
if (!hlrToShape.HCompound().IsNull()) {
res.push_back(hlrToShape.HCompound());
}
if (!hlrToShape.Rg1LineHCompound().IsNull()) {
res.push_back(hlrToShape.Rg1LineHCompound());
}
if (!hlrToShape.OutLineHCompound().IsNull()) {
res.push_back(hlrToShape.OutLineHCompound());
}
if (!hlrToShape.IsoLineHCompound().IsNull()) {
res.push_back(hlrToShape.IsoLineHCompound());
}
}
catch (const Standard_Failure&) {
throw Base::RuntimeError(
"SketchObject::projectShape - OCC error occurred while extracting edges");
}
catch (...) {
throw Base::RuntimeError(
"SketchObject::projectShape - unknown error occurred while extracting edges");
}
return res;
}
void processFace (const Rotation& invRot,
const Placement& invPlm,
const gp_Trsf& mov,
const gp_Pln& sketchPlane,
const Handle(Geom_Plane)& gPlane,
gp_Ax3& sketchAx3,
TopoDS_Shape& aProjFace,
std::vector<std::unique_ptr<Part::Geometry>>& geos,
TopoDS_Shape& refSubShape)
{
const TopoDS_Face& face = TopoDS::Face(refSubShape);
BRepAdaptor_Surface surface(face);
if (surface.GetType() == GeomAbs_Plane) {
// Check that the plane is perpendicular to the sketch plane
Geom_Plane plane = surface.Plane();
gp_Dir dnormal = plane.Axis().Direction();
gp_Dir snormal = sketchPlane.Axis().Direction();
// Extract all edges from the face
TopExp_Explorer edgeExp;
for (edgeExp.Init(face, TopAbs_EDGE); edgeExp.More(); edgeExp.Next()) {
TopoDS_Edge edge = TopoDS::Edge(edgeExp.Current());
// Process each edge
processEdge(edge, geos, gPlane, invPlm, mov, sketchPlane, invRot, sketchAx3, aProjFace);
}
if (fabs(dnormal.Angle(snormal) - std::numbers::pi/2) < Precision::Confusion()) {
// The face is normal to the sketch plane
// We don't want to keep the projection of all the edges of the face.
// We need a single line that goes from min to max of all the projections.
bool initialized = false;
Vector3d start, end;
// Lambda to determine if a point should replace start or end
auto updateExtremes = [&](const Vector3d& point) {
if ((point - start).Length() < (point - end).Length()) {
// `point` is closer to `start` than `end`, check if it's further out than `start`
if ((point - end).Length() > (end - start).Length()) {
start = point;
}
}
else {
// `point` is closer to `end`, check if it's further out than `end`
if ((point - start).Length() > (end - start).Length()) {
end = point;
}
}
};
for (auto& geo : geos) {
auto* line = dynamic_cast<Part::GeomLineSegment*>(geo.get());
if (!line) {
// The face being normal to the sketch, we should have
// only lines. This is just a fail-safe in case there's a
// straight bspline or something like this.
continue;
}
if (!initialized) {
start = line->getStartPoint();
end = line->getEndPoint();
initialized = true;
continue;
}
updateExtremes(line->getStartPoint());
updateExtremes(line->getEndPoint());
}
if (initialized) {
auto* unifiedLine = new Part::GeomLineSegment();
unifiedLine->setPoints(start, end);
geos.clear(); // Clear other segments
geos.emplace_back(unifiedLine);
}
else {
// In case we have not initialized, perhaps the projections were
// only straight bsplines.
// Then we use the old method that will give a line with 20000 length:
// Get vector that is normal to both sketch plane normal and plane normal.
// This is the line's direction
gp_Dir lnormal = dnormal.Crossed(snormal);
BRepBuilderAPI_MakeEdge builder(gp_Lin(plane.Location(), lnormal));
builder.Build();
if (builder.IsDone()) {
const TopoDS_Edge& edge = TopoDS::Edge(builder.Shape());
BRepAdaptor_Curve curve(edge);
if (curve.GetType() == GeomAbs_Line) {
geos.emplace_back(projectLine(curve, gPlane, invPlm));
}
}
}
}
}
else {
std::vector<TopoDS_Shape> res = projectShape(face, sketchAx3);
for (auto& resShape : res) {
TopExp_Explorer explorer(resShape, TopAbs_EDGE);
while (explorer.More()) {
TopoDS_Edge projEdge = TopoDS::Edge(explorer.Current());
processEdge2(projEdge, geos);
explorer.Next();
}
}
}
}
}
void SketchObject::rebuildExternalGeometry(std::optional<ExternalToAdd> extToAdd)
{
Base::StateLocker lock(managedoperation, true); // no need to check input data validity as this is an sketchobject managed operation.
// Analyze the state of existing external geometries to infer the desired state for new ones.
// If any geometry from a source link is "defining", we'll treat the whole link as "defining".
std::map<std::string, bool> linkIsDefiningMap;
for (const auto& geo : ExternalGeo.getValues()) {
auto egf = ExternalGeometryFacade::getFacade(geo);
if (!egf->getRef().empty()) {
bool isDefining = egf->testFlag(ExternalGeometryExtension::Defining);
if (linkIsDefiningMap.find(egf->getRef()) == linkIsDefiningMap.end()) {
linkIsDefiningMap[egf->getRef()] = isDefining;
}
else {
linkIsDefiningMap[egf->getRef()] = linkIsDefiningMap[egf->getRef()] && isDefining;
}
}
}
// get the actual lists of the externals
auto Types = ExternalTypes.getValues();
auto Objects = ExternalGeometry.getValues();
auto SubElements = ExternalGeometry.getSubValues();
assert(externalGeoRef.size() == Objects.size());
auto keys = externalGeoRef;
// re-check for any missing geometry element. The code here has a side
// effect that the linked external geometry will continue to work even if
// ExternalGeometry is wiped out.
for(auto &geo : ExternalGeo.getValues()) {
auto egf = ExternalGeometryFacade::getFacade(geo);
if(egf->getRef().size() && egf->testFlag(ExternalGeometryExtension::Missing)) {
const std::string &ref = egf->getRef();
auto pos = ref.find('.');
if(pos == std::string::npos)
continue;
std::string objName = ref.substr(0,pos);
auto obj = getDocument()->getObject(objName.c_str());
if(!obj)
continue;
App::ElementNamePair elementName;
App::GeoFeature::resolveElement(obj,ref.c_str()+pos+1,elementName);
if(elementName.oldName.size()
&& !App::GeoFeature::hasMissingElement(elementName.oldName.c_str()))
{
Objects.push_back(obj);
SubElements.push_back(elementName.oldName);
keys.push_back(ref);
}
}
}
Base::Placement Plm = Placement.getValue();
Base::Vector3d Pos = Plm.getPosition();
Base::Rotation Rot = Plm.getRotation();
Base::Rotation invRot = Rot.inverse();
Base::Vector3d dN(0, 0, 1);
Rot.multVec(dN, dN);
Base::Vector3d dX(1, 0, 0);
Rot.multVec(dX, dX);
Base::Placement invPlm = Plm.inverse();
Base::Matrix4D invMat = invPlm.toMatrix();
gp_Trsf mov;
mov.SetValues(invMat[0][0],
invMat[0][1],
invMat[0][2],
invMat[0][3],
invMat[1][0],
invMat[1][1],
invMat[1][2],
invMat[1][3],
invMat[2][0],
invMat[2][1],
invMat[2][2],
invMat[2][3]);
gp_Ax3 sketchAx3(
gp_Pnt(Pos.x, Pos.y, Pos.z), gp_Dir(dN.x, dN.y, dN.z), gp_Dir(dX.x, dX.y, dX.z));
gp_Pln sketchPlane(sketchAx3);
Handle(Geom_Plane) gPlane = new Geom_Plane(sketchPlane);
BRepBuilderAPI_MakeFace mkFace(sketchPlane);
TopoDS_Shape aProjFace = mkFace.Shape();
Types.resize(Objects.size(), static_cast<long>(ExtType::Projection));
std::set<std::string> refSet;
// We use a vector here to keep the order (roughly) the same as ExternalGeometry
std::vector<std::vector<std::unique_ptr<Part::Geometry> > > newGeos;
newGeos.reserve(Objects.size());
for (int i=0; i < int(Objects.size()); i++) {
const App::DocumentObject *Obj=Objects[i];
const std::string &SubElement=SubElements[i];
const std::string &key = keys[i];
bool beingCreated = false;
if (extToAdd) {
beingCreated = extToAdd->obj == Obj && extToAdd->subname == SubElement;
}
bool projection = Types[i] == (int)ExtType::Projection || Types[i] == (int)ExtType::Both;
bool intersection = Types[i] == (int)ExtType::Intersection || Types[i] == (int)ExtType::Both;
// Skip frozen geometries
bool frozen = false;
bool sync = false;
for(auto id : externalGeoRefMap[key]) {
auto it = externalGeoMap.find(id);
if(it != externalGeoMap.end()) {
auto egf = ExternalGeometryFacade::getFacade(ExternalGeo[it->second]);
if(egf->testFlag(ExternalGeometryExtension::Frozen)) {
frozen = true;
}
if (egf->testFlag(ExternalGeometryExtension::Sync)) {
sync = true;
}
}
}
if(frozen && !sync) {
refSet.insert(std::move(key));
continue;
}
if (!Obj || !Obj->getNameInDocument()) {
continue;
}
std::vector<std::unique_ptr<Part::Geometry> > geos;
auto importVertex = [&](const TopoDS_Shape& refSubShape) {
gp_Pnt P = BRep_Tool::Pnt(TopoDS::Vertex(refSubShape));
GeomAPI_ProjectPointOnSurf proj(P, gPlane);
P = proj.NearestPoint();
Base::Vector3d p(P.X(), P.Y(), P.Z());
invPlm.multVec(p, p);
auto* point = new Part::GeomPoint(p);
GeometryFacade::setConstruction(point, true);
geos.emplace_back(point);
};
try {
TopoDS_Shape refSubShape;
if (Obj->isDerivedFrom<Part::Datum>()) {
auto* datum = static_cast<const Part::Datum*>(Obj);
refSubShape = datum->getShape();
}
else if (Obj->isDerivedFrom<Part::Feature>()) {
auto* refObj = static_cast<const Part::Feature*>(Obj);
const Part::TopoShape& refShape = refObj->Shape.getShape();
refSubShape = refShape.getSubShape(SubElement.c_str());
}
else if (Obj->isDerivedFrom<App::Plane>()) {
auto* pl = static_cast<const App::Plane*>(Obj);
Base::Placement plm = pl->Placement.getValue();
Base::Vector3d base = plm.getPosition();
Base::Rotation rot = plm.getRotation();
Base::Vector3d normal(0, 0, 1);
rot.multVec(normal, normal);
gp_Pln plane(gp_Pnt(base.x, base.y, base.z), gp_Dir(normal.x, normal.y, normal.z));
BRepBuilderAPI_MakeFace fBuilder(plane);
if (!fBuilder.IsDone())
throw Base::RuntimeError(
"Sketcher: addExternal(): Failed to build face from App::Plane");
TopoDS_Face f = TopoDS::Face(fBuilder.Shape());
refSubShape = f;
}
else if (Obj->isDerivedFrom<Part::DatumLine>()) {
auto* line = static_cast<const Part::DatumLine*>(Obj);
Base::Placement plm = line->Placement.getValue();
Base::Vector3d base = plm.getPosition();
Base::Vector3d dir = line->getDirection();
gp_Lin l(gp_Pnt(base.x, base.y, base.z), gp_Dir(dir.x, dir.y, dir.z));
BRepBuilderAPI_MakeEdge eBuilder(l);
if (!eBuilder.IsDone()) {
throw Base::RuntimeError(
"Sketcher: addExternal(): Failed to build edge from Part::DatumLine");
}
TopoDS_Edge e = TopoDS::Edge(eBuilder.Shape());
refSubShape = e;
}
else if (Obj->isDerivedFrom<Part::DatumPoint>()) {
auto* point = static_cast<const Part::DatumPoint*>(Obj);
Base::Placement plm = point->Placement.getValue();
Base::Vector3d base = plm.getPosition();
gp_Pnt p(base.x, base.y, base.z);
BRepBuilderAPI_MakeVertex eBuilder(p);
if (!eBuilder.IsDone()) {
throw Base::RuntimeError(
"Sketcher: addExternal(): Failed to build vertex from Part::DatumPoint");
}
TopoDS_Vertex v = TopoDS::Vertex(eBuilder.Shape());
refSubShape = v;
}
else {
throw Base::TypeError(
"Datum feature type is not yet supported as external geometry for a sketch");
}
if (projection && !refSubShape.IsNull()) {
switch (refSubShape.ShapeType()) {
case TopAbs_FACE: {
processFace(invRot, invPlm, mov, sketchPlane, gPlane, sketchAx3, aProjFace, geos, refSubShape);
} break;
case TopAbs_EDGE: {
const TopoDS_Edge& edge = TopoDS::Edge(refSubShape);
processEdge(edge, geos, gPlane, invPlm, mov, sketchPlane, invRot, sketchAx3, aProjFace);
} break;
case TopAbs_VERTEX: {
importVertex(refSubShape);
} break;
default:
throw Base::TypeError("Unknown type of geometry");
break;
}
if (beingCreated && !extToAdd->intersection) {
// We are adding the projections, so we need to initialize those
for (auto& geo : geos) {
auto egf = ExternalGeometryFacade::getFacade(geo.get());
egf->setFlag(ExternalGeometryExtension::Defining, extToAdd->defining);
}
}
}
int projSize = geos.size();
if (intersection && !refSubShape.IsNull()) {
FCBRepAlgoAPI_Section maker(refSubShape, sketchPlane);
maker.Approximation(Standard_True);
if (!maker.IsDone())
FC_THROWM(Base::CADKernelError, "Failed to get intersection");
Part::TopoShape intersectionShape(maker.Shape());
auto edges = intersectionShape.getSubTopoShapes(TopAbs_EDGE);
for (const auto& s : edges) {
TopoDS_Edge edge = TopoDS::Edge(s.getShape());
processEdge(edge, geos, gPlane, invPlm, mov, sketchPlane, invRot, sketchAx3, aProjFace);
}
// Section of some face (e.g. sphere) produce more than one arcs
// from the same circle. So we try to fit the arcs with a single
// circle/arc.
if (refSubShape.ShapeType() == TopAbs_FACE && geos.size() > 1) {
auto wires = Part::TopoShape().makeElementWires(edges);
if (wires.countSubShapes(TopAbs_WIRE) == 1) {
TopoDS_Vertex firstVertex, lastVertex;
BRepTools_WireExplorer exp(TopoDS::Wire(wires.getSubShape(TopAbs_WIRE, 1)));
firstVertex = exp.CurrentVertex();
while (!exp.More())
exp.Next();
lastVertex = exp.CurrentVertex();
gp_Pnt P1 = BRep_Tool::Pnt(firstVertex);
gp_Pnt P2 = BRep_Tool::Pnt(lastVertex);
if (auto geo = fitArcs(geos, P1, P2, ArcFitTolerance.getValue())) {
geos.clear();
geos.emplace_back(geo);
}
}
}
for (const auto& s : intersectionShape.getSubShapes(TopAbs_VERTEX, TopAbs_EDGE)) {
importVertex(s);
}
if (beingCreated && extToAdd->intersection) {
// We are adding the projections, so we need to initialize those
for (size_t i = projSize; i < geos.size(); ++i) {
auto egf = ExternalGeometryFacade::getFacade(geos[i].get());
egf->setFlag(ExternalGeometryExtension::Defining, extToAdd->defining);
}
}
}
} catch (Base::Exception &e) {
FC_ERR("Failed to project external geometry in "
<< getFullName() << ": " << key << std::endl << e.what());
continue;
} catch (Standard_Failure &e) {
FC_ERR("Failed to project external geometry in "
<< getFullName() << ": " << key << std::endl << e.GetMessageString());
continue;
} catch (std::exception &e) {
FC_ERR("Failed to project external geometry in "
<< getFullName() << ": " << key << std::endl << e.what());
continue;
} catch (...) {
FC_ERR("Failed to project external geometry in "
<< getFullName() << ": " << key << std::endl << "Unknown exception");
continue;
}
if (geos.empty()) {
continue;
}
if(!refSet.emplace(key).second) {
FC_WARN("Duplicated external reference in " << getFullName() << ": " << key);
continue;
}
for (auto& geo : geos) {
ExternalGeometryFacade::getFacade(geo.get())->setRef(key);
}
newGeos.push_back(std::move(geos));
}
// allocate unique geometry id
for(auto &geos : newGeos) {
auto egf = ExternalGeometryFacade::getFacade(geos.front().get());
auto &refs = externalGeoRefMap[egf->getRef()];
while(refs.size() < geos.size())
refs.push_back(++geoLastId);
// In case a projection reduces output geometries, delete them
std::set<long> geoIds;
geoIds.insert(refs.begin()+geos.size(),refs.end());
// Sync id and ref of the new geometries
int i = 0;
for(auto &geo : geos)
GeometryFacade::setId(geo.get(), refs[i++]);
delExternalPrivate(geoIds,false);
}
auto geoms = ExternalGeo.getValues();
// now update the geometries
for(auto &geos : newGeos) {
if (geos.empty()) {
continue;
}
// Get the reference key for this group of geometries. All geos in this vector share the same ref.
const std::string& key = ExternalGeometryFacade::getFacade(geos.front().get())->getRef();
auto itKey = linkIsDefiningMap.find(key);
bool hasLinkState = itKey != linkIsDefiningMap.end();
bool isLinkDefining = hasLinkState ? itKey->second : false;
for(auto &geo : geos) {
auto it = externalGeoMap.find(GeometryFacade::getId(geo.get()));
if(it == externalGeoMap.end()) {
// This is a new geometries.
// Set its defining state based on the inferred state of its parent link.
if (hasLinkState) {
ExternalGeometryFacade::getFacade(geo.get())->setFlag(ExternalGeometryExtension::Defining, isLinkDefining);
}
geoms.push_back(geo.release());
continue;
}
// This is an existing geometry. Update it while keeping the old flags
ExternalGeometryFacade::copyFlags(geoms[it->second], geo.get());
geoms[it->second] = geo.release();
}
}
// Check for any missing references
bool hasError = false;
for(auto geo : geoms) {
auto egf = ExternalGeometryFacade::getFacade(geo);
egf->setFlag(ExternalGeometryExtension::Sync,false);
if(egf->getRef().empty())
continue;
if(!refSet.count(egf->getRef())) {
FC_ERR( "External geometry " << getFullName() << ".e" << egf->getId()
<< " missing reference: " << egf->getRef());
hasError = true;
egf->setFlag(ExternalGeometryExtension::Missing,true);
} else {
egf->setFlag(ExternalGeometryExtension::Missing,false);
}
}
ExternalGeo.setValues(std::move(geoms));
rebuildVertexIndex();
// clean up geometry reference
if(refSet.size() != (size_t)ExternalGeometry.getSize()) {
if(refSet.size() < keys.size()) {
auto itObj = Objects.begin();
auto itSub = SubElements.begin();
for(auto &ref : keys) {
if(!refSet.count(ref)) {
itObj = Objects.erase(itObj);
itSub = SubElements.erase(itSub);
}else {
++itObj;
++itSub;
}
}
}
ExternalGeometry.setValues(Objects,SubElements);
}
solverNeedsUpdate=true;
Constraints.acceptGeometry(getCompleteGeometry());
if (hasError && this->isRecomputing()) {
throw Base::RuntimeError("Missing external geometry reference");
}
}
void SketchObject::fixExternalGeometry(const std::vector<int> &geoIds) {
std::set<int> idSet(geoIds.begin(),geoIds.end());
auto geos = ExternalGeo.getValues();
auto objs = ExternalGeometry.getValues();
auto subs = ExternalGeometry.getSubValues();
bool touched = false;
for(int i=2;i<(int)geos.size();++i) {
auto &geo = geos[i];
auto egf = ExternalGeometryFacade::getFacade(geo);
int GeoId = -i-1;
if(egf->getRef().empty()
|| !egf->testFlag(ExternalGeometryExtension::Missing)
|| (idSet.size() && !idSet.count(GeoId)))
continue;
std::string ref = egf->getRef();
auto pos = ref.find('.');
if(pos == std::string::npos) {
FC_ERR("Invalid geometry reference " << ref);
continue;
}
std::string objName = ref.substr(0,pos);
auto obj = getDocument()->getObject(objName.c_str());
if(!obj) {
FC_ERR("Cannot find object in reference " << ref);
continue;
}
auto elements = Part::Feature::getRelatedElements(obj,ref.c_str()+pos+1);
if(!elements.size()) {
FC_ERR("No related reference found for " << ref);
continue;
}
geo = geo->clone();
egf->setGeometry(geo);
egf->setFlag(ExternalGeometryExtension::Missing,false);
ref = objName + "." + Data::ComplexGeoData::elementMapPrefix();
elements.front().name.appendToBuffer(ref);
egf->setRef(ref);
objs.push_back(obj);
subs.emplace_back();
elements.front().index.appendToStringBuffer(subs.back());
touched = true;
}
if(touched) {
ExternalGeo.setValues(geos);
ExternalGeometry.setValues(objs,subs);
rebuildExternalGeometry();
}
}
std::vector<Part::Geometry*> SketchObject::getCompleteGeometry() const
{
std::vector<Part::Geometry*> vals = getInternalGeometry();
const auto &geos = getExternalGeometry();
vals.insert(vals.end(), geos.rbegin(), geos.rend()); // in reverse order
return vals;
}
GeoListFacade SketchObject::getGeoListFacade() const
{
std::vector<GeometryFacadeUniquePtr> facade;
facade.reserve(Geometry.getSize() + ExternalGeo.getSize());
for (auto geo : Geometry.getValues())
facade.push_back(GeometryFacade::getFacade(geo));
const auto &externalGeos = ExternalGeo.getValues();
for(auto rit = externalGeos.rbegin(); rit != externalGeos.rend(); rit++)
facade.push_back(GeometryFacade::getFacade(*rit));
return GeoListFacade::getGeoListModel(std::move(facade), Geometry.getSize());
}
void SketchObject::rebuildVertexIndex()
{
VertexId2GeoId.resize(0);
VertexId2PosId.resize(0);
int imax = getHighestCurveIndex();
int i = 0;
const std::vector<Part::Geometry*> geometry = getCompleteGeometry();
if (geometry.size() <= 2)
return;
for (auto it = geometry.cbegin(); it != geometry.cend() - 2; ++it, ++i) {
if (i > imax)
i = -getExternalGeometryCount();
if ((*it)->is<Part::GeomPoint>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::start);
}
else if ((*it)->is<Part::GeomLineSegment>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::start);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::end);
}
else if ((*it)->is<Part::GeomCircle>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::mid);
}
else if ((*it)->is<Part::GeomEllipse>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::mid);
}
else if ((*it)->is<Part::GeomArcOfCircle>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::start);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::end);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::mid);
}
else if ((*it)->is<Part::GeomArcOfEllipse>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::start);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::end);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::mid);
}
else if ((*it)->is<Part::GeomArcOfHyperbola>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::start);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::end);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::mid);
}
else if ((*it)->is<Part::GeomArcOfParabola>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::start);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::end);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::mid);
}
else if ((*it)->is<Part::GeomBSplineCurve>()) {
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::start);
VertexId2GeoId.push_back(i);
VertexId2PosId.push_back(PointPos::end);
}
}
}
const std::vector<std::map<int, Sketcher::PointPos>> SketchObject::getCoincidenceGroups()
{
// this function is different from that in getCoincidentPoints in that:
// - getCoincidentPoints only considers direct coincidence (the points that are linked via a
// single coincidence)
// - this function provides an array of maps of points, each map containing the points that are
// coincident by virtue
// of any number of interrelated coincidence constraints (if coincidence 1-2 and coincidence
// 2-3, {1,2,3} are in that set)
const std::vector<Sketcher::Constraint*>& vals = Constraints.getValues();
std::vector<std::map<int, Sketcher::PointPos>> coincidenttree;
// push the constraints
for (const auto& constr : vals) {
if (constr->Type != Sketcher::Coincident) {
continue;
}
int firstpresentin = -1;
int secondpresentin = -1;
int i = 0;
for (auto iti = coincidenttree.begin(); iti != coincidenttree.end(); ++iti, ++i) {
// First
std::map<int, Sketcher::PointPos>::const_iterator filiterator;
filiterator = (*iti).find(constr->First);
if (filiterator != (*iti).end() && constr->FirstPos == (*filiterator).second) {
firstpresentin = i;
}
// Second
filiterator = (*iti).find(constr->Second);
if (filiterator != (*iti).end() && constr->SecondPos == (*filiterator).second) {
secondpresentin = i;
}
}
if (firstpresentin != -1 && secondpresentin != -1) {
// we have to merge those sets into one
coincidenttree[firstpresentin].insert(coincidenttree[secondpresentin].begin(),
coincidenttree[secondpresentin].end());
coincidenttree.erase(coincidenttree.begin() + secondpresentin);
}
else if (firstpresentin == -1 && secondpresentin == -1) {
// we do not have any of the values, so create a setCursor
std::map<int, Sketcher::PointPos> tmp;
tmp.insert(std::pair<int, Sketcher::PointPos>(constr->First, constr->FirstPos));
tmp.insert(std::pair<int, Sketcher::PointPos>(constr->Second, constr->SecondPos));
coincidenttree.push_back(std::move(tmp));
}
else if (firstpresentin != -1) {
// add to existing group
coincidenttree[firstpresentin].insert(
std::pair<int, Sketcher::PointPos>(constr->Second, constr->SecondPos));
}
else {// secondpresentin != -1
// add to existing group
coincidenttree[secondpresentin].insert(
std::pair<int, Sketcher::PointPos>(constr->First, constr->FirstPos));
}
}
return coincidenttree;
}
void SketchObject::isCoincidentWithExternalGeometry(int GeoId, bool& start_external,
bool& mid_external, bool& end_external)
{
start_external = false;
mid_external = false;
end_external = false;
const std::vector<std::map<int, Sketcher::PointPos>> coincidenttree = getCoincidenceGroups();
for (const auto& cGroup : coincidenttree) {
const auto& geoId1iterator = cGroup.find(GeoId);
if (geoId1iterator == cGroup.end()) {
continue;
}
if (cGroup.begin()->first >= 0) {
continue;
}
// `GeoId` is in this set and the first key in this ordered element key is external
if (geoId1iterator->second == Sketcher::PointPos::start)
start_external = true;
else if (geoId1iterator->second == Sketcher::PointPos::mid)
mid_external = true;
else if (geoId1iterator->second == Sketcher::PointPos::end)
end_external = true;
}
}
const std::map<int, Sketcher::PointPos> SketchObject::getAllCoincidentPoints(int GeoId,
PointPos PosId)
{
const std::vector<std::map<int, Sketcher::PointPos>> coincidenttree = getCoincidenceGroups();
for (const auto& cGroup : coincidenttree) {
std::map<int, Sketcher::PointPos>::const_iterator geoId1iterator;
geoId1iterator = cGroup.find(GeoId);
if (geoId1iterator != cGroup.end()) {
// If GeoId is in this set
if (geoId1iterator->second == PosId)// and posId matches
return cGroup;
}
}
std::map<int, Sketcher::PointPos> empty;
return empty;
}
void SketchObject::getDirectlyCoincidentPoints(int GeoId, PointPos PosId,
std::vector<int>& GeoIdList,
std::vector<PointPos>& PosIdList) const
{
const std::vector<Constraint*>& constraints = this->Constraints.getValues();
GeoIdList.clear();
PosIdList.clear();
GeoIdList.push_back(GeoId);
PosIdList.push_back(PosId);
for (const auto& constr : constraints) {
if (constr->Type == Sketcher::Coincident) {
if (constr->First == GeoId && constr->FirstPos == PosId) {
GeoIdList.push_back(constr->Second);
PosIdList.push_back(constr->SecondPos);
}
else if (constr->Second == GeoId && constr->SecondPos == PosId) {
GeoIdList.push_back(constr->First);
PosIdList.push_back(constr->FirstPos);
}
}
if (constr->Type == Sketcher::Tangent) {
if (constr->First == GeoId && constr->FirstPos == PosId &&
(constr->SecondPos == Sketcher::PointPos::start ||
constr->SecondPos == Sketcher::PointPos::end)) {
GeoIdList.push_back(constr->Second);
PosIdList.push_back(constr->SecondPos);
}
if (constr->Second == GeoId && constr->SecondPos == PosId &&
(constr->FirstPos == Sketcher::PointPos::start ||
constr->FirstPos == Sketcher::PointPos::end)) {
GeoIdList.push_back(constr->First);
PosIdList.push_back(constr->FirstPos);
}
}
}
if (GeoIdList.size() == 1) {
GeoIdList.clear();
PosIdList.clear();
}
}
void SketchObject::getDirectlyCoincidentPoints(int VertexId, std::vector<int>& GeoIdList,
std::vector<PointPos>& PosIdList) const
{
int GeoId;
PointPos PosId;
getGeoVertexIndex(VertexId, GeoId, PosId);
getDirectlyCoincidentPoints(GeoId, PosId, GeoIdList, PosIdList);
}
bool SketchObject::arePointsCoincident(int GeoId1, PointPos PosId1, int GeoId2, PointPos PosId2)
{
if (GeoId1 == GeoId2 && PosId1 == PosId2)
return true;
const std::vector<std::map<int, Sketcher::PointPos>> coincidenttree = getCoincidenceGroups();
for (const auto& cGroup : coincidenttree) {
const auto& geoId1iterator = cGroup.find(GeoId1);
if (geoId1iterator != cGroup.end()) {
// If First is in this set
const auto& geoId2iterator = cGroup.find(GeoId2);
if (geoId2iterator != cGroup.end()) {
// If Second is in this set
if (geoId1iterator->second == PosId1 && geoId2iterator->second == PosId2)
return true;
}
}
}
return false;
}
bool SketchObject::hasBlockConstraint() const
{
return std::ranges::any_of(Constraints.getValues(), [](auto& c) {
return c->Type == Block;
});
}
void SketchObject::getConstraintIndices(int GeoId, std::vector<int>& constraintList)
{
const std::vector<Constraint*>& constraints = this->Constraints.getValues();
int i = 0;
for (const auto& constr : constraints) {
if (constr->involvesGeoId(GeoId)) {
constraintList.push_back(i);
}
++i;
}
}
void SketchObject::appendConflictMsg(const std::vector<int>& conflicting, std::string& msg)
{
appendConstraintsMsg(conflicting,
"Remove the following conflicting constraint:",
"Remove at least one of the following conflicting constraints:",
msg);
}
void SketchObject::appendRedundantMsg(const std::vector<int>& redundant, std::string& msg)
{
appendConstraintsMsg(redundant,
"Remove the following redundant constraint:",
"Remove the following redundant constraints:",
msg);
}
void SketchObject::appendMalformedConstraintsMsg(const std::vector<int>& malformed,
std::string& msg)
{
appendConstraintsMsg(malformed,
"Remove the following malformed constraint:",
"Remove the following malformed constraints:",
msg);
}
void SketchObject::appendConstraintsMsg(const std::vector<int>& vector,
const std::string& singularmsg,
const std::string& pluralmsg, std::string& msg)
{
std::stringstream ss;
if (msg.length() > 0)
ss << msg;
if (!vector.empty()) {
if (vector.size() == 1)
ss << singularmsg << std::endl;
else
ss << pluralmsg;
ss << vector[0] << std::endl;
for (unsigned int i = 1; i < vector.size(); i++)
ss << ", " << vector[i];
ss << "\n";
}
msg = ss.str();
}
void SketchObject::getGeometryWithDependentParameters(
std::vector<std::pair<int, PointPos>>& geometrymap)
{
auto geos = getInternalGeometry();
int geoid = -1;
for (auto geo : geos) {
++geoid;
if (!geo) {
continue;
}
if (!geo->hasExtension(Sketcher::SolverGeometryExtension::getClassTypeId())) {
continue;
}
auto solvext = std::static_pointer_cast<const Sketcher::SolverGeometryExtension>(
geo->getExtension(Sketcher::SolverGeometryExtension::getClassTypeId()).lock());
if (solvext->getGeometry()
!= Sketcher::SolverGeometryExtension::NotFullyConstraint) {
continue;
}
// The solver differentiates whether the parameters that are dependent are not
// those of start, end, mid, and assigns them to the edge (edge params = curve
// params - parms of start, end, mid). The user looking at the UI expects that
// the edge of a NotFullyConstraint geometry will always move, even if the edge
// parameters are independent, for example if mid is the only dependent
// parameter. In other words, the user could reasonably restrict the edge to
// reach a fully constrained element. Under this understanding, the edge
// parameter would always be dependent, unless the element is fully constrained.
//
// While this is ok from a user visual expectation point of view, it leads to a
// loss of information of whether restricting the point start, end, mid that is
// dependent may suffice, or even if such points are restricted, the edge would
// still need to be restricted.
//
// Because Python gets the information in this function, it would lead to Python
// users having access to a lower amount of detail.
//
// For this reason, this function returns edge as dependent parameter if and
// only if constraining the parameters of the points would not suffice to
// constraint the element.
if (solvext->getEdge() == SolverGeometryExtension::Dependent)
geometrymap.emplace_back(geoid, Sketcher::PointPos::none);
if (solvext->getStart() == SolverGeometryExtension::Dependent)
geometrymap.emplace_back(geoid, Sketcher::PointPos::start);
if (solvext->getEnd() == SolverGeometryExtension::Dependent)
geometrymap.emplace_back(geoid, Sketcher::PointPos::start);
if (solvext->getMid() == SolverGeometryExtension::Dependent)
geometrymap.emplace_back(geoid, Sketcher::PointPos::start);
}
}
bool SketchObject::evaluateConstraint(const Constraint* constraint) const
{
// if requireXXX, GeoUndef is treated as an error. If not requireXXX,
// GeoUndef is accepted. Index range checking is done on everything regardless.
// constraints always require a First!!
bool requireSecond = false;
bool requireThird = false;
switch (constraint->Type) {
case Radius:
case Diameter:
case Weight:
case Horizontal:
case Vertical:
case Distance:
case DistanceX:
case DistanceY:
case Coincident:
case Perpendicular:
case Parallel:
case Equal:
case PointOnObject:
case Angle:
break;
case Tangent:
requireSecond = true;
break;
case Symmetric:
case SnellsLaw:
requireSecond = true;
requireThird = true;
break;
default:
break;
}
int intGeoCount = getHighestCurveIndex() + 1;
int extGeoCount = getExternalGeometryCount();
// the actual checks
bool ret = true;
int geoId;
// First is always required and GeoId must be within range
geoId = constraint->First;
ret = ret && (geoId >= -extGeoCount && geoId < intGeoCount);
geoId = constraint->Second;
ret = ret
&& ((geoId == GeoEnum::GeoUndef && !requireSecond)
|| (geoId >= -extGeoCount && geoId < intGeoCount));
geoId = constraint->Third;
ret = ret
&& ((geoId == GeoEnum::GeoUndef && !requireThird)
|| (geoId >= -extGeoCount && geoId < intGeoCount));
return ret;
}
bool SketchObject::evaluateConstraints() const
{
int intGeoCount = getHighestCurveIndex() + 1;
int extGeoCount = getExternalGeometryCount();
std::vector<Part::Geometry*> geometry = getCompleteGeometry();
const std::vector<Sketcher::Constraint*>& constraints = Constraints.getValuesForce();
if (static_cast<int>(geometry.size()) != extGeoCount + intGeoCount) {
return false;
}
if (geometry.size() < 2) {
return false;
}
for (auto it : constraints) {
if (!evaluateConstraint(it)) {
return false;
}
}
if (!constraints.empty()) {
if (!Constraints.scanGeometry(geometry)) {
return false;
}
}
return true;
}
void SketchObject::validateConstraints()
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
std::vector<Part::Geometry*> geometry = getCompleteGeometry();
const std::vector<Sketcher::Constraint*>& constraints = Constraints.getValuesForce();
std::vector<Sketcher::Constraint*> newConstraints;
newConstraints.reserve(constraints.size());
std::vector<Sketcher::Constraint*>::const_iterator it;
for (it = constraints.begin(); it != constraints.end(); ++it) {
bool valid = evaluateConstraint(*it);
if (valid) {
newConstraints.push_back(*it);
}
}
if (newConstraints.size() != constraints.size()) {
Constraints.setValues(std::move(newConstraints));
acceptGeometry();
}
else if (!Constraints.scanGeometry(geometry)) {
Constraints.acceptGeometry(geometry);
}
}
std::string SketchObject::validateExpression(const App::ObjectIdentifier& path,
std::shared_ptr<const App::Expression> expr)
{
const App::Property* prop = path.getProperty();
assert(expr);
if (!prop)
return "Property not found";
if (prop == &Constraints) {
const Constraint* constraint = Constraints.getConstraint(path);
if (!constraint->isDriving)
return "Reference constraints cannot be set!";
}
auto deps = expr->getDeps();
auto it = deps.find(this);
if (it == deps.end()) {
return "";
}
auto it2 = it->second.find("Constraints");
if (it2 != it->second.end()) {
for (auto& oid : it2->second) {
const Constraint* constraint = Constraints.getConstraint(oid);
if (!constraint->isDriving)
return "Reference constraint from this sketch cannot be used in this "
"expression.";
}
}
geoMap.clear();
const auto &vals = getInternalGeometry();
for(long i=0; i<(long)vals.size(); ++i) {
auto geo = vals[i];
auto gf = GeometryFacade::getFacade(geo);
if(!gf->getId()) {
gf->setId(++geoLastId);
}
else if(gf->getId() > geoLastId) {
geoLastId = gf->getId();
}
while(!geoMap.insert(std::make_pair(gf->getId(),i)).second) {
FC_WARN("duplicate geometry id " << gf->getId() << " -> " << geoLastId+1);
gf->setId(++geoLastId);
}
}
updateGeoHistory();
return "";
}
// This function is necessary for precalculation of an angle when adding
// an angle constraint. It is also used here, in SketchObject, to
// lock down the type of tangency/perpendicularity.
double SketchObject::calculateAngleViaPoint(int GeoId1, int GeoId2, double px, double py)
{
// Temporary sketch based calculation. Slow, but guaranteed consistency with constraints.
Sketcher::Sketch sk;
const auto* p1 = dynamic_cast<const Part::GeomCurve*>(this->getGeometry(GeoId1));
const auto* p2 = dynamic_cast<const Part::GeomCurve*>(this->getGeometry(GeoId2));
if (p1 && p2) {
// TODO: Check if any of these are B-splines
int i1 = sk.addGeometry(this->getGeometry(GeoId1));
int i2 = sk.addGeometry(this->getGeometry(GeoId2));
if (p1->is<Part::GeomBSplineCurve>() ||
p2->is<Part::GeomBSplineCurve>()) {
double p1ClosestParam, p2ClosestParam;
Base::Vector3d pt(px, py, 0);
p1->closestParameter(pt, p1ClosestParam);
p2->closestParameter(pt, p2ClosestParam);
return sk.calculateAngleViaParams(i1, i2, p1ClosestParam, p2ClosestParam);
}
return sk.calculateAngleViaPoint(i1, i2, px, py);
}
else
throw Base::ValueError("Null geometry in calculateAngleViaPoint");
}
void SketchObject::constraintsRenamed(
const std::map<App::ObjectIdentifier, App::ObjectIdentifier>& renamed)
{
ExpressionEngine.renameExpressions(renamed);
for (auto doc : App::GetApplication().getDocuments())
doc->renameObjectIdentifiers(renamed);
}
void SketchObject::constraintsRemoved(const std::set<App::ObjectIdentifier>& removed)
{
auto i = removed.begin();
while (i != removed.end()) {
ExpressionEngine.setValue(*i, std::shared_ptr<App::Expression>());
++i;
}
}
// Tests if the provided point lies exactly in a curve (satisfies
// point-on-object constraint). It is used to decide whether it is nesessary to
// constrain a point onto curves when 3-element selection tangent-via-point-like
// constraints are applied.
bool SketchObject::isPointOnCurve(int geoIdCurve, double px, double py)
{
// DeepSOIC: this may be slow, but I wanted to reuse the existing code
Sketcher::Sketch sk;
int icrv = sk.addGeometry(this->getGeometry(geoIdCurve));
Base::Vector3d pp;
pp.x = px;
pp.y = py;
Part::GeomPoint p(pp);
int ipnt = sk.addPoint(p);
int icstr = sk.addPointOnObjectConstraint(ipnt, Sketcher::PointPos::start, icrv);
double err = sk.calculateConstraintError(icstr);
return err * err < 10.0 * sk.getSolverPrecision();
}
// This one was done just for fun to see to what precision the constraints are solved.
double SketchObject::calculateConstraintError(int ConstrId)
{
Sketcher::Sketch sk;
const std::vector<Constraint*>& clist = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(clist.size()))
return std::numeric_limits<double>::quiet_NaN();
Constraint* cstr = clist[ConstrId]->clone();
double result = 0.0;
try {
std::vector<int> GeoIdList;
int g;
GeoIdList.push_back(cstr->First);
GeoIdList.push_back(cstr->Second);
GeoIdList.push_back(cstr->Third);
// add only necessary geometry to the sketch
for (std::size_t i = 0; i < GeoIdList.size(); i++) {
g = GeoIdList[i];
if (g != GeoEnum::GeoUndef) {
GeoIdList[i] = sk.addGeometry(this->getGeometry(g));
}
}
cstr->First = GeoIdList[0];
cstr->Second = GeoIdList[1];
cstr->Third = GeoIdList[2];
int icstr = sk.addConstraint(cstr);
result = sk.calculateConstraintError(icstr);
}
catch (...) {// cleanup
delete cstr;
throw;
}
delete cstr;
return result;
}
PyObject* SketchObject::getPyObject()
{
if (PythonObject.is(Py::_None())) {
// ref counter is set to 1
PythonObject = Py::Object(new SketchObjectPy(this), true);
}
return Py::new_reference_to(PythonObject);
}
unsigned int SketchObject::getMemSize() const
{
return 0;
}
void SketchObject::Save(Writer& writer) const
{
int index = -1;
auto &geos = const_cast<Part::PropertyGeometryList&>(ExternalGeo).getValues();
for(auto geo : geos)
ExternalGeometryFacade::getFacade(geo)->setRefIndex(-1);
if(isExporting()) {
// We cannot export shape with the new topological naming, because it
// uses hasher indices that are unique only within its owner document.
// Therefore, we cannot rely on Geometry::Ref as key to map geometry to
// external object reference. So, before exporting, we pre-calculate
// the mapping and store them in Geometry::RefIndex. When importing,
// inside updateGeometryRefs() (called by onDocumentRestore()), we shall
// regenerate Geometry::Ref based on RefIndex.
//
// Note that the regenerated Ref will not be using the new topological
// naming either, because we didn't export them. This is exactly the
// same as if we are opening a legacy file without new names.
// updateGeometryRefs() will know how to handle the name change thanks
// to a flag setup in onUpdateElementReference().
for(auto &key : externalGeoRef) {
++index;
auto iter = externalGeoRefMap.find(key);
if(iter == externalGeoRefMap.end())
continue;
for(auto id : iter->second) {
auto it = externalGeoMap.find(id);
if(it != externalGeoMap.end())
ExternalGeometryFacade::getFacade(geos[it->second])->setRefIndex(index);
}
}
}
// save the father classes
Part::Part2DObject::Save(writer);
}
void SketchObject::Restore(XMLReader& reader)
{
// read the father classes
Part::Part2DObject::Restore(reader);
}
void SketchObject::handleChangedPropertyType(Base::XMLReader &reader,
const char *TypeName, App::Property *prop)
{
if (prop == &Exports) {
if(strcmp(TypeName, "App::PropertyLinkList") == 0)
Exports.Restore(reader);
}
}
static inline bool checkMigration(Part::PropertyGeometryList &prop)
{
for (auto g : prop.getValues()) {
if(g->hasExtension(Part::GeometryMigrationExtension::getClassTypeId())
|| !g->hasExtension(SketchGeometryExtension::getClassTypeId()))
return true;
}
return false;
}
void SketchObject::onChanged(const App::Property* prop)
{
if (prop == &Geometry) {
onGeometryChanged();
}
else if (prop == &Constraints) {
onConstraintsChanged();
}
else if (prop == &ExternalGeo && !prop->testStatus(App::Property::User3)) {
onExternalGeoChanged();
}
else if (prop == &ExternalGeometry) {
onExternalGeometryChanged();
}
else if (prop == &Placement) {
onPlacementChanged();
}
else if (prop == &ExpressionEngine) {
onExpressionEngineChanged();
}
#if 0
// For now do not delete anything (#0001791). When changing the support
// face it might be better to check which external geometries can be kept.
else if (prop == &AttachmentSupport) {
onAttachmentSupportChanged();
}
#endif
Part::Part2DObject::onChanged(prop);
}
void SketchObject::onGeometryChanged()
{
if (isRestoring() && checkMigration(Geometry)) {
// Construction migration to extension
for (auto geometryValue : Geometry.getValues()) {
if (!geometryValue->hasExtension(
Part::GeometryMigrationExtension::getClassTypeId())) {
continue;
}
auto ext = std::static_pointer_cast<Part::GeometryMigrationExtension>(
geometryValue
->getExtension(Part::GeometryMigrationExtension::getClassTypeId())
.lock());
// at this point IA geometry is already migrated
auto gf = GeometryFacade::getFacade(geometryValue);
if (ext->testMigrationType(Part::GeometryMigrationExtension::Construction)) {
bool oldconstr = ext->getConstruction()
|| (geometryValue->is<Part::GeomPoint>() && !gf->isInternalAligned());
gf->setConstruction(oldconstr);
}
if (ext->testMigrationType(Part::GeometryMigrationExtension::GeometryId)) {
gf->setId(ext->getId());
}
}
}
geoMap.clear();
const auto &vals = getInternalGeometry();
for (long i = 0; i < (long)vals.size(); ++i) {
auto geo = vals[i];
auto gf = GeometryFacade::getFacade(geo);
if (gf->getId() == 0) {
gf->setId(++geoLastId);
}
else if (gf->getId() > geoLastId) {
geoLastId = gf->getId();
}
while (!geoMap.insert(std::make_pair(gf->getId(), i)).second) {
FC_WARN("duplicate geometry id " << gf->getId() << " -> "
<< geoLastId + 1); // NOLINT
gf->setId(++geoLastId);
}
}
updateGeoHistory();
auto doc = getDocument();
if (doc && doc->isPerformingTransaction()) {
// undo/redo
setStatus(App::PendingTransactionUpdate, true);
return;
}
if (internaltransaction) {
return;
}
// internal sketchobject operations changing both geometry and constraints will
// explicitly perform an update
if (managedoperation || isRestoring()) {
// if geometry changed, the constraint geometry indices must be updated
acceptGeometry();
return;
}
// this change was not effect via SketchObject, but using direct access to
// properties, check input data
// declares constraint invalid if indices go beyond the geometry and any
// call to getValues with return an empty list until this is fixed.
bool invalidinput = Constraints.checkConstraintIndices(
getHighestCurveIndex(), -getExternalGeometryCount());
if (!invalidinput) {
acceptGeometry();
}
else {
Base::Console().error(
this->getFullLabel() + " SketchObject::onChanged ",
QT_TRANSLATE_NOOP("Notifications", "Unmanaged change of Constraint "
"Property results in invalid constraint indices") "\n");
}
Base::StateLocker lock(internaltransaction, true);
setUpSketch();
}
void SketchObject::onConstraintsChanged()
{
auto doc = getDocument();
if (doc && doc->isPerformingTransaction()) {
// undo/redo
setStatus(App::PendingTransactionUpdate, true);
return;
}
if (internaltransaction) {
return;
}
if (managedoperation || isRestoring()) {
Constraints.checkGeometry(getCompleteGeometry());
return;
}
// this change was not effect via SketchObject, but using direct access to
// properties, check input data
// declares constraint invalid if indices go beyond the geometry and any
// call to getValues with return an empty list until this is fixed.
bool invalidinput = Constraints.checkConstraintIndices(
getHighestCurveIndex(), -getExternalGeometryCount());
if (!invalidinput) {
if (Constraints.checkGeometry(getCompleteGeometry())) {
// if there are invalid geometry indices in the constraints, we need
// to update them
acceptGeometry();
}
}
else {
Base::Console().error(
this->getFullLabel() + " SketchObject::onChanged ",
QT_TRANSLATE_NOOP("Notifications", "Unmanaged change of Constraint "
"Property results in invalid constraint indices") "\n");
}
Base::StateLocker lock(internaltransaction, true);
setUpSketch();
}
/// not to be confused with `onExternalGeometryChanged`. These names may need fixing.
void SketchObject::onExternalGeoChanged()
{
if (ExternalGeo.testStatus(App::Property::User3)) {
return;
}
auto doc = getDocument();
if (doc && doc->isPerformingTransaction()) {
setStatus(App::PendingTransactionUpdate, true);
}
if (isRestoring() && checkMigration(ExternalGeo)) {
for (auto geometryValue : ExternalGeo.getValues()) {
if (!geometryValue->hasExtension(
Part::GeometryMigrationExtension::getClassTypeId())) {
continue;
}
auto ext = std::static_pointer_cast<Part::GeometryMigrationExtension>(
geometryValue
->getExtension(Part::GeometryMigrationExtension::getClassTypeId())
.lock());
std::unique_ptr<ExternalGeometryFacade> egf;
if (ext->testMigrationType(Part::GeometryMigrationExtension::GeometryId)) {
egf = ExternalGeometryFacade::getFacade(geometryValue);
egf->setId(ext->getId());
}
if (!ext->testMigrationType(Part::GeometryMigrationExtension::ExternalReference)) {
continue;
}
if (!egf) {
egf = ExternalGeometryFacade::getFacade(geometryValue);
}
egf->setRef(ext->getRef());
egf->setRefIndex(ext->getRefIndex());
egf->setFlags(ext->getFlags());
}
}
externalGeoRefMap.clear();
externalGeoMap.clear();
std::set<std::string> detached;
for(int i=0; i<ExternalGeo.getSize(); ++i) {
auto geo = ExternalGeo[i];
auto egf = ExternalGeometryFacade::getFacade(geo);
if (egf->testFlag(ExternalGeometryExtension::Detached)) {
if (!egf->getRef().empty()) {
detached.insert(egf->getRef());
egf->setRef(std::string());
}
egf->setFlag(ExternalGeometryExtension::Detached,false);
egf->setFlag(ExternalGeometryExtension::Missing,false);
}
if (egf->getId() > geoLastId) {
geoLastId = egf->getId();
}
if (!externalGeoMap.emplace(egf->getId(), i).second) {
FC_WARN("duplicate geometry id " << egf->getId() << " -> "
<< geoLastId + 1); // NOLINT
egf->setId(++geoLastId);
externalGeoMap[egf->getId()] = i;
}
if (!egf->getRef().empty()) {
externalGeoRefMap[egf->getRef()].push_back(egf->getId());
}
}
if (detached.empty()) {
signalElementsChanged();
return;
}
auto objs = ExternalGeometry.getValues();
assert(externalGeoRef.size() == objs.size());
auto itObj = objs.begin();
auto subs = ExternalGeometry.getSubValues();
auto itSub = subs.begin();
for (const auto& i : externalGeoRef) {
if (detached.count(i) == 0U) {
++itObj;
++itSub;
continue;
}
itObj = objs.erase(itObj);
itSub = subs.erase(itSub);
auto& refs = externalGeoRefMap[i];
for (long id : refs) {
auto it = externalGeoMap.find(id);
if (it!=externalGeoMap.end()) {
auto geo = ExternalGeo[it->second];
ExternalGeometryFacade::getFacade(geo)->setRef(std::string());
}
}
refs.clear();
}
ExternalGeometry.setValues(objs, subs);
}
void SketchObject::onExternalGeometryChanged()
{
auto doc = getDocument();
if (doc && doc->isPerformingTransaction()) {
setStatus(App::PendingTransactionUpdate, true);
}
if(!isRestoring()) {
// must wait till onDocumentRestored() when shadow references are
// fully restored
updateGeometryRefs();
signalElementsChanged();
}
}
void SketchObject::onPlacementChanged()
{
if (ExternalGeometry.getSize() > 0) {
touch();
}
}
void SketchObject::onExpressionEngineChanged()
{
auto doc = getDocument();
if (!isRestoring() && doc && !doc->isPerformingTransaction() && noRecomputes
&& !managedoperation) {
// if we do not have a recompute, the sketch must be solved to
// update the DoF of the solver, constraints and UI
try {
auto res = ExpressionEngine.execute();
if (res) {
FC_ERR("Failed to recompute " << ExpressionEngine.getFullName() << ": "
<< res->Why); // NOLINT
delete res;
}
} catch (Base::Exception &e) {
e.reportException();
FC_ERR("Failed to recompute " << ExpressionEngine.getFullName() << ": "
<< e.what()); // NOLINT
}
solve();
}
}
void SketchObject::onAttachmentSupportChanged()
{
// make sure not to change anything while restoring this object
if (isRestoring()) {
return;
}
// if support face has changed then clear the external geometry
delConstraintsToExternal();
for (int i=0; i < getExternalGeometryCount(); i++) {
delExternal(0);
}
rebuildExternalGeometry();
}
void SketchObject::onUpdateElementReference(const App::Property *prop)
{
if(prop == &ExternalGeometry) {
updateGeoRef = true;
// Must call updateGeometryRefs() now to avoid the case of recursive
// property change (e.g. temporary object removal in SubShapeBinder)
// afterwards causing assertion failure, although this may mean extra
// call of updateGeometryRefs() later in onChange().
updateGeometryRefs();
signalElementsChanged();
}
}
void SketchObject::updateGeometryRefs()
{
const auto &objs = ExternalGeometry.getValues();
const auto &subs = ExternalGeometry.getSubValues();
const auto &shadows = ExternalGeometry.getShadowSubs();
assert(subs.size() == shadows.size());
std::vector<std::string> originalRefs;
std::map<std::string,std::string> refMap;
if (updateGeoRef) {
assert(externalGeoRef.size() == objs.size());
updateGeoRef = false;
originalRefs = std::move(externalGeoRef);
}
externalGeoRef.clear();
std::unordered_map<std::string, int> legacyMap;
for (int i=0;i<(int)objs.size();++i) {
auto obj = objs[i];
const std::string& sub = shadows[i].newName.empty() ? subs[i] : shadows[i].newName;
externalGeoRef.emplace_back(obj->getNameInDocument());
auto &key = externalGeoRef.back();
key += '.';
legacyMap[key + Data::oldElementName(sub.c_str())] = i;
if (!obj->isDerivedFrom<Part::Datum>()) {
key += Data::newElementName(sub.c_str());
}
if (!originalRefs.empty() && originalRefs[i] != key) {
refMap[originalRefs[i]] = key;
}
}
bool touched = false;
auto geos = ExternalGeo.getValues();
if (!refMap.empty()) {
for(auto &v : refMap) {
auto it = externalGeoRefMap.find(v.first);
if (it == externalGeoRefMap.end()) {
continue;
}
for (long id : it->second) {
auto iter = externalGeoMap.find(id);
if (iter != externalGeoMap.end()) {
auto &geo = geos[iter->second];
geo = geo->clone();
auto egf = ExternalGeometryFacade::getFacade(geo);
// NOLINTNEXTLINE
FC_LOG(getFullName() << " ref change on ExternalEdge" << iter->second - 1 << ' '
<< egf->getRef() << " -> " << v.second);
egf->setRef(v.second);
touched = true;
}
}
}
if (touched) {
ExternalGeo.setValues(std::move(geos));
}
return;
}
// `refMap` is empty from here
for (auto geo : geos) {
auto egf = ExternalGeometryFacade::getFacade(geo);
if (egf->getRefIndex() >= 0) {
if (egf->getRefIndex() < (int)externalGeoRef.size()
&& egf->getRef() != externalGeoRef[egf->getRefIndex()]) {
touched = true;
egf->setRef(externalGeoRef[egf->getRefIndex()]);
}
egf->setRefIndex(-1);
continue;
}
if (egf->getId() < 0 && !egf->getRef().empty()) {
// NOLINTNEXTLINE
FC_ERR("External geometry reference corrupted in " << getFullName()
<< " Please check.");
// This could happen if someone saved the sketch containing
// external geometries using some rogue releases during the
// migration period. As a remedy, We re-initiate the
// external geometry here to trigger rebuild later, with
// call to rebuildExternalGeometry()
initExternalGeo();
return;
}
auto it = legacyMap.find(egf->getRef());
if (it == legacyMap.end() || egf->getRef() == externalGeoRef[it->second]) {
continue;
}
if (getDocument() && !getDocument()->isPerformingTransaction()) {
// FIXME: this is a bug. Find out when and why does this happen
//
// Amendment: maybe the original bug is because of not handling external geometry
// changes during undo/redo, which should be considered as normal. So warning
// only if not undo/redo.
//
// NOLINTNEXTLINE
FC_WARN("Update legacy external reference "
<< egf->getRef() << " -> " << externalGeoRef[it->second] << " in "
<< getFullName());
}
else {
// NOLINTNEXTLINE
FC_LOG("Update undo/redo external reference "
<< egf->getRef() << " -> " << externalGeoRef[it->second] << " in "
<< getFullName());
}
touched = true;
egf->setRef(externalGeoRef[it->second]);
}
if (touched) {
ExternalGeo.setValues(std::move(geos));
}
}
void SketchObject::onUndoRedoFinished()
{
// upon undo/redo, PropertyConstraintList does not have updated valid geometry keys, which
// results in empty constraint lists when using getValues
//
// The sketch will also have invalid vertex indices, requiring a call to rebuildVertexIndex
//
// Historically this was "solved" by issuing a recompute, which is absolutely unnecessary and
// prevents solve() from working before such a recompute in case it is redoing an operation with
// invalid data.
Constraints.checkConstraintIndices(getHighestCurveIndex(), -getExternalGeometryCount());
acceptGeometry();
synchroniseGeometryState();
solve();
}
void SketchObject::synchroniseGeometryState()
{
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
for (size_t i = 0; i < vals.size(); i++) {
auto gf = GeometryFacade::getFacade(vals[i]);
auto facadeInternalAlignment = gf->getInternalType();
auto facadeBlockedState = gf->getBlocked();
Sketcher::InternalType::InternalType constraintInternalAlignment = InternalType::None;
bool constraintBlockedState = false;
for (auto cstr : Constraints.getValues()) {
if (cstr->First == int(i)) {
getInternalTypeState(cstr, constraintInternalAlignment);
getBlockedState(cstr, constraintBlockedState);
}
}
if (constraintInternalAlignment != facadeInternalAlignment)
gf->setInternalType(constraintInternalAlignment);
if (constraintBlockedState != facadeBlockedState)
gf->setBlocked(constraintBlockedState);
}
}
bool SketchObject::getInternalTypeState(
const Constraint* cstr, Sketcher::InternalType::InternalType& internaltypestate) const
{
if (cstr->Type == InternalAlignment) {
switch (cstr->AlignmentType) {
case Undef:
case NumInternalAlignmentType:
internaltypestate = InternalType::None;
break;
case EllipseMajorDiameter:
internaltypestate = InternalType::EllipseMajorDiameter;
break;
case EllipseMinorDiameter:
internaltypestate = InternalType::EllipseMinorDiameter;
break;
case EllipseFocus1:
internaltypestate = InternalType::EllipseFocus1;
break;
case EllipseFocus2:
internaltypestate = InternalType::EllipseFocus2;
break;
case HyperbolaMajor:
internaltypestate = InternalType::HyperbolaMajor;
break;
case HyperbolaMinor:
internaltypestate = InternalType::HyperbolaMinor;
break;
case HyperbolaFocus:
internaltypestate = InternalType::HyperbolaFocus;
break;
case ParabolaFocus:
internaltypestate = InternalType::ParabolaFocus;
break;
case BSplineControlPoint:
internaltypestate = InternalType::BSplineControlPoint;
break;
case BSplineKnotPoint:
internaltypestate = InternalType::BSplineKnotPoint;
break;
case ParabolaFocalAxis:
internaltypestate = InternalType::ParabolaFocalAxis;
break;
}
return true;
}
return false;
}
bool SketchObject::getBlockedState(const Constraint* cstr, bool& blockedstate) const
{
if (cstr->Type == Block) {
blockedstate = true;
return true;
}
return false;
}
void SketchObject::onDocumentRestored()
{
try {
onSketchRestore();
Part::Part2DObject::onDocumentRestored();
}
catch (...) {
}
}
void SketchObject::restoreFinished()
{
App::DocumentObject::restoreFinished();
onSketchRestore();
}
void SketchObject::onSketchRestore()
{
try {
migrateSketch();
updateGeometryRefs();
if(ExternalGeo.getSize()<=2) {
if (ExternalGeo.getSize() < 2)
initExternalGeo();
for(auto &key : externalGeoRef) {
long id = getDocument()->getStringHasher()->getID(key.c_str()).value();
if(geoLastId < id)
geoLastId = id;
externalGeoRefMap[key].push_back(id);
}
rebuildExternalGeometry();
if(ExternalGeometry.getSize()+2!=ExternalGeo.getSize())
FC_WARN("Failed to restore some external geometry in " << getFullName());
}else
acceptGeometry();
synchroniseGeometryState();
// this may happen when saving a sketch directly in edit mode
// but never performed a recompute before
if (Shape.getValue().IsNull() && hasConflicts() == 0) {
if (this->solve(true) == 0)
Shape.setValue(solvedSketch.toShape());
}
// Sanity check on constraints with expression. It is added because the
// way SketchObject syncs expression and constraints heavily relies on
// proper setup of undo/redo transactions. The missing transaction in
// EditDatumDialog may cause stray or worse wrongly bound expressions.
for (auto &v : ExpressionEngine.getExpressions()) {
if (v.first.getProperty() != &Constraints)
continue;
const Constraint * cstr = nullptr;
try {
cstr = Constraints.getConstraint(v.first);
} catch (Base::Exception &) {
}
if (!cstr || !cstr->isDimensional()) {
FC_WARN((cstr ? "Invalid" : "Orphan")
<< " constraint expression in "
<< getFullName() << "."
<< v.first.toString()
<< ": " << v.second->toString());
ExpressionEngine.setValue(v.first, nullptr);
}
}
} catch (Base::Exception &e) {
e.reportException();
FC_ERR("Error while restoring " << getFullName());
} catch (...) {
}
}
// clang-format on
void SketchObject::migrateSketch()
{
const auto& allGeoms = getInternalGeometry();
bool noextensions = std::ranges::any_of(allGeoms, [](const auto& geo) {
return !geo->hasExtension(SketchGeometryExtension::getClassTypeId());
});
if (noextensions) {
for (const auto& c : Constraints.getValues()) {
addGeometryState(c);
// Convert B-Spline controlpoints radius/diameter constraints to Weight constraints
if (c->Type != InternalAlignment || c->AlignmentType != BSplineControlPoint) {
continue;
}
int circleGeoId = c->First;
int bSplineGeoId = c->Second;
auto bsp = static_cast<const Part::GeomBSplineCurve*>(getGeometry(bSplineGeoId));
std::vector<double> weights = bsp->getWeights();
if (!(c->InternalAlignmentIndex < int(weights.size()))) {
continue;
}
for (auto& ccp : Constraints.getValues()) {
if ((ccp->Type == Radius || ccp->Type == Diameter) && ccp->First == circleGeoId) {
ccp->Type = Weight;
ccp->setValue(weights[c->InternalAlignmentIndex]);
}
}
}
// Construction migration to extension
for (auto& g : Geometry.getValues()) {
if (!g->hasExtension(Part::GeometryMigrationExtension::getClassTypeId())) {
continue;
}
auto ext = std::static_pointer_cast<Part::GeometryMigrationExtension>(
g->getExtension(Part::GeometryMigrationExtension::getClassTypeId()).lock()
);
if (!ext->testMigrationType(Part::GeometryMigrationExtension::Construction)) {
continue;
}
// at this point IA geometry is already migrated
auto gf = GeometryFacade::getFacade(g);
bool oldConstr = ext->getConstruction()
|| (g->is<Part::GeomPoint>() && !gf->isInternalAligned());
GeometryFacade::setConstruction(g, oldConstr);
g->deleteExtension(Part::GeometryMigrationExtension::getClassTypeId());
}
}
/* parabola axis as internal geometry */
auto constraints = Constraints.getValues();
auto geometries = getInternalGeometry();
bool parabolaFound = std::ranges::any_of(geometries, &Part::Geometry::is<Part::GeomArcOfParabola>);
if (!parabolaFound) {
return;
}
auto focalAxisFound = std::ranges::any_of(constraints, [](auto c) {
return c->Type == InternalAlignment && c->AlignmentType == ParabolaFocalAxis;
});
if (focalAxisFound) {
return;
}
// There are parabolas and there isn't an IA axis. (1) there are no axis or (2) there is a
// legacy construction line
// maps parabola geoid to focusGeoId
std::map<int, int> parabolaGeoId2FocusGeoId;
// populate parabola and focus geoids
for (const auto& c : constraints) {
if (c->Type == InternalAlignment && c->AlignmentType == ParabolaFocus) {
parabolaGeoId2FocusGeoId[c->Second] = {c->First};
}
}
// maps axis geoid to parabolaGeoId
std::map<int, int> axisGeoId2ParabolaGeoId;
// populate axis geoid
for (const auto& [parabolaGeoId, focusGeoId] : parabolaGeoId2FocusGeoId) {
// look for a line from focusGeoId:start to Geoid:mid_external
std::vector<int> focusGeoIdListGeoIdList;
std::vector<PointPos> focusPosIdList;
getDirectlyCoincidentPoints(
focusGeoId,
Sketcher::PointPos::start,
focusGeoIdListGeoIdList,
focusPosIdList
);
std::vector<int> parabGeoIdListGeoIdList;
std::vector<PointPos> parabposidlist;
getDirectlyCoincidentPoints(
parabolaGeoId,
Sketcher::PointPos::mid,
parabGeoIdListGeoIdList,
parabposidlist
);
for (const auto& parabGeoIdListGeoId : parabGeoIdListGeoIdList) {
auto iterParabolaGeoId = std::ranges::find(focusGeoIdListGeoIdList, parabGeoIdListGeoId);
if (iterParabolaGeoId != focusGeoIdListGeoIdList.end()) {
axisGeoId2ParabolaGeoId[*iterParabolaGeoId] = parabolaGeoId;
}
}
}
std::vector<Constraint*> newConstraints;
newConstraints.reserve(constraints.size());
for (const auto& c : constraints) {
if (c->Type != Coincident) {
newConstraints.push_back(c);
continue;
}
auto axisMajorCoincidentFound
= std::ranges::any_of(axisGeoId2ParabolaGeoId, [&](const auto& pair) {
auto parabolaGeoId = pair.second;
auto axisgeoid = pair.first;
return (c->First == axisgeoid && c->Second == parabolaGeoId
&& c->SecondPos == PointPos::mid)
|| (c->Second == axisgeoid && c->First == parabolaGeoId
&& c->FirstPos == PointPos::mid);
});
if (axisMajorCoincidentFound) {
// we skip this coincident, the other coincident on axis will be substituted
// by internal geometry constraint
continue;
}
auto focusCoincidentFound = std::ranges::find_if(axisGeoId2ParabolaGeoId, [&](const auto& pair) {
auto parabolaGeoId = pair.second;
auto axisgeoid = pair.first;
auto focusGeoId = parabolaGeoId2FocusGeoId[parabolaGeoId];
return (c->First == axisgeoid && c->Second == focusGeoId
&& c->SecondPos == PointPos::start)
|| (c->Second == axisgeoid && c->First == focusGeoId
&& c->FirstPos == PointPos::start);
});
if (focusCoincidentFound != axisGeoId2ParabolaGeoId.end()) {
auto* newConstr = new Sketcher::Constraint();
newConstr->Type = Sketcher::InternalAlignment;
newConstr->AlignmentType = Sketcher::ParabolaFocalAxis;
newConstr->First = focusCoincidentFound->first; // axis geoid
newConstr->FirstPos = Sketcher::PointPos::none;
newConstr->Second = focusCoincidentFound->second; // parabola geoid
newConstr->SecondPos = Sketcher::PointPos::none;
newConstraints.push_back(newConstr);
addGeometryState(newConstr);
// we skip the coincident, as we have substituted it by internal geometry
// constraint
continue;
}
newConstraints.push_back(c);
}
Constraints.setValues(std::move(newConstraints));
Base::Console().critical(
this->getFullName(),
QT_TRANSLATE_NOOP(
"Notifications",
"Parabolas were migrated. Migrated files won't open in previous "
"versions of FreeCAD!!\n"
)
);
}
// clang-format off
void SketchObject::getGeoVertexIndex(int VertexId, int& GeoId, PointPos& PosId) const
{
if (VertexId < 0 || VertexId >= int(VertexId2GeoId.size())) {
GeoId = GeoEnum::GeoUndef;
PosId = PointPos::none;
return;
}
GeoId = VertexId2GeoId[VertexId];
PosId = VertexId2PosId[VertexId];
}
int SketchObject::getVertexIndexGeoPos(int GeoId, PointPos PosId) const
{
for (std::size_t i = 0; i < VertexId2GeoId.size(); i++) {
if (VertexId2GeoId[i] == GeoId && VertexId2PosId[i] == PosId)
return i;
}
return -1;
}
/// changeConstraintsLocking locks or unlocks all tangent and perpendicular
/// constraints. (Constraint locking prevents it from flipping to another valid
/// configuration, when e.g. external geometry is updated from outside.) The
/// sketch solve is not triggered by the function, but the SketchObject is
/// touched (a recompute will be necessary). The geometry should not be affected
/// by the function.
/// The bLock argument specifies, what to do. If true, all constraints are
/// unlocked and locked again. If false, all tangent and perp. constraints are
/// unlocked.
int SketchObject::changeConstraintsLocking(bool bLock)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
int cntSuccess = 0;
int cntToBeAffected = 0;//==cntSuccess+cntFail
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> newVals(vals);// modifiable copy of pointers array
for (size_t i = 0; i < newVals.size(); i++) {
if (newVals[i]->Type == Tangent || newVals[i]->Type == Perpendicular) {
// create a constraint copy, affect it, replace the pointer
cntToBeAffected++;
Constraint* constNew = newVals[i]->clone();
bool ret = AutoLockTangencyAndPerpty(newVals[i], /*bForce=*/true, bLock);
if (ret)
cntSuccess++;
newVals[i] = constNew;
Base::Console().log("Constraint%i will be affected\n", i + 1);
}
}
this->Constraints.setValues(std::move(newVals));
Base::Console().log("ChangeConstraintsLocking: affected %i of %i tangent/perp constraints\n",
cntSuccess,
cntToBeAffected);
return cntSuccess;
}
/*!
* \brief SketchObject::port_reversedExternalArcs finds constraints that link to endpoints of
* external-geometry arcs, and swaps the endpoints in the constraints. This is needed after CCW
* emulation was introduced, to port old sketches. \param justAnalyze if true, nothing is actually
* done - only the number of constraints to be affected is returned. \return the number of
* constraints changed/to be changed.
*/
int SketchObject::port_reversedExternalArcs(bool justAnalyze)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
int cntToBeAffected = 0;
const std::vector<Constraint*>& vals = this->Constraints.getValues();
std::vector<Constraint*> newVals(vals);// modifiable copy of pointers array
for (std::size_t ic = 0; ic < newVals.size(); ic++) {// ic = index of constraint
bool affected = false;
Constraint* constNew = nullptr;
for (int ig = 1; ig <= 3; ig++) {
// cycle through constraint.first, second, third
int geoId = 0;
Sketcher::PointPos posId = PointPos::none;
switch (ig) {
case 1:
geoId = newVals[ic]->First;
posId = newVals[ic]->FirstPos;
break;
case 2:
geoId = newVals[ic]->Second;
posId = newVals[ic]->SecondPos;
break;
case 3:
geoId = newVals[ic]->Third;
posId = newVals[ic]->ThirdPos;
break;
}
if (geoId <= GeoEnum::RefExt
&& (posId == Sketcher::PointPos::start || posId == Sketcher::PointPos::end)) {
// we are dealing with a link to an endpoint of external geom
Part::Geometry* g = this->ExternalGeo[-geoId - 1];
if (g->is<Part::GeomArcOfCircle>()) {
const auto* segm = static_cast<const Part::GeomArcOfCircle*>(g);
if (segm->isReversed()) {
// Gotcha! a link to an endpoint of external arc that is reversed.
// create a constraint copy, affect it, replace the pointer
if (!affected)
constNew = newVals[ic]->clone();
affected = true;
// Do the fix on temp vars
if (posId == Sketcher::PointPos::start)
posId = Sketcher::PointPos::end;
else if (posId == Sketcher::PointPos::end)
posId = Sketcher::PointPos::start;
}
}
}
if (!affected)
continue;
// Propagate the fix made on temp vars to the constraint
switch (ig) {
case 1:
constNew->First = geoId;
constNew->FirstPos = posId;
break;
case 2:
constNew->Second = geoId;
constNew->SecondPos = posId;
break;
case 3:
constNew->Third = geoId;
constNew->ThirdPos = posId;
break;
}
}
if (affected) {
cntToBeAffected++;
newVals[ic] = constNew;
Base::Console().log("Constraint%i will be affected\n", ic + 1);
};
}
if (!justAnalyze) {
this->Constraints.setValues(std::move(newVals));
Base::Console().log("Swapped start/end of reversed external arcs in %i constraints\n",
cntToBeAffected);
}
return cntToBeAffected;
}
/// Locks tangency/perpendicularity type of such a constraint.
/// The constraint passed must be writable (i.e. the one that is not
/// yet in the constraint list).
/// Tangency type (internal/external) is derived from current geometry
/// the constraint refers to.
/// Same for perpendicularity type.
///
/// This function catches exceptions, because it's not a reason to
/// not create a constraint if tangency/perp-ty type cannot be determined.
///
/// Arguments:
/// cstr - pointer to a constraint to be locked/unlocked
/// bForce - specifies whether to ignore the already locked constraint or not.
/// bLock - specifies whether to lock the constraint or not (if bForce is
/// true, the constraint gets unlocked, otherwise nothing is done at all).
///
/// Return values:
/// true - success.
/// false - fail (this indicates an error, or that a constraint locking isn't supported).
bool SketchObject::AutoLockTangencyAndPerpty(Constraint* cstr, bool bForce, bool bLock)
{
using std::numbers::pi;
try {
/*tangency type already set. If not bForce - don't touch.*/
if (cstr->getValue() != 0.0 && !bForce)
return true;
if (!bLock) {
cstr->setValue(0.0);// reset
}
else {
// decide on tangency type. Write the angle value into the datum field of the
// constraint.
int geoId1, geoId2, geoIdPt;
PointPos posPt;
geoId1 = cstr->First;
geoId2 = cstr->Second;
geoIdPt = cstr->Third;
posPt = cstr->ThirdPos;
if (geoIdPt == GeoEnum::GeoUndef) {// not tangent-via-point, try endpoint-to-endpoint...
// First check if it is a tangency at knot constraint, if not continue with checking
// for endpoints. Endpoint constraints make use of the AngleViaPoint framework at
// solver level, so they need locking angle calculation, tangency at knot constraint
// does not.
auto geof = getGeometryFacade(cstr->First);
if (geof->isInternalType(InternalType::BSplineKnotPoint)) {
// there is point that is a B-Spline knot in a two element constraint
// this is not implement using AngleViaPoint (TangencyViaPoint)
return false;
}
geoIdPt = cstr->First;
posPt = cstr->FirstPos;
}
if (posPt == PointPos::none) {
// not endpoint-to-curve and not endpoint-to-endpoint tangent (is simple tangency)
// no tangency lockdown is implemented for simple tangency. Do nothing.
return false;
}
Base::Vector3d p = getPoint(geoIdPt, posPt);
// this piece of code is also present in Sketch.cpp, correct for offset
// and to do the autodecision for old sketches.
// the difference between the datum value and the actual angle to apply.
// (datum=angle+offset)
double angleOffset = 0.0;
// the desired angle value (and we are to decide if 180* should be added to it)
double angleDesire = 0.0;
if (cstr->Type == Tangent) {
angleOffset = -pi / 2;
angleDesire = 0.0;
}
if (cstr->Type == Perpendicular) {
angleOffset = 0;
angleDesire = pi / 2;
}
double angleErr = calculateAngleViaPoint(geoId1, geoId2, p.x, p.y) - angleDesire;
// bring angleErr to -pi..pi
if (angleErr > pi)
angleErr -= pi * 2;
if (angleErr < -pi)
angleErr += pi * 2;
// the autodetector
if (fabs(angleErr) > pi / 2)
angleDesire += pi;
// external tangency. The angle stored is offset by Pi/2 so that a value of 0.0 is
// invalid and treated as "undecided".
cstr->setValue(angleDesire + angleOffset);
}
}
catch (Base::Exception& e) {
// failure to determine tangency type is not a big deal, so a warning.
Base::Console().warning("Error in AutoLockTangency. %s \n", e.what());
return false;
}
return true;
}
App::DocumentObject *SketchObject::getSubObject(
const char *subname, PyObject **pyObj,
Base::Matrix4D *pmat, bool transform, int depth) const
{
while(subname && *subname=='.') ++subname; // skip leading .
std::string sub;
const char *mapped = Data::isMappedElement(subname);
if(!subname || !subname[0]) {
return Part2DObject::getSubObject(subname,pyObj,pmat,transform,depth);
}
const char *element = Data::findElementName(subname);
if(element != subname) {
const char *dot = strchr(subname,'.');
if(!dot)
return 0;
std::string name(subname,dot-subname);
auto child = Exports.find(name.c_str());
if(!child)
return 0;
return child->getSubObject(dot+1,pyObj,pmat,true,depth+1);
}
Data::IndexedName indexedName = checkSubName(subname);
int index = indexedName.getIndex();
const char * shapetype = indexedName.getType();
const Part::Geometry *geo = 0;
Part::TopoShape subshape;
Base::Vector3d point;
if (auto realType = convertInternalName(indexedName.getType())) {
if (realType[0] == '\0')
subshape = InternalShape.getShape();
else {
auto shapeType = Part::TopoShape::shapeType(realType, true);
if (shapeType != TopAbs_SHAPE)
subshape = InternalShape.getShape().getSubTopoShape(shapeType, indexedName.getIndex(), true);
}
if (subshape.isNull())
return nullptr;
}
else if (!pyObj || !mapped) {
if (!pyObj
|| (index > 0
&& !boost::algorithm::contains(subname, "edge")
&& !boost::algorithm::contains(subname, "vertex")))
return Part2DObject::getSubObject(subname,pyObj,pmat,transform,depth);
}
else {
subshape = Shape.getShape().getSubTopoShape(subname, true);
if (!subshape.isNull())
return Part2DObject::getSubObject(subname,pyObj,pmat,transform,depth);
}
if (subshape.isNull()) {
if (boost::equals(shapetype,"Edge") ||
boost::equals(shapetype,"edge")) {
geo = getGeometry(index - 1);
if (!geo)
return nullptr;
}
else if (boost::equals(shapetype,"ExternalEdge")) {
int GeoId = index - 1;
GeoId = -GeoId - 3;
geo = getGeometry(GeoId);
if(!geo)
return nullptr;
}
else if (boost::equals(shapetype,"Vertex") ||
boost::equals(shapetype,"vertex")) {
int VtId = index- 1;
int GeoId;
PointPos PosId;
getGeoVertexIndex(VtId,GeoId,PosId);
if (PosId==PointPos::none)
return nullptr;
point = getPoint(GeoId,PosId);
}
else if (boost::equals(shapetype,"RootPoint"))
point = getPoint(Sketcher::GeoEnum::RtPnt,PointPos::start);
else if (boost::equals(shapetype,"H_Axis"))
geo = getGeometry(Sketcher::GeoEnum::HAxis);
else if (boost::equals(shapetype,"V_Axis"))
geo = getGeometry(Sketcher::GeoEnum::VAxis);
else if (boost::equals(shapetype,"Constraint")) {
int ConstrId = PropertyConstraintList::getIndexFromConstraintName(shapetype);
const std::vector< Constraint * > &vals = this->Constraints.getValues();
if (ConstrId < 0 || ConstrId >= int(vals.size()))
return nullptr;
if(pyObj)
*pyObj = vals[ConstrId]->getPyObject();
return const_cast<SketchObject*>(this);
}
else {
return nullptr;
}
}
if (pmat && transform)
*pmat *= Placement.getValue().toMatrix();
if (!pyObj) {
return const_cast<SketchObject*>(this);
}
// pyObj exists from here
Part::TopoShape shape;
std::string name = convertSubName(indexedName,false);
if (geo) {
shape = getEdge(geo,name.c_str());
if(pmat && !shape.isNull()) {
shape.transformShape(*pmat,false,true);
}
}
else if (!subshape.isNull()) {
shape = subshape;
if (pmat) {
shape.transformShape(*pmat,false,true);
}
}
else {
if(pmat) {
point = (*pmat)*point;
}
shape = BRepBuilderAPI_MakeVertex(gp_Pnt(point.x,point.y,point.z)).Vertex();
// Originally in ComplexGeoData::setElementName
// LinkStable/src/App/ComplexGeoData.cpp#L1631
// No longer possible after map separated in ElementMap.cpp
if (!shape.hasElementMap()) {
shape.resetElementMap(std::make_shared<Data::ElementMap>());
}
shape.setElementName(Data::IndexedName::fromConst("Vertex", 1),
Data::MappedName::fromRawData(name.c_str()),0);
}
shape.Tag = getID();
*pyObj = Py::new_reference_to(Part::shape2pyshape(shape));
return const_cast<SketchObject*>(this);
}
std::vector<Data::IndexedName>
SketchObject::getHigherElements(const char *element, bool silent) const
{
std::vector<Data::IndexedName> res;
if (boost::istarts_with(element, "vertex")) {
int n = 0;
int index = atoi(element+6);
for (auto cstr : Constraints.getValues()) {
++n;
if (cstr->Type != Sketcher::Coincident)
continue;
if(cstr->First >= 0 && index == getSolvedSketch().getPointId(cstr->First, cstr->FirstPos) + 1)
res.push_back(Data::IndexedName::fromConst("Constraint", n));
if(cstr->Second >= 0 && index == getSolvedSketch().getPointId(cstr->Second, cstr->SecondPos) + 1)
res.push_back(Data::IndexedName::fromConst("Constraint", n));
}
}
return res;
auto getNames = [this, &silent, &res](const char *element) {
bool internal = boost::starts_with(element, internalPrefix());
const auto &shape = internal ? InternalShape.getShape() : Shape.getShape();
for (const auto &indexedName : shape.getHigherElements(element+(internal?internalPrefix().size() : 0), silent)) {
if (!internal) {
res.push_back(indexedName);
}
else if (boost::equals(indexedName.getType(), "Face")
|| boost::equals(indexedName.getType(), "Edge")
|| boost::equals(indexedName.getType(), "Wire")) {
res.emplace_back((internalPrefix() + indexedName.getType()).c_str(), indexedName.getIndex());
}
}
};
getNames(element);
const auto &elementMap = getInternalElementMap();
auto it = elementMap.find(element);
if (it != elementMap.end()) {
res.emplace_back(it->second.c_str());
getNames(it->second.c_str());
}
return res;
}
std::vector<const char *> SketchObject::getElementTypes(bool all) const
{
if (!all)
return Part::Part2DObject::getElementTypes();
static std::vector<const char *> res { Part::TopoShape::shapeName(TopAbs_VERTEX).c_str(),
Part::TopoShape::shapeName(TopAbs_EDGE).c_str(),
"ExternalEdge",
"Constraint",
"InternalEdge",
"InternalFace",
"InternalVertex",
};
return res;
}
void SketchObject::setExpression(const App::ObjectIdentifier& path,
std::shared_ptr<App::Expression> expr)
{
DocumentObject::setExpression(path, std::move(expr));
if (noRecomputes) {
// if we do not have a recompute, the sketch must be solved to update the DoF of the solver,
// constraints and UI
try {
auto res = ExpressionEngine.execute();
if (res) {
FC_ERR("Failed to recompute " << ExpressionEngine.getFullName() << ": "
<< res->Why);
delete res;
}
}
catch (Base::Exception& e) {
e.reportException();
FC_ERR("Failed to recompute " << ExpressionEngine.getFullName() << ": " << e.what());
}
solve();
}
}
const std::string &SketchObject::internalPrefix()
{
static std::string _prefix("Internal");
return _prefix;
}
const char *SketchObject::convertInternalName(const char *name)
{
if (name && boost::starts_with(name, internalPrefix()))
return name + internalPrefix().size();
return nullptr;
}
App::ElementNamePair SketchObject::getElementName(
const char *name, ElementNameType type) const
{
App::ElementNamePair ret;
if(!name) return ret;
if(hasSketchMarker(name))
return Part2DObject::getElementName(name,type);
const char *mapped = Data::isMappedElement(name);
Data::IndexedName index = checkSubName(name);
index.appendToStringBuffer(ret.oldName);
if (auto realName = convertInternalName(ret.oldName.c_str())) {
Data::MappedElement mappedElement;
if (mapped)
mappedElement = InternalShape.getShape().getElementName(name);
else if (type == ElementNameType::Export)
ret.newName = getExportElementName(InternalShape.getShape(), realName).newName;
else
mappedElement = InternalShape.getShape().getElementName(realName);
if (mapped || type != ElementNameType::Export) {
if (mappedElement.index) {
ret.oldName = internalPrefix();
mappedElement.index.appendToStringBuffer(ret.oldName);
}
if (mappedElement.name) {
ret.newName = Data::ComplexGeoData::elementMapPrefix();
mappedElement.name.appendToBuffer(ret.newName);
}
else if (mapped)
ret.newName = name;
}
if (ret.newName.size()) {
if (auto dot = strrchr(ret.newName.c_str(), '.'))
ret.newName.resize(dot+1-ret.newName.c_str());
else
ret.newName += ".";
ret.newName += ret.oldName;
}
if (mapped && (!mappedElement.index || !mappedElement.name))
ret.oldName.insert(0, Data::MISSING_PREFIX);
return ret;
}
if(!mapped) {
auto occindex = Part::TopoShape::shapeTypeAndIndex(name);
if (occindex.second)
return Part2DObject::getElementName(name,type);
}
if(index && type==ElementNameType::Export) {
if(boost::starts_with(ret.oldName,"Vertex"))
ret.oldName[0] = 'v';
else if(boost::starts_with(ret.oldName,"Edge"))
ret.oldName[0] = 'e';
}
ret.newName = convertSubName(index, true);
if(!Data::isMappedElement(ret.newName.c_str()))
ret.newName.clear();
return ret;
}
Part::TopoShape SketchObject::getEdge(const Part::Geometry *geo, const char *name) const
{
Part::TopoShape shape(geo->toShape());
// Originally in ComplexGeoData::setElementName
// LinkStable/src/App/ComplexGeoData.cpp#L1631
// No longer possible after map separated in ElementMap.cpp
if ( !shape.hasElementMap() ) {
shape.resetElementMap(std::make_shared<Data::ElementMap>());
}
shape.setElementName(Data::IndexedName::fromConst("Edge", 1),
Data::MappedName::fromRawData(name),0L);
TopTools_IndexedMapOfShape vmap;
TopExp::MapShapes(shape.getShape(), TopAbs_VERTEX, vmap);
std::ostringstream ss;
for(int i=1;i<=vmap.Extent();++i) {
auto gpt = BRep_Tool::Pnt(TopoDS::Vertex(vmap(i)));
Base::Vector3d pt(gpt.X(),gpt.Y(),gpt.Z());
PointPos pos[] = {PointPos::start,PointPos::end};
for(size_t j=0;j<sizeof(pos)/sizeof(pos[0]);++j) {
if(getPoint(geo,pos[j]) == pt) {
ss.str("");
ss << name << 'v' << static_cast<int>(pos[j]);
shape.setElementName(Data::IndexedName::fromConst("Vertex", i),
Data::MappedName::fromRawData(ss.str().c_str()),0L);
break;
}
}
}
return shape;
}
Data::IndexedName SketchObject::checkSubName(const char *subname) const
{
static std::vector<const char *> types = {
"Edge",
"Vertex",
"edge",
"vertex",
"ExternalEdge",
"RootPoint",
"H_Axis",
"V_Axis",
"Constraint",
// other feature from LS3 not related to TNP
"InternalEdge",
"InternalFace",
"InternalVertex",
};
if(!subname) return Data::IndexedName();
const char *mappedSubname = Data::isMappedElement(subname);
// if not a mapped name parse the indexed name directly, uppercasing "edge" and "vertex"
if(!mappedSubname) {
Data::IndexedName result(subname, types, true);
if (boost::equals(result.getType(), "edge"))
return Data::IndexedName("Edge", result.getIndex());
if (boost::equals(result.getType(), "vertex"))
return Data::IndexedName("Vertex", result.getIndex());
return result;
}
bio::stream<bio::array_source> iss(mappedSubname+1, std::strlen(mappedSubname+1));
int id = -1;
bool valid = false;
switch (mappedSubname[0]) {
case '\0': // check length != 0
break;
case 'g': // = geometry
case 'e': // = external geometry
if (iss >> id) {
valid = true;
}
break;
// for RootPoint, H_Axis, V_Axis
default: {
const char* dot = strchr(mappedSubname, '.');
if (dot) {
mappedSubname = dot + 1;
}
return Data::IndexedName(mappedSubname, types, false);
}
}
if (!valid) {
FC_ERR("invalid subname " << subname);
return Data::IndexedName();
}
int geoId;
const Part::Geometry* geo = 0;
switch (mappedSubname[0]) {
case 'g': {
auto it = geoMap.find(id);
if (it != geoMap.end()) {
geoId = it->second;
geo = getGeometry(geoId);
}
break;
}
case 'e': {
auto it = externalGeoMap.find(id);
if (it != externalGeoMap.end()) {
geoId = -it->second - 1;
geo = getGeometry(geoId);
}
break;
}
}
if (geo && GeometryFacade::getId(geo) == id) {
char sep;
int posId = static_cast<int>(PointPos::none);
if ((iss >> sep >> posId) && sep == 'v') {
int idx = getVertexIndexGeoPos(geoId, static_cast<PointPos>(posId));
// Outside edit-mode circles exposes the seam point but not the center, while in edit-mode we expose the center but not the seam.
// getVertexIndexGeoPos searching for a circle start point (g1v1 for example) (which happens outside of edit mode) will fail.
// see https://github.com/FreeCAD/FreeCAD/issues/25089
// The following fix works because circles have always 1 vertex, whether in or out of edit mode.
if (idx < 0 && (static_cast<PointPos>(posId) == PointPos::start || static_cast<PointPos>(posId) == PointPos::end)) {
if (geo->is<Part::GeomCircle>() || geo->is<Part::GeomEllipse>()) {
idx = getVertexIndexGeoPos(geoId, PointPos::mid);
}
}
if (idx < 0) {
FC_ERR("invalid subname " << subname);
return Data::IndexedName();
}
return Data::IndexedName::fromConst("Vertex", idx + 1);
}
else if (geoId >= 0) {
return Data::IndexedName::fromConst("Edge", geoId + 1);
}
else {
return Data::IndexedName::fromConst("ExternalEdge", -geoId - 2);
}
}
FC_ERR("cannot find subname " << subname);
return Data::IndexedName();
}
Data::IndexedName SketchObject::shapeTypeFromGeoId(int geoId, PointPos posId) const
{
if (geoId == GeoEnum::HAxis) {
if (posId == PointPos::start) {
return Data::IndexedName::fromConst("RootPoint", 0);
}
return Data::IndexedName::fromConst("H_Axis", 0);
}
if (geoId == GeoEnum::VAxis) {
return Data::IndexedName::fromConst("V_Axis", 0);
}
if (posId == PointPos::none) {
auto geo = getGeometry(geoId);
if (geo && geo->isDerivedFrom<Part::GeomPoint>()) {
posId = PointPos::start;
}
}
if(posId != PointPos::none) {
int idx = getVertexIndexGeoPos(geoId, posId);
if (idx < 0) {
return Data::IndexedName();
}
return Data::IndexedName::fromConst("Vertex", idx + 1);
}
if (geoId >= 0) {
return Data::IndexedName::fromConst("Edge", geoId + 1);
}
return Data::IndexedName::fromConst("ExternalEdge", -geoId - 2);
}
bool SketchObject::geoIdFromShapeType(const Data::IndexedName & indexedName,
int &geoId,
PointPos &posId) const
{
posId = PointPos::none;
geoId = Sketcher::GeoEnum::GeoUndef;
if (!indexedName)
return false;
const char *shapetype = indexedName.getType();
if (boost::equals(shapetype,"Edge") ||
boost::equals(shapetype,"edge")) {
geoId = indexedName.getIndex() - 1;
} else if (boost::equals(shapetype,"ExternalEdge")) {
geoId = indexedName.getIndex() - 1;
geoId = Sketcher::GeoEnum::RefExt - geoId;
} else if (boost::equals(shapetype,"Vertex") ||
boost::equals(shapetype,"vertex")) {
int VtId = indexedName.getIndex() - 1;
getGeoVertexIndex(VtId,geoId,posId);
if (posId==PointPos::none) return false;
} else if (boost::equals(shapetype,"H_Axis")) {
geoId = Sketcher::GeoEnum::HAxis;
} else if (boost::equals(shapetype,"V_Axis")) {
geoId = Sketcher::GeoEnum::VAxis;
} else if (boost::equals(shapetype,"RootPoint")) {
geoId = Sketcher::GeoEnum::RtPnt;
posId = PointPos::start;
} else
return false;
return true;
}
std::string SketchObject::convertSubName(const char *subname, bool postfix) const {
return convertSubName(checkSubName(subname), postfix);
}
std::string SketchObject::convertSubName(const Data::IndexedName &indexedName, bool postfix) const {
std::ostringstream ss;
if (auto realType = convertInternalName(indexedName.getType())) {
auto mapped = InternalShape.getShape().getMappedName(
Data::IndexedName::fromConst(realType, indexedName.getIndex()));
if (!mapped) {
if (postfix)
ss << indexedName;
} else if (postfix)
ss << Data::ComplexGeoData::elementMapPrefix() << mapped << '.' << indexedName;
else
ss << mapped;
return ss.str();
}
int geoId;
PointPos posId;
if (!geoIdFromShapeType(indexedName, geoId, posId)) {
ss << indexedName;
return ss.str();
}
if (geoId == Sketcher::GeoEnum::HAxis ||
geoId == Sketcher::GeoEnum::VAxis ||
geoId == Sketcher::GeoEnum::RtPnt) {
if (postfix)
ss << Data::ELEMENT_MAP_PREFIX;
ss << indexedName;
if (postfix)
ss << '.' << indexedName;
return ss.str();
}
auto geo = getGeometry(geoId);
if (!geo) {
std::string res = indexedName.toString();
return res;
}
if (postfix)
ss << Data::ELEMENT_MAP_PREFIX;
ss << (geoId >= 0 ? 'g' : 'e') << GeometryFacade::getFacade(geo)->getId();
if (posId != PointPos::none)
ss << 'v' << static_cast<int>(posId);
if (postfix) {
// rename Edge to edge, and Vertex to vertex to avoid ambiguous of
// element mapping of the public shape and internal geometry.
if (indexedName.getIndex() <= 0)
ss << '.' << indexedName;
else if (boost::starts_with(indexedName.getType(), "Edge"))
ss << ".e" << (indexedName.getType() + 1) << indexedName.getIndex();
else if (boost::starts_with(indexedName.getType(), "Vertex"))
ss << ".v" << (indexedName.getType() + 1) << indexedName.getIndex();
else
ss << '.' << indexedName;
}
return ss.str();
}
std::string SketchObject::getGeometryReference(int GeoId) const {
auto geo = getGeometry(GeoId);
if (!geo) {
return {};
}
auto egf = ExternalGeometryFacade::getFacade(geo);
if (egf->getRef().empty()) {
return {};
}
const std::string &ref = egf->getRef();
if (egf->testFlag(ExternalGeometryExtension::Missing)) {
return std::string("? ") + ref;
}
auto pos = ref.find('.');
if (pos == std::string::npos) {
return ref;
}
std::string objName = ref.substr(0, pos);
auto obj = getDocument()->getObject(objName.c_str());
if (!obj) {
return ref;
}
App::ElementNamePair elementName;
App::GeoFeature::resolveElement(obj, ref.c_str() + pos + 1, elementName);
if (!elementName.oldName.empty()) {
return objName + "." + elementName.oldName;
}
return ref;
}
int SketchObject::autoConstraint(double precision, double angleprecision, bool includeconstruction)
{
return analyser->autoconstraint(precision, angleprecision, includeconstruction);
}
int SketchObject::detectMissingPointOnPointConstraints(double precision, bool includeconstruction)
{
return analyser->detectMissingPointOnPointConstraints(precision, includeconstruction);
}
void SketchObject::analyseMissingPointOnPointCoincident(double angleprecision)
{
analyser->analyseMissingPointOnPointCoincident(angleprecision);
}
int SketchObject::detectMissingVerticalHorizontalConstraints(double angleprecision)
{
return analyser->detectMissingVerticalHorizontalConstraints(angleprecision);
}
int SketchObject::detectMissingEqualityConstraints(double precision)
{
return analyser->detectMissingEqualityConstraints(precision);
}
std::vector<ConstraintIds>& SketchObject::getMissingPointOnPointConstraints()
{
return analyser->getMissingPointOnPointConstraints();
}
std::vector<ConstraintIds>& SketchObject::getMissingVerticalHorizontalConstraints()
{
return analyser->getMissingVerticalHorizontalConstraints();
}
std::vector<ConstraintIds>& SketchObject::getMissingLineEqualityConstraints()
{
return analyser->getMissingLineEqualityConstraints();
}
std::vector<ConstraintIds>& SketchObject::getMissingRadiusConstraints()
{
return analyser->getMissingRadiusConstraints();
}
void SketchObject::setMissingRadiusConstraints(std::vector<ConstraintIds>& cl)
{
if (analyser)
analyser->setMissingRadiusConstraints(cl);
}
void SketchObject::setMissingLineEqualityConstraints(std::vector<ConstraintIds>& cl)
{
if (analyser)
analyser->setMissingLineEqualityConstraints(cl);
}
void SketchObject::setMissingVerticalHorizontalConstraints(std::vector<ConstraintIds>& cl)
{
if (analyser)
analyser->setMissingVerticalHorizontalConstraints(cl);
}
void SketchObject::setMissingPointOnPointConstraints(std::vector<ConstraintIds>& cl)
{
if (analyser)
analyser->setMissingPointOnPointConstraints(cl);
}
void SketchObject::makeMissingPointOnPointCoincident(bool onebyone)
{
if (analyser) {
onebyone ? analyser->makeMissingPointOnPointCoincidentOneByOne()
: analyser->makeMissingPointOnPointCoincident();
}
}
void SketchObject::makeMissingVerticalHorizontal(bool onebyone)
{
if (analyser) {
onebyone ? analyser->makeMissingVerticalHorizontalOneByOne()
: analyser->makeMissingVerticalHorizontal();
}
}
void SketchObject::makeMissingEquality(bool onebyone)
{
if (analyser) {
onebyone ? analyser->makeMissingEqualityOneByOne()
: analyser->makeMissingEquality();
}
}
int SketchObject::detectDegeneratedGeometries(double tolerance)
{
return analyser->detectDegeneratedGeometries(tolerance);
}
int SketchObject::removeDegeneratedGeometries(double tolerance)
{
return analyser->removeDegeneratedGeometries(tolerance);
}
int SketchObject::autoRemoveRedundants(DeleteOptions options)
{
auto redundants = getLastRedundant();
if (redundants.empty()) {
return 0;
}
// getLastRedundant is base 1, while delConstraints is base 0
for (size_t i = 0; i < redundants.size(); i++)
redundants[i]--;
delConstraints(redundants, options);
return redundants.size();
}
int SketchObject::renameConstraint(int GeoId, std::string name)
{
// only change the constraint item if the names are different
const Constraint* item = Constraints[GeoId];
if (item->Name != name) {
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
Constraint* copy = item->clone();
copy->Name = std::move(name);
Constraints.set1Value(GeoId, copy);
delete copy;
// make sure any prospective solver access updates the constraint pointer that just got
// invalidated
solverNeedsUpdate = true;
return 0;
}
return -1;
}
std::vector<Base::Vector3d> SketchObject::getOpenVertices() const
{
std::vector<Base::Vector3d> points;
if (analyser)
points = analyser->getOpenVertices();
return points;
}
// SketchGeometryExtension interface
size_t setGeometryIdHelper(int GeoId, long id, std::vector<Part::Geometry*>& newVals, size_t searchOffset = 0, bool returnOnMatch = false)
{
// deep copy
for (size_t i = searchOffset; i < newVals.size(); i++) {
newVals[i] = newVals[i]->clone();
if ((int)i == GeoId) {
auto gf = GeometryFacade::getFacade(newVals[i]);
gf->setId(id);
if (returnOnMatch) {
return i;
}
}
}
return 0;
}
int SketchObject::setGeometryId(int GeoId, long id)
{
// no need to check input data validity as this is an sketchobject managed operation.
Base::StateLocker lock(managedoperation, true);
if (GeoId < 0 || GeoId >= int(Geometry.getValues().size()))
return -1;
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
std::vector<Part::Geometry*> newVals(vals);
setGeometryIdHelper(GeoId, id, newVals);
// There is not actual internal transaction going on here, however neither the geometry indices
// nor the vertices need to be updated so this is a convenient way of preventing it.
{
Base::StateLocker preventUpdate(internaltransaction, true);
this->Geometry.setValues(std::move(newVals));
}
return 0;
}
int SketchObject::setGeometryIds(std::vector<std::pair<int, long>> GeoIdsToIds)
{
Base::StateLocker lock(managedoperation, true);
std::sort(GeoIdsToIds.begin(), GeoIdsToIds.end());
size_t searchOffset = 0;
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
std::vector<Part::Geometry*> newVals(vals);
for (size_t i = 0; i < GeoIdsToIds.size(); ++i) {
int GeoId = GeoIdsToIds[i].first;
long id = GeoIdsToIds[i].second;
searchOffset = setGeometryIdHelper(GeoId, id, newVals, searchOffset, i != GeoIdsToIds.size()-1);
}
// There is not actual internal transaction going on here, however neither the geometry indices
// nor the vertices need to be updated so this is a convenient way of preventing it.
{
Base::StateLocker preventUpdate(internaltransaction, true);
this->Geometry.setValues(std::move(newVals));
}
return 0;
}
int SketchObject::getGeometryId(int GeoId, long& id) const
{
if (GeoId < 0 || GeoId >= int(Geometry.getValues().size()))
return -1;
const std::vector<Part::Geometry*>& vals = getInternalGeometry();
auto gf = GeometryFacade::getFacade(vals[GeoId]);
id = gf->getId();
return 0;
}
// Python Sketcher feature ---------------------------------------------------------
namespace App
{
/// @cond DOXERR
PROPERTY_SOURCE_TEMPLATE(Sketcher::SketchObjectPython, Sketcher::SketchObject)
template<>
const char* Sketcher::SketchObjectPython::getViewProviderName() const
{
return "SketcherGui::ViewProviderPython";
}
template<>
PyObject* Sketcher::SketchObjectPython::getPyObject()
{
if (PythonObject.is(Py::_None())) {
// ref counter is set to 1
PythonObject = Py::Object(new FeaturePythonPyT<SketchObjectPy>(this), true);
}
return Py::new_reference_to(PythonObject);
}
/// @endcond
// explicit template instantiation
template class SketcherExport FeaturePythonT<Sketcher::SketchObject>;
}// namespace App
// clang-format on