From fdf0013dcb23e8abbf0b6a8c60f3ddd126ed05e6 Mon Sep 17 00:00:00 2001 From: paddle Date: Tue, 17 Jun 2025 15:43:02 +0200 Subject: [PATCH 1/8] Sketcher: Text tool --- src/Mod/Part/App/Geometry.cpp | 387 +++++++++ src/Mod/Part/App/Geometry.h | 30 + src/Mod/Sketcher/App/Constraint.cpp | 208 ++++- src/Mod/Sketcher/App/Constraint.h | 22 +- src/Mod/Sketcher/App/ConstraintPyImp.cpp | 101 +++ src/Mod/Sketcher/App/Sketch.cpp | 177 ++++- src/Mod/Sketcher/App/Sketch.h | 34 +- src/Mod/Sketcher/App/SketchObject.cpp | 178 ++++- src/Mod/Sketcher/App/SketchObject.h | 17 + src/Mod/Sketcher/App/SketchObject.pyi | 13 + src/Mod/Sketcher/App/SketchObjectPyImp.cpp | 38 + src/Mod/Sketcher/Gui/CMakeLists.txt | 5 + src/Mod/Sketcher/Gui/CommandAlterGeometry.cpp | 1 + src/Mod/Sketcher/Gui/CommandConstraints.cpp | 248 ++++++ src/Mod/Sketcher/Gui/CommandConstraints.h | 12 + src/Mod/Sketcher/Gui/CommandCreateGeo.cpp | 39 + src/Mod/Sketcher/Gui/ConstraintFilters.h | 36 +- .../Gui/DrawSketchDefaultWidgetController.h | 81 +- src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h | 3 +- .../Sketcher/Gui/DrawSketchHandlerArcSlot.h | 1 + .../Sketcher/Gui/DrawSketchHandlerBSpline.h | 1 + .../Sketcher/Gui/DrawSketchHandlerCircle.h | 1 + .../Sketcher/Gui/DrawSketchHandlerEllipse.h | 1 + .../Sketcher/Gui/DrawSketchHandlerFillet.h | 1 + src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h | 1 + .../Sketcher/Gui/DrawSketchHandlerLineSet.h | 1 + .../Sketcher/Gui/DrawSketchHandlerOffset.h | 1 + .../Sketcher/Gui/DrawSketchHandlerPolygon.h | 3 +- .../Sketcher/Gui/DrawSketchHandlerRectangle.h | 1 + .../Sketcher/Gui/DrawSketchHandlerRotate.h | 3 +- src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h | 3 +- src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h | 9 +- .../Sketcher/Gui/DrawSketchHandlerSymmetry.h | 9 +- src/Mod/Sketcher/Gui/DrawSketchHandlerText.h | 750 ++++++++++++++++++ .../Sketcher/Gui/DrawSketchHandlerTranslate.h | 9 +- src/Mod/Sketcher/Gui/EditDatumDialog.cpp | 8 + .../Gui/EditModeConstraintCoinManager.cpp | 109 +++ .../Gui/EditModeGeometryCoinConverter.cpp | 166 ++-- .../Gui/EditModeGeometryCoinConverter.h | 6 +- .../Gui/EditModeGeometryCoinManager.cpp | 13 +- ...ditModeInformationOverlayCoinConverter.cpp | 13 + src/Mod/Sketcher/Gui/EditTextDialog.cpp | 108 +++ src/Mod/Sketcher/Gui/EditTextDialog.h | 71 ++ src/Mod/Sketcher/Gui/EditTextDialog.ui | 88 ++ src/Mod/Sketcher/Gui/Resources/Sketcher.qrc | 3 + .../icons/constraints/Constraint_Group.svg | 443 +++++++++++ .../icons/constraints/Constraint_Text.svg | 293 +++++++ .../icons/geometry/Sketcher_CreateText.svg | 113 +-- .../geometry/Sketcher_CreateText_Constr.svg | 254 ++++++ .../Gui/SketcherToolDefaultWidget.cpp | 132 ++- .../Sketcher/Gui/SketcherToolDefaultWidget.h | 36 +- .../Sketcher/Gui/SketcherToolDefaultWidget.ui | 28 + .../Sketcher/Gui/TaskSketcherConstraints.cpp | 20 + src/Mod/Sketcher/Gui/TaskSketcherElements.cpp | 225 ++++-- src/Mod/Sketcher/Gui/TaskSketcherElements.h | 4 +- src/Mod/Sketcher/Gui/Utils.cpp | 40 + src/Mod/Sketcher/Gui/Utils.h | 4 + src/Mod/Sketcher/Gui/ViewProviderSketch.cpp | 44 +- src/Mod/Sketcher/Gui/Workbench.cpp | 7 +- 59 files changed, 4324 insertions(+), 329 deletions(-) create mode 100644 src/Mod/Sketcher/Gui/DrawSketchHandlerText.h create mode 100644 src/Mod/Sketcher/Gui/EditTextDialog.cpp create mode 100644 src/Mod/Sketcher/Gui/EditTextDialog.h create mode 100644 src/Mod/Sketcher/Gui/EditTextDialog.ui create mode 100644 src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Group.svg create mode 100644 src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Text.svg create mode 100644 src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreateText_Constr.svg diff --git a/src/Mod/Part/App/Geometry.cpp b/src/Mod/Part/App/Geometry.cpp index b93a11b2d3..77d40a8a19 100644 --- a/src/Mod/Part/App/Geometry.cpp +++ b/src/Mod/Part/App/Geometry.cpp @@ -28,6 +28,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -37,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +67,8 @@ #include #include #include +#include +#include #include #include #include @@ -86,6 +92,8 @@ #include #include #include +#include +#include #include #include #include @@ -96,6 +104,7 @@ #include #include #include +#include #if OCC_VERSION_HEX < 0x070600 # include @@ -105,11 +114,31 @@ #include #include #include +#include +#include #include +#include +#include +// FreeType Headers +#include +#include FT_FREETYPE_H +#include FT_GLYPH_H +#include FT_OUTLINE_H +#include +#include + +// headers to scale text correctly +#include +#include + +#include +#include #include #include +#include #include +#include #include #include #include @@ -7305,4 +7334,362 @@ std::unique_ptr makeFromCurveAdaptor(const Adaptor3d_Curve& adapt, bo return geoCurve; } + +// ===== TEXT TO EDGES (adapted from FT2FC.cpp) ===== +namespace +{ +// Context for FreeType decomposition callbacks +struct FTDC_Ctx +{ + std::vector Wires; + std::vector Edges; + FT_Vector LastVert; + Handle(Geom_Surface) surf; +}; + +// Make a TopoDS_Wire from a list of TopoDS_Edges +TopoDS_Wire edgesToWire(std::vector& Edges) +{ + if (Edges.empty()) { + return TopoDS_Wire(); + } + BRepBuilderAPI_MakeWire mkWire; + for (const auto& edge : Edges) { + mkWire.Add(edge); + } + if (mkWire.IsDone()) { + TopoDS_Wire wire = mkWire.Wire(); + BRepLib::BuildCurves3d(wire); + return wire; + } + else { + Base::Console().warning("edgesToWire: Failed to build a valid wire from edges.\n"); + return TopoDS_Wire(); + } +} + +// FT Decompose callbacks +int move_cb(const FT_Vector* pt, void* p) +{ + FTDC_Ctx* dc = static_cast(p); + if (!dc->Edges.empty()) { + TopoDS_Wire newWire = edgesToWire(dc->Edges); + if (!newWire.IsNull()) { + dc->Wires.push_back(newWire); + } + dc->Edges.clear(); + } + dc->LastVert = *pt; + return 0; +} + +int line_cb(const FT_Vector* pt, void* p) +{ + FTDC_Ctx* dc = static_cast(p); + gp_Pnt2d v1(dc->LastVert.x, dc->LastVert.y); + gp_Pnt2d v2(pt->x, pt->y); + if (!v1.IsEqual(v2, Precision::Confusion())) { + Handle(Geom2d_TrimmedCurve) lseg = GCE2d_MakeSegment(v1, v2); + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(lseg, dc->surf); + dc->Edges.push_back(edge); + } + dc->LastVert = *pt; + return 0; +} + +int quad_cb(const FT_Vector* pt0, const FT_Vector* pt1, void* p) +{ + FTDC_Ctx* dc = static_cast(p); + TColgp_Array1OfPnt2d Poles(1, 3); + Poles.SetValue(1, gp_Pnt2d(dc->LastVert.x, dc->LastVert.y)); + Poles.SetValue(2, gp_Pnt2d(pt0->x, pt0->y)); + Poles.SetValue(3, gp_Pnt2d(pt1->x, pt1->y)); + Handle(Geom2d_BezierCurve) bcseg = new Geom2d_BezierCurve(Poles); + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(bcseg, dc->surf); + dc->Edges.push_back(edge); + dc->LastVert = *pt1; + return 0; +} + +int cubic_cb(const FT_Vector* pt0, const FT_Vector* pt1, const FT_Vector* pt2, void* p) +{ + FTDC_Ctx* dc = static_cast(p); + TColgp_Array1OfPnt2d Poles(1, 4); + Poles.SetValue(1, gp_Pnt2d(dc->LastVert.x, dc->LastVert.y)); + Poles.SetValue(2, gp_Pnt2d(pt0->x, pt0->y)); + Poles.SetValue(3, gp_Pnt2d(pt1->x, pt1->y)); + Poles.SetValue(4, gp_Pnt2d(pt2->x, pt2->y)); + Handle(Geom2d_BezierCurve) bcseg = new Geom2d_BezierCurve(Poles); + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(bcseg, dc->surf); + dc->Edges.push_back(edge); + dc->LastVert = *pt2; + return 0; +} + + +} // end anonymous namespace + +/** + * @brief Takes a set of base shapes, transforms them to fit a two-point + * definition, and converts them to a vector of Part::Geometry. + * + * This is the core transformation logic shared by textToEdges and the Symbol tool. + * + * @param geos Output vector of unique_ptr to Part::Geometry. + * @param baseShapes Input vector of raw TopoDS_Shape objects at origin. + * @param p1 The start point (typically bottom-left) of placement. + * @param p2 The end point, which defines the size and orientation. + * @param height If true, the distance p1-p2 defines the height. + * If false, it defines the width. + */ +void transformAndConvertToGeometry( + std::vector>& geos, + const std::vector& baseShapes, + const Base::Vector3d& p1, + const Base::Vector3d& p2, + bool height +) +{ + if (baseShapes.empty()) { + return; + } + + Base::Vector3d dir = p2 - p1; + double length = dir.Length(); + if (length < Precision::Confusion()) { + return; + } + + // 1. Calculate the bounding box of the base shapes + Bnd_Box bndBox; + for (const auto& shape : baseShapes) { + if (!shape.IsNull()) { + BRepBndLib::Add(shape, bndBox); + } + } + + if (bndBox.IsVoid()) { + Base::Console().warning( + "transformAndConvertToGeometry: Could not determine bounds of generated geometry.\n" + ); + return; + } + + Standard_Real xmin, ymin, zmin, xmax, ymax, zmax; + bndBox.Get(xmin, ymin, zmin, xmax, ymax, zmax); + double baseWidth = xmax - xmin; + double baseHeight = ymax - ymin; + + // This transform will move the geometry's bottom-left corner to the origin (0,0,0) + gp_Vec initialTranslationVec(-xmin, -ymin, -zmin); + + // 2. Determine scale and rotation + double angle; + double scale; + + if (height) { + if (baseHeight < Precision::Confusion()) { + return; + } + scale = length / baseHeight; + angle = std::atan2(dir.y, dir.x) - 0.5 * M_PI; + } + else { // Width mode + if (baseWidth < Precision::Confusion()) { + return; + } + scale = length / baseWidth; + angle = std::atan2(dir.y, dir.x); + } + + // 3. Construct the final transformation matrix + gp_Trsf initialTranslate; + initialTranslate.SetTranslation(initialTranslationVec); + gp_Trsf scaleTrsf; + scaleTrsf.SetScale(gp::Origin(), scale); + gp_Trsf rotateTrsf; + rotateTrsf.SetRotation(gp::XOY().Axis(), angle); + gp_Trsf finalTranslate; + finalTranslate.SetTranslation(gp_Vec(p1.x, p1.y, p1.z)); + gp_Trsf finalTrsf = finalTranslate * rotateTrsf * scaleTrsf * initialTranslate; + + // 4. Apply transformation and convert to Sketcher geometry + for (const auto& shape : baseShapes) { + BRepBuilderAPI_Transform performer(shape, finalTrsf, true); + if (!performer.IsDone()) { + continue; + } + + for (TopExp_Explorer explorer(performer.Shape(), TopAbs_EDGE); explorer.More(); + explorer.Next()) { + Standard_Real first, last; + const TopoDS_Edge& edge = TopoDS::Edge(explorer.Current()); + Handle(Geom_Curve) curve = BRep_Tool::Curve(edge, first, last); + if (curve.IsNull()) { + continue; + } + + std::unique_ptr newGeo; + + if (BRep_Tool::IsClosed(edge)) { + newGeo = Part::makeFromCurve(curve); + } + else { + if (curve->IsKind(STANDARD_TYPE(Geom_TrimmedCurve))) { + Handle(Geom_TrimmedCurve) trc = Handle(Geom_TrimmedCurve)::DownCast(curve); + curve = trc->BasisCurve(); + } + + if (curve->IsKind(STANDARD_TYPE(Geom_BezierCurve))) { + Handle(Geom_TrimmedCurve) tcurve + = new Geom_TrimmedCurve(curve, first, last, true, false); + Part::GeomTrimmedCurve geomcurve(tcurve); + newGeo.reset(geomcurve.toBSpline(first, last)); + } + else { + newGeo = Part::makeFromTrimmedCurve(curve, first, last); + } + } + + if (!newGeo) { + Base::Console().warning( + "transformAndConvertToGeometry: Could not create geometry from curve.\n" + ); + continue; + } + + try { + geos.emplace_back(std::move(newGeo)); + } + catch (const Base::Exception& e) { + Base::Console().warning("BSpline conversion failed: %s\n", e.what()); + } + } + } +} + +// The core logic, refactored from FT2FC to be Python-independent +std::vector makeTextWires( + std::string& text, + std::string& fontFile, + double height, + double tracking +) +{ + if (text.empty()) { + return {}; + } + + if (fontFile.empty()) { +#if defined(FC_OS_LINUX) + fontFile = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; +#elif defined(FC_OS_WIN32) + fontFile = "C:/Windows/Fonts/Arial.ttf"; +#elif defined(FC_OS_MAC) + fontFile = "/System/Library/Fonts/Helvetica.ttc"; // Common system default +#endif + } + + std::vector allWires; + FT_Library ftLib; + if (FT_Init_FreeType(&ftLib) != 0) { + Base::Console().error("makeTextWires: Could not initialize FreeType library\n"); + return allWires; + } + + std::ifstream fontStream(fontFile, std::ios::binary); + if (!fontStream) { + Base::Console().error("makeTextWires: Cannot open font file: %s\n", fontFile.c_str()); + FT_Done_FreeType(ftLib); + return allWires; + } + std::vector fontBuffer( + (std::istreambuf_iterator(fontStream)), + std::istreambuf_iterator() + ); + + FT_Face ftFace; + if (FT_New_Memory_Face( + ftLib, + reinterpret_cast(fontBuffer.data()), + fontBuffer.size(), + 0, + &ftFace + ) + != 0) { + Base::Console().error("makeTextWires: Failed to load font face from %s\n", fontFile.c_str()); + FT_Done_FreeType(ftLib); + return allWires; + } + + FT_Set_Char_Size(ftFace, 0, 48 * 64 * 10, 0, 0); + double scaleFactor = (height / static_cast(ftFace->height)) / 10.0; + FT_Outline_Funcs ftCallbacks = {move_cb, line_cb, quad_cb, cubic_cb, 0, 0}; + FT_UInt ftLoadFlags = FT_LOAD_DEFAULT | FT_LOAD_NO_BITMAP; + + double penPos = 0.0; + FT_ULong prevCharcode = 0; + + std::wstring_convert, char32_t> converter; + std::u32string wide_text = converter.from_bytes(text); + + for (size_t i = 0; i < wide_text.length(); ++i) { + FT_ULong charcode = wide_text[i]; + if (charcode == ' ') { + if (FT_Load_Char(ftFace, charcode, ftLoadFlags) == 0) { + penPos += ftFace->glyph->advance.x; + } + prevCharcode = charcode; + continue; + } + + if (FT_Load_Char(ftFace, charcode, ftLoadFlags) != 0) { + continue; + } + + if (prevCharcode != 0 && FT_HAS_KERNING(ftFace)) { + FT_Vector kern; + FT_Get_Kerning( + ftFace, + FT_Get_Char_Index(ftFace, prevCharcode), + FT_Get_Char_Index(ftFace, charcode), + FT_KERNING_DEFAULT, + &kern + ); + penPos += kern.x; + } + + FTDC_Ctx ctx; + ctx.surf = new Geom_Plane(gp::Origin(), gp::DZ()); + FT_Outline_Decompose(&ftFace->glyph->outline, &ftCallbacks, &ctx); + + if (!ctx.Edges.empty()) { + TopoDS_Wire lastWire = edgesToWire(ctx.Edges); + if (!lastWire.IsNull()) { + ctx.Wires.push_back(lastWire); + } + } + + gp_Trsf charTransform; + charTransform.SetScale(gp::Origin(), scaleFactor); + gp_Vec translation(penPos * scaleFactor + i * tracking, 0.0, 0.0); + charTransform.SetTranslationPart(translation); + + for (const auto& wire : ctx.Wires) { + BRepBuilderAPI_Transform performer(charTransform); + performer.Perform(wire, true); // true = create a copy + if (performer.IsDone()) { + allWires.push_back(performer.Shape()); + } + } + + penPos += ftFace->glyph->advance.x; + prevCharcode = charcode; + } + + FT_Done_Face(ftFace); + FT_Done_FreeType(ftLib); + return allWires; +} + } // namespace Part diff --git a/src/Mod/Part/App/Geometry.h b/src/Mod/Part/App/Geometry.h index c4cd77849f..49e58c638d 100644 --- a/src/Mod/Part/App/Geometry.h +++ b/src/Mod/Part/App/Geometry.h @@ -1417,4 +1417,34 @@ PartExport std::unique_ptr makeFromTrimmedCurve( ); PartExport std::unique_ptr makeFromCurveAdaptor(const Adaptor3d_Curve&, bool silent = false); + +/** + * @brief Creates a series of edges representing a text string. + * + * This function generates geometric edges for a given text string using a specified font file. + * The text is scaled and positioned within a bounding box defined by two points. + * + * @param geos Output vector to be populated with geometry (GeomTrimmedCurve). The vector is cleared + * first. + * @param p1 The origin point for the text's baseline. + * @param p2 A point used to define the height and orientation. The text height will be + * (p2-p1).Length() and its orientation angle will be the angle of the vector (p2-p1). + * @param plainText The string to be rendered. + * @param fontFile The absolute path to the TTF, OTF, etc., font file. + * @param tracking Additional spacing between characters. + */ +PartExport void transformAndConvertToGeometry( + std::vector>& geos, + const std::vector& baseShapes, + const Base::Vector3d& p1, + const Base::Vector3d& p2, + bool height +); + +PartExport std::vector makeTextWires( + std::string& text, + std::string& fontFile, + double height = 1.0, + double tracking = 0.0 +); } // namespace Part diff --git a/src/Mod/Sketcher/App/Constraint.cpp b/src/Mod/Sketcher/App/Constraint.cpp index 5c0f6b2e90..fb69d03a82 100644 --- a/src/Mod/Sketcher/App/Constraint.cpp +++ b/src/Mod/Sketcher/App/Constraint.cpp @@ -88,6 +88,7 @@ Constraint* Constraint::copy() const temp->isInVirtualSpace = this->isInVirtualSpace; temp->isVisible = this->isVisible; temp->isActive = this->isActive; + temp->isTextHeight = this->isTextHeight; temp->elements = this->elements; // Do not copy tag, otherwise it is considered a clone, and a "rename" by the expression engine. @@ -149,8 +150,12 @@ unsigned int Constraint::getMemSize() const void Constraint::Save(Writer& writer) const { std::string encodeName = encodeAttribute(Name); + std::string encodeText = encodeAttribute(Text); + std::string encodeFont = encodeAttribute(Font); writer.Stream() << writer.ind() << "Type == InternalAlignment) { writer.Stream() << "InternalAlignmentType=\"" << (int)AlignmentType << "\" " @@ -162,7 +167,8 @@ void Constraint::Save(Writer& writer) const << "IsDriving=\"" << (int)isDriving << "\" " << "IsInVirtualSpace=\"" << (int)isInVirtualSpace << "\" " << "IsVisible=\"" << (int)isVisible << "\" " - << "IsActive=\"" << (int)isActive << "\" "; + << "IsActive=\"" << (int)isActive << "\" " + << "IsTextHeight=\"" << (int)isTextHeight << "\" "; // Save elements { @@ -195,6 +201,8 @@ void Constraint::Restore(XMLReader& reader) { reader.readElement("Constrain"); Name = reader.getAttribute("Name"); + Text = reader.hasAttribute("Text") ? reader.getAttribute("Text") : ""; + Font = reader.hasAttribute("Font") ? reader.getAttribute("Font") : ""; Type = reader.getAttribute("Type"); Value = reader.getAttribute("Value"); @@ -234,6 +242,10 @@ void Constraint::Restore(XMLReader& reader) isActive = reader.getAttribute("IsActive"); } + if (reader.hasAttribute("IsTextHeight")) { + isTextHeight = reader.getAttribute("IsTextHeight"); + } + if (reader.hasAttribute("ElementIds") && reader.hasAttribute("ElementPositions")) { auto splitAndClean = [](std::string_view input) { const char delimiter = ' '; @@ -391,32 +403,31 @@ GeoElementId Constraint::getElement(size_t index) const #endif return elements[index]; } + void Constraint::setElement(size_t index, GeoElementId element) { - if (index >= elements.size()) { - throw Base::IndexError("Constraint::getElement index out of range"); - } - - elements[index] = element; + if (ensureElementExists(index)) { + elements[index] = element; #if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS - if (index < 3) { - switch (index) { - case 0: - First = element.GeoId; - FirstPos = element.Pos; - break; - case 1: - Second = element.GeoId; - SecondPos = element.Pos; - break; - case 2: - Third = element.GeoId; - ThirdPos = element.Pos; - break; + if (index < 3) { + switch (index) { + case 0: + First = element.GeoId; + FirstPos = element.Pos; + break; + case 1: + Second = element.GeoId; + SecondPos = element.Pos; + break; + case 2: + Third = element.GeoId; + ThirdPos = element.Pos; + break; + } } - } #endif + } } size_t Constraint::getElementsSize() const @@ -434,3 +445,158 @@ void Constraint::addElement(GeoElementId element) elements.push_back(element); #endif } + +int Constraint::getGeoId(int index) const +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + if (index < 3) { + switch (index) { + case 0: + return First; + case 1: + return Second; + case 2: + return Third; + } + } +#endif + return hasElement(index) ? elements[index].GeoId : GeoEnum::GeoUndef; +} + +PointPos Constraint::getPosId(int index) const +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + if (index < 3) { + switch (index) { + case 0: + return FirstPos; + case 1: + return SecondPos; + case 2: + return ThirdPos; + } + } +#endif + return hasElement(index) ? elements[index].Pos : PointPos::none; +} + +int Constraint::getPosIdAsInt(int index) const +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + if (index < 3) { + switch (index) { + case 0: + return (int)FirstPos; + case 1: + return (int)SecondPos; + case 2: + return (int)ThirdPos; + } + } +#endif + return hasElement(index) ? elements[index].posIdAsInt() : 0; +} + +bool Constraint::hasElement(int index) const +{ + return index >= 0 && index < elements.size(); +} + +void Constraint::setGeoId(int index, int geoId) +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + if (index < 3) { + switch (index) { + case 0: + First = geoId; + break; + case 1: + Second = geoId; + break; + case 2: + Third = geoId; + break; + } + } +#endif + if (ensureElementExists(index)) { + elements[index].GeoId = geoId; + } +} + +void Constraint::setPosId(int index, PointPos pos) +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + if (index < 3) { + switch (index) { + case 0: + FirstPos = pos; + break; + case 1: + SecondPos = pos; + break; + case 2: + ThirdPos = pos; + break; + } + } +#endif + if (ensureElementExists(index)) { + elements[index].Pos = pos; + } +} + +void Constraint::setPosId(int index, int pos) +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + if (index < 3) { + switch (index) { + case 0: + FirstPos = static_cast(pos); + break; + case 1: + SecondPos = static_cast(pos); + break; + case 2: + ThirdPos = static_cast(pos); + break; + } + } +#endif + if (ensureElementExists(index)) { + elements[index].Pos = static_cast(pos); + } +} + +bool Constraint::ensureElementExists(int index) +{ + if (index < 0) { + return false; // Indicate failure for an invalid index + } + if (index >= elements.size()) { + elements.resize(index + 1); + } + return true; +} + +void Constraint::swapElements(int index1, int index2) +{ + if (index1 == index2) { + return; + } + if (ensureElementExists(index1) && ensureElementExists(index2)) { + std::swap(elements[index1], elements[index2]); + } +} + +bool Constraint::isElementsEmpty() const +{ + return elements.empty(); +} + +void Constraint::truncateElements(size_t newSize) +{ + if (newSize < elements.size()) { + elements.resize(newSize); + } +} diff --git a/src/Mod/Sketcher/App/Constraint.h b/src/Mod/Sketcher/App/Constraint.h index cd75bb6098..42feb9c494 100644 --- a/src/Mod/Sketcher/App/Constraint.h +++ b/src/Mod/Sketcher/App/Constraint.h @@ -69,6 +69,8 @@ enum ConstraintType : int Block = 17, Diameter = 18, Weight = 19, + Group = 20, + Text = 21, NumConstraintTypes // must be the last item! }; @@ -189,7 +191,9 @@ private: "SnellsLaw", "Block", "Diameter", - "Weight"}}; + "Weight", + "Group", + "Text"}}; // clang-format on constexpr static std::array @@ -212,6 +216,8 @@ public: ConstraintType Type {None}; InternalAlignmentType AlignmentType {Undef}; std::string Name; + std::string Text; + std::string Font; float LabelDistance {10.F}; float LabelPosition {0.F}; bool isDriving {true}; @@ -222,11 +228,23 @@ public: bool isVisible {true}; bool isActive {true}; + bool isTextHeight {true}; GeoElementId getElement(size_t index) const; void setElement(size_t index, GeoElementId element); - size_t getElementsSize() const; void addElement(GeoElementId element); + bool hasElement(int index) const; + size_t getElementsSize() const; + bool isElementsEmpty() const; + void truncateElements(size_t newSize); + int getGeoId(int index) const; + PointPos getPosId(int index) const; + int getPosIdAsInt(int index) const; + void setGeoId(int index, int geoId); + void setPosId(int index, PointPos pos); + void setPosId(int index, int pos); + void swapElements(int index1, int index2); + bool ensureElementExists(int index); #ifdef SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS // Deprecated, use getElement/setElement instead diff --git a/src/Mod/Sketcher/App/ConstraintPyImp.cpp b/src/Mod/Sketcher/App/ConstraintPyImp.cpp index a051cc619c..6935f0cbce 100644 --- a/src/Mod/Sketcher/App/ConstraintPyImp.cpp +++ b/src/Mod/Sketcher/App/ConstraintPyImp.cpp @@ -97,13 +97,102 @@ int ConstraintPy::PyInit(PyObject* args, PyObject* /*kwd*/) PyObject* index_or_value; PyObject* oNumArg4; PyObject* oNumArg5; + PyObject* py_elements_list = nullptr; + char* text_str = nullptr; // Variable for the text content + char* font_str = nullptr; // Variable for the font name int any_index; PyObject* activated; PyObject* driving; + PyObject* py_is_height = nullptr; Sketcher::Constraint* constraint = this->getConstraintPtr(); + auto parseElementsList = [](PyObject* list, Sketcher::Constraint* constr_ptr) -> bool { + Py_ssize_t list_size = PyList_Size(list); + + // The list should contain pairs of (geoId, posId), so its size must be even. + if (list_size % 2 != 0) { + PyErr_SetString( + PyExc_ValueError, + "Element list must have an even number of items (pairs of GeoId, PosId)." + ); + return false; // Failure + } + + for (Py_ssize_t i = 0; i < list_size; i += 2) { + PyObject* py_geoId_obj = PyList_GetItem(list, i); + PyObject* py_posId_obj = PyList_GetItem(list, i + 1); + + // Perform crucial type checking on list items + if (!py_geoId_obj || !py_posId_obj || !PyLong_Check(py_geoId_obj) + || !PyLong_Check(py_posId_obj)) { + PyErr_SetString(PyExc_TypeError, "Element list items must be integers."); + return false; // Failure + } + + int geoId = PyLong_AsLong(py_geoId_obj); + int posId = PyLong_AsLong(py_posId_obj); + + // Use the C++ API to populate the constraint + constr_ptr->setElement( + i / 2, + Sketcher::GeoElementId(geoId, static_cast(posId)) + ); + } + + return true; // Success + }; + + // Attempt to parse (string, list) for 'Group' + if (PyArg_ParseTuple(args, "sO!", &ConstraintType, &PyList_Type, &py_elements_list)) { + if (strcmp(ConstraintType, "Group") == 0) { + constraint->Type = Sketcher::Group; + if (!parseElementsList(py_elements_list, constraint)) { + return -1; // The lambda set the Python error, so just return. + } + return 0; // Success! + } + } + PyErr_Clear(); + + // Attempt to parse (string, list, string, string, bool) for 'Text' + if (PyArg_ParseTuple( + args, + "sO!ss|O", + &ConstraintType, + &PyList_Type, + &py_elements_list, + &text_str, + &font_str, + &py_is_height + )) { + + if (strcmp(ConstraintType, "Text") == 0) { + constraint->Type = Sketcher::Text; + + // Call the shared lambda for list parsing + if (!parseElementsList(py_elements_list, constraint)) { + return -1; // The lambda set the Python error. + } + + // Set the specific members for the Text constraint + constraint->Text = text_str; + constraint->Font = font_str; + + // Check and set the optional boolean + if (py_is_height && PyBool_Check(py_is_height)) { + constraint->isTextHeight = (py_is_height == Py_True); + } + else { + constraint->isTextHeight = true; + } + + return 0; // Success! + } + } + PyErr_Clear(); + auto handleSi = [&]() -> bool { if (strcmp("Horizontal", ConstraintType) == 0) { constraint->Type = Horizontal; @@ -904,6 +993,12 @@ std::string ConstraintPy::representation() const result << "'PointOnObject' (" << getConstraintPtr()->First << "," << getConstraintPtr()->Second << ")>"; break; + case Group: + result << "'Group'>"; + break; + case Text: + result << "'Text'>"; + break; default: result << "'?'>"; break; @@ -974,6 +1069,12 @@ Py::String ConstraintPy::getType() const case PointOnObject: return Py::String("PointOnObject"); break; + case Group: + return Py::String("Group"); + break; + case Text: + return Py::String("Text"); + break; default: return Py::String("Undefined"); break; diff --git a/src/Mod/Sketcher/App/Sketch.cpp b/src/Mod/Sketcher/App/Sketch.cpp index 62398a1a0e..058dbbb5e3 100644 --- a/src/Mod/Sketcher/App/Sketch.cpp +++ b/src/Mod/Sketcher/App/Sketch.cpp @@ -197,6 +197,17 @@ int Sketch::setUpSketch( clear(); + // The geometries that are in groups are going to be ignored by the solver. + std::set slaveGeoIds; + for (const auto& c : ConstraintList) { + if (c->Type == Group || c->Type == Text) { + // Start from index 1, as 0 is the frame. + for (int i = 1; c->hasElement(i); ++i) { + slaveGeoIds.insert(c->getGeoId(i)); + } + } + } + std::vector intGeoList, extGeoList; std::copy(GeoList.begin(), GeoList.end() - extGeoCount, std::back_inserter(intGeoList)); std::copy(GeoList.end() - extGeoCount, GeoList.end(), std::back_inserter(extGeoList)); @@ -248,7 +259,7 @@ int Sketch::setUpSketch( buildInternalAlignmentGeometryMap(ConstraintList); - addGeometry(intGeoList, onlyBlockedGeometry); + addGeometry(intGeoList, onlyBlockedGeometry, slaveGeoIds); int extStart = Geoms.size(); addGeometry(extGeoList, true); int extEnd = Geoms.size() - 1; @@ -258,6 +269,23 @@ int Sketch::setUpSketch( // The Geoms list might be empty after an undo/redo if (!Geoms.empty()) { + // Disable any constraint that act on geometries that are in a group. + for (size_t i = 0; i < ConstraintList.size(); ++i) { + const auto& c = ConstraintList[i]; + + if (c->Type == Group || c->Type == Text) { + continue; + } + + bool hasSlaveReference = false; + for (int j = 0; c->hasElement(j); ++j) { + if (slaveGeoIds.count(c->getGeoId(j))) { + unenforceableConstraints[i] = true; + break; + } + } + } + addConstraints(ConstraintList, unenforceableConstraints); } clearTemporaryConstraints(); @@ -731,18 +759,33 @@ int Sketch::addGeometry(const std::vector& geos, bool fixed) return ret; } -int Sketch::addGeometry(const std::vector& geos, const std::vector& blockedGeometry) +int Sketch::addGeometry( + const std::vector& geos, + const std::vector& blockedGeometry, + const std::set& slaveGeoIds +) { assert(geos.size() == blockedGeometry.size()); int ret = -1; + int geoIdCounter = 0; std::vector::const_iterator it; std::vector::const_iterator bit; for (it = geos.begin(), bit = blockedGeometry.begin(); it != geos.end() && bit != blockedGeometry.end(); - ++it, ++bit) { - ret = addGeometry(*it, *bit); + ++it, ++bit, ++geoIdCounter) { + + // Check if the current geometry is in group. + bool isSlave = slaveGeoIds.count(geoIdCounter); + if (isSlave) { + GeoDef def; + def.geo = (*it)->clone(); + Geoms.push_back(def); + } + else { + ret = addGeometry(*it, *bit); + } } return ret; } @@ -2471,6 +2514,19 @@ int Sketch::addConstraint(const Constraint* constraint) c.driving ); } break; + case Text: + case Group: { + if (constraint->isElementsEmpty()) { + return -1; + } + // Check that the first element is correctly the group construction line + if (Geoms[checkGeoId(constraint->getGeoId(0))].type != Line) { + return -1; + } + + rtn = ++ConstraintsCounter; + break; + } case Sketcher::None: // ambiguous enum value case Sketcher::Block: // handled separately while adding geometry case NumConstraintTypes: @@ -4759,6 +4815,8 @@ bool Sketch::updateNonDrivingConstraints() int Sketch::solve() { + captureGroupStates(); + Base::TimeElapsed start_time; std::string solvername; @@ -4777,6 +4835,10 @@ int Sketch::solve() SolveTime = Base::TimeElapsed::diffTimeF(start_time, end_time); + if (result == GCS::Success) { + applyGroupTransformations(); + } + return result; } @@ -5513,3 +5575,110 @@ void Sketch::Save(Writer&) const void Sketch::Restore(XMLReader&) {} + +// Group functions related ------------------------------------------------- + +Sketch::GroupLineState Sketch::getGroupLineState(int geoId) const +{ + GroupLineState state; + state.startPoint = getPoint(geoId, PointPos::start); + state.endPoint = getPoint(geoId, PointPos::end); + return state; +} + +void Sketch::captureGroupStates() +{ + preSolveGroupStates.clear(); + + // A set to keep track of which parameters we've already moved. + std::set movedParams; + + for (const auto& constrDef : Constrs) { + const Constraint* c = constrDef.constr; + + if ((c->Type != Group && c->Type != Text) || !c->hasElement(1)) { + continue; + } + + // --- Capture Frame State --- + int frameGeoId = c->getGeoId(0); + preSolveGroupStates[frameGeoId] = getGroupLineState(frameGeoId); + } +} + +void Sketch::applyGroupTransformations() +{ + if (preSolveGroupStates.empty()) { + return; + } + + for (const auto& constrDef : Constrs) { + const Constraint* c = constrDef.constr; + if ((c->Type != Group && c->Type != Text) || !c->hasElement(1)) { + continue; + } + + int frameGeoId = c->getGeoId(0); + + // Get the "before" and "after" states of the frame line + GroupLineState preSolveFrame = preSolveGroupStates.at(frameGeoId); + GroupLineState postSolveFrame = getGroupLineState(frameGeoId); + + // --- Calculate the Transformation --- + Base::Vector3d preVec = preSolveFrame.getVec(); + Base::Vector3d postVec = postSolveFrame.getVec(); + + // Handle potential zero-length lines to avoid division by zero + double preLen = preVec.Length(); + double scale = (preLen > Precision::Confusion()) ? postVec.Length() / preLen : 1.0; + + // --- Create the Transformation Matrix --- + + // 1. T1: Matrix to translate the group to the origin (using pre-solve start point) + Base::Matrix4D T1; // Identity + T1[0][3] = -preSolveFrame.startPoint.x; + T1[1][3] = -preSolveFrame.startPoint.y; + T1[2][3] = 0; + + // 2. S: Matrix for scaling + Base::Matrix4D S; // Identity + S[0][0] = scale; + S[1][1] = scale; + S[2][2] = scale; + + // 3. R: Matrix for rotation + Base::Matrix4D R; // Identity + if (preLen > Precision::Confusion()) { + // We can get the axis and angle from the two vectors and use rotLine + Base::Vector3d rotationAxis = preVec.Cross(postVec); + double rotationAngle = preVec.GetAngle(postVec); + // Only apply rotation if the vectors are not collinear + if (rotationAxis.Length() > Precision::Confusion()) { + R.rotLine(rotationAxis, rotationAngle); + } + } + + // 4. T2: Matrix to translate the group to its new final position + Base::Matrix4D T2; // Identity + T2[0][3] = postSolveFrame.startPoint.x; + T2[1][3] = postSolveFrame.startPoint.y; + T2[2][3] = postSolveFrame.startPoint.z; + + // 5. Combine the matrices in the correct order: T_final = T2 * R * S * T1 + // The * operator is overloaded for matrix multiplication. + Base::Matrix4D transform = T2 * R * S * T1; + + // --- Loop through slave elements and apply the transform --- + for (int i = 1; c->hasElement(i); ++i) { + int slaveGeoId = c->getGeoId(i); + + // Get the slave's current (pre-solve) state + Part::Geometry* slaveGeo = Geoms[checkGeoId(slaveGeoId)].geo; + + // Apply the calculated transformation + slaveGeo->transform(transform); + } + } + + preSolveGroupStates.clear(); +} diff --git a/src/Mod/Sketcher/App/Sketch.h b/src/Mod/Sketcher/App/Sketch.h index 88760185ca..bfd175d5aa 100644 --- a/src/Mod/Sketcher/App/Sketch.h +++ b/src/Mod/Sketcher/App/Sketch.h @@ -88,7 +88,11 @@ public: int addGeometry(const std::vector& geos, bool fixed = false); /// add unspecified geometry, where each element's "fixed" status is given by the /// blockedGeometry array - int addGeometry(const std::vector& geos, const std::vector& blockedGeometry); + int addGeometry( + const std::vector& geos, + const std::vector& blockedGeometry, + const std::set& slaveGeoIds + ); /// get boolean list indicating whether the geometry is to be blocked or not void getBlockedGeometry( std::vector& blockedGeometry, @@ -626,6 +630,34 @@ private: Base::Vector3d initToPoint; double moveStep; + // Group related things : + /// container to store information about groups + struct GroupLineState + { + Base::Vector3d startPoint; + Base::Vector3d endPoint; + + // Convenience method to get the length (scale) + double getLength() const + { + return (endPoint - startPoint).Length(); + } + + // Convenience method to get the orientation vector + Base::Vector3d getVec() const + { + return endPoint - startPoint; + } + }; + // This map stores the state of group lines just BEFORE a solve. + // We will use this to calculate the transformation AFTER the solve. + // Key: GeoId of the frame line. + // Value: it's initial position. + std::map preSolveGroupStates; + void captureGroupStates(); + void applyGroupTransformations(); + GroupLineState getGroupLineState(int geoId) const; + public: GCS::Algorithm defaultSolver; GCS::Algorithm defaultSolverRedundant; diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 9c0be8fc15..1f072e79a3 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -906,6 +906,109 @@ double SketchObject::getDatum(int ConstrId) const return this->Constraints[ConstrId]->getValue(); } +int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string& newFont) +{ +; // 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& vals = this->Constraints.getValues(); + if (ConstrId < 0 || ConstrId >= int(vals.size())) { + return -1; + } + + auto* constr = vals[ConstrId]; + if (constr->Type != Text || !constr->hasElement(1)) { + return -1; + } + + // First we replace the old geometries by the new text. + const bool isHeight = constr->isTextHeight; + const std::string oldText = constr->Text; + const std::string oldFont = constr->Font; + int handleGeoId = constr->getGeoId(0); + int firstTextGeoId = constr->getGeoId(1); + bool handleLast = handleGeoId > firstTextGeoId; + + // Check if text is construction or normal geos + auto* geo1 = getGeometry(firstTextGeoId); + bool isConstruction = GeometryFacade::getConstruction(geo1); + + // Delete all the old text geos. Not the handle! + std::vector geoIdsToDelete; + for (int i = 1; constr->hasElement(i); ++i) { + geoIdsToDelete.push_back(constr->getGeoId(i)); + if (handleLast) { + --handleGeoId; // handle line is added after all text geos. + } + } + + delGeometries(geoIdsToDelete); + auto* line = dynamic_cast(getGeometry(handleGeoId)); + if (!line) { + return -1; + } + + // Generate text geos based on new text/font : + std::vector> newGeos; + std::vector shapes = Part::makeTextWires(newText, newFont); + Part::transformAndConvertToGeometry(newGeos, + shapes, + line->getStartPoint(), + line->getEndPoint(), + isHeight); + if (isConstruction) { + for (size_t i = 0; i < newGeos.size(); ++i) { + Sketcher::GeometryFacade::setConstruction(newGeos[i].get(), isConstruction); + } + } + + // Add the geometries to sketch + int lastGeoid = getHighestCurveIndex(); + std::vector newGeosRawPtrs; + newGeosRawPtrs.reserve(newGeos.size()); + + // Populate the raw pointer vector and release ownership from the unique_ptrs. + for (auto& geo_ptr : newGeos) { + if (isConstruction) { + Sketcher::GeometryFacade::setConstruction(geo_ptr.get(), isConstruction); + } + // Add the raw pointer to the new vector. + newGeosRawPtrs.push_back(geo_ptr.get()); + // Release ownership from the unique_ptr. The SketchObject will now manage this memory. + geo_ptr.release(); + } + newGeos.clear(); + addGeometry(newGeosRawPtrs); + + // Create a new constraint to replace the one that was deleted. + int newLastGeoid = getHighestCurveIndex(); + constr = new Constraint(); + constr->Type = Text; + constr->truncateElements(0); // remove the First/Second/Third that are created automatically + constr->addElement(GeoElementId(handleGeoId)); + for (int i = lastGeoid + 1; i <= newLastGeoid; ++i) { + constr->addElement(GeoElementId(i)); + } + constr->Text = newText; + constr->Font = newFont; + constr->isTextHeight = isHeight; + + addConstraint(constr); + + int err = solve(); + + if (err) { + constr->Text = oldText; + constr->Font = oldFont; + } + + return err; +} + int SketchObject::setDriving(int ConstrId, bool isdriving) { // no need to check input data validity as this is an sketchobject managed operation. @@ -3084,6 +3187,14 @@ void SketchObject::changeConstraintAfterDeletingGeo(Constraint* constr, return; } + for (int i = 0; constr->hasElement(i); ++i) { + if (constr->getGeoId(i) == deletedGeoId){ + constr->Type = ConstraintType::None; + return; + } + } + + // legacy to make sure we're not missing something... if (constr->involvesGeoId(deletedGeoId)) { constr->Type = ConstraintType::None; return; @@ -3100,14 +3211,10 @@ void SketchObject::changeConstraintAfterDeletingGeo(Constraint* constr, }; } - if (needsUpdate(constr->First)) { - constr->First -= step; - } - if (needsUpdate(constr->Second)) { - constr->Second -= step; - } - if (needsUpdate(constr->Third)) { - constr->Third -= step; + for (int i = 0; constr->hasElement(i); ++i) { + if (needsUpdate(constr->getGeoId(i))) { + constr->setGeoId(i, constr->getGeoId(i) - step); + } } } @@ -10110,6 +10217,8 @@ bool SketchObject::evaluateConstraint(const Constraint* constraint) const case Angle: break; case Tangent: + case Group: + case Text: requireSecond = true; break; case Symmetric: @@ -10256,6 +10365,59 @@ std::string SketchObject::validateExpression(const App::ObjectIdentifier& path, return ""; } +bool SketchObject::isInGroup(int geoId, bool includeHandle) const +{ + const std::vector& vals = Constraints.getValues(); + + for (const auto& constr : vals) { + if (constr->Type == Group || constr->Type == Text) { + // First is the group construction line. We include it or not in our search. + int iStart = includeHandle ? 0 : 1; + for (int i = iStart; constr->hasElement(i); ++i) { + if (constr->getGeoId(i) == geoId) { + return true; + } + } + } + } + return false; +} + +bool SketchObject::isGroupHandle(int geoId) const +{ + const std::vector& vals = Constraints.getValues(); + + for (const auto& constr : vals) { + if (constr->Type == Group || constr->Type == Text) { + if (constr->getGeoId(0) == geoId) { + return true; + } + } + } + return false; +} + +int SketchObject::getGroupHandleIfInGroup(int geoId) +{ + const std::vector& vals = Constraints.getValues(); + + for (const auto& constr : vals) { + if (constr->Type == Group || constr->Type == Text) { + // First is the group construction line. + int groupHandleGeoId = -1; + for (int i = 0; constr->hasElement(i); ++i) { + if (i == 0) { + groupHandleGeoId = constr->getGeoId(i); + } + else if (constr->getGeoId(i) == geoId) { + return groupHandleGeoId; + } + } + } + } + return geoId; +} + // 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. diff --git a/src/Mod/Sketcher/App/SketchObject.h b/src/Mod/Sketcher/App/SketchObject.h index 7d271452d1..dba65ceb69 100644 --- a/src/Mod/Sketcher/App/SketchObject.h +++ b/src/Mod/Sketcher/App/SketchObject.h @@ -142,6 +142,21 @@ public: \retval bool - true if the geometry is supported */ bool isSupportedGeometry(const Part::Geometry* geo) const; + + /*! + \brief Returns true if the geometry is in a group + \param geoId - the geometry id in the sketch + \param includeHandle - return true if geoId is the group construction line handle + \retval bool - true if the geometry is supported + */ + bool isInGroup(int geoId, bool includeHandle = true) const; + bool isGroupHandle(int geoId) const; + /*! + \brief Returns geoId if it's not in a group. Or the group handle if it is in a group. + \param geoId - the geometry id in the sketch + */ + int getGroupHandleIfInGroup(int geoId); + /*! \brief Add geometry to a sketch - It adds a copy with a different uuid (internally uses copy() instead of clone()) \param geo - geometry to add \param construction - true for construction @@ -338,6 +353,8 @@ public: int setDatum(int ConstrId, double Datum); /// get the datum of a Distance or Angle constraint double getDatum(int ConstrId) const; + /// set the text and font of a text constraint + int setTextAndFont(int ConstrId, std::string& newText, std::string& newFont); /// set the driving status of this constraint and solve int setDriving(int ConstrId, bool isdriving); /// get the driving status of this constraint diff --git a/src/Mod/Sketcher/App/SketchObject.pyi b/src/Mod/Sketcher/App/SketchObject.pyi index 5af2af5605..7d41ac7d84 100644 --- a/src/Mod/Sketcher/App/SketchObject.pyi +++ b/src/Mod/Sketcher/App/SketchObject.pyi @@ -409,6 +409,19 @@ class SketchObject(Part2DObject): """ ... + def setTextAndFont(self, constraint: int, text: str, font: str) -> None: + """ + Set the text and font of a Text constraint. + + setTextAndFont(constraint: int, text: str, font: str) + + Args: + constraint: The index of the Text constraint. + text: The text string to display. + font: The full path to the font file (.ttf, .otf, etc.). + """ + ... + @constmethod def getDatum(self, constraint: Union[int, str], /) -> Quantity: """ diff --git a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp index fc8a890b7f..6851756d51 100644 --- a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp +++ b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp @@ -756,6 +756,44 @@ PyObject* SketchObjectPy::delConstraintsToExternal() Py_Return; } +PyObject* SketchObjectPy::setTextAndFont(PyObject* args, PyObject* kwd) +{ + int constrIndex = -1; + char* textStr; + char* fontStr; + char* constrName = nullptr; + + // Try to parse (int, str, str) + if (PyArg_ParseTuple(args, "iss", &constrIndex, &textStr, &fontStr)) { + // This format is valid, proceed. + } + + std::string text(textStr); + std::string font(fontStr); + + // Call the C++ implementation + int err = this->getSketchObjectPtr()->setTextAndFont(constrIndex, text, font); + + // Handle errors returned from the C++ function + if (err) { + std::stringstream str; + if (err == -1) { + str << "Invalid constraint index or not a Text constraint: " << constrIndex; + } + else if (err == -6) { + str << "Cannot set text/font because of invalid geometry in the sketch"; + } + else { // Generic error for solver failures etc. + str << "Failed to set text/font for constraint with index " << constrIndex + << ". The operation would result in an invalid sketch."; + } + PyErr_SetString(PyExc_ValueError, str.str().c_str()); + return nullptr; + } + + Py_Return; +} + PyObject* SketchObjectPy::setDatum(PyObject* args) { double Datum; diff --git a/src/Mod/Sketcher/Gui/CMakeLists.txt b/src/Mod/Sketcher/Gui/CMakeLists.txt index 159a288d61..74ed660a78 100644 --- a/src/Mod/Sketcher/Gui/CMakeLists.txt +++ b/src/Mod/Sketcher/Gui/CMakeLists.txt @@ -21,6 +21,7 @@ set(SketcherGui_UIC_SRCS TaskSketcherSolverAdvanced.ui TaskSketcherValidation.ui InsertDatum.ui + EditTextDialog.ui SketchOrientationDialog.ui SketchMirrorDialog.ui SketcherSettings.ui @@ -53,6 +54,7 @@ SET(SketcherGui_SRCS DrawSketchHandlerArcOfHyperbola.h DrawSketchHandlerArcOfParabola.h DrawSketchHandlerArcSlot.h + DrawSketchHandlerText.h DrawSketchHandlerBSpline.h DrawSketchHandlerPoint.h DrawSketchHandlerFillet.h @@ -136,6 +138,9 @@ SET(SketcherGui_SRCS Workbench.h EditDatumDialog.cpp EditDatumDialog.h + EditTextDialog.ui + EditTextDialog.cpp + EditTextDialog.h PropertyVisualLayerList.cpp PropertyVisualLayerList.h SketchOrientationDialog.cpp diff --git a/src/Mod/Sketcher/Gui/CommandAlterGeometry.cpp b/src/Mod/Sketcher/Gui/CommandAlterGeometry.cpp index 257fc23066..bcb75698ec 100644 --- a/src/Mod/Sketcher/Gui/CommandAlterGeometry.cpp +++ b/src/Mod/Sketcher/Gui/CommandAlterGeometry.cpp @@ -87,6 +87,7 @@ CmdSketcherToggleConstruction::CmdSketcherToggleConstruction() rcCmdMgr.addCommandMode("ToggleConstruction", "Sketcher_CreateSlot"); rcCmdMgr.addCommandMode("ToggleConstruction", "Sketcher_CompSlot"); rcCmdMgr.addCommandMode("ToggleConstruction", "Sketcher_CreateArc"); + rcCmdMgr.addCommandMode("ToggleConstruction", "Sketcher_CreateText"); rcCmdMgr.addCommandMode("ToggleConstruction", "Sketcher_Create3PointArc"); rcCmdMgr.addCommandMode("ToggleConstruction", "Sketcher_CreateEllipseByCenter"); rcCmdMgr.addCommandMode("ToggleConstruction", "Sketcher_CreateEllipseBy3Points"); diff --git a/src/Mod/Sketcher/Gui/CommandConstraints.cpp b/src/Mod/Sketcher/Gui/CommandConstraints.cpp index 5b010edbd4..15e01d3800 100644 --- a/src/Mod/Sketcher/Gui/CommandConstraints.cpp +++ b/src/Mod/Sketcher/Gui/CommandConstraints.cpp @@ -25,7 +25,12 @@ #include #include +#include #include +#include +#include + +#include #include @@ -10440,6 +10445,248 @@ bool CmdSketcherConstrainSnellsLaw::isActive() return isCreateConstraintActive(getActiveGuiDocument()); } +// ====================================================================================== + +DEF_STD_CMD_A(CmdSketcherConstrainGroup) + +CmdSketcherConstrainGroup::CmdSketcherConstrainGroup() + : Command("Sketcher_ConstrainGroup") +{ + sAppModule = "Sketcher"; + sGroup = "Sketcher"; + sMenuText = QT_TR_NOOP("Group Constrain"); + sToolTipText = QT_TR_NOOP("Constrains the selected geometries together as a single entity." + "The position and size of the grouped geometries can be defined by constraining the construction line that is generated." + "Constraints applied to grouped edges are ignored as long as the Group constraint is here."); + sWhatsThis = "Sketcher_ConstrainGroup"; + sStatusTip = sToolTipText; + sPixmap = "Constraint_Group"; + sAccel = "K, G"; + eType = ForEdit; +} + +void CmdSketcherConstrainGroup::activated(int iMsg) +{ + Q_UNUSED(iMsg); + + // get the selection + std::vector selection = getSelection().getSelectionEx(); + + // only one sketch with its subelements are allowed to be selected + if (selection.size() != 1 + || !selection[0].isObjectTypeOf(Sketcher::SketchObject::getClassTypeId())) { + const char dmbg[] = "Constraint_Group"; + + QString strError = QObject::tr("Selected objects are not just geometry " + "from one sketch.", + dmbg); + + Gui::TranslatedUserWarning(getActiveGuiDocument()->getDocument(), + QObject::tr("Wrong selection"), + std::move(strError)); + } + + // get the needed lists and objects + auto* Obj = static_cast(selection[0].getObject()); + const std::vector& SubNames = selection[0].getSubNames(); + + if (SubNames.empty()) { + Gui::TranslatedUserWarning(Obj, + QObject::tr("Wrong selection"), + QObject::tr("No geometries selected")); + return; + } + + std::vector elts; + + for (auto& subName : SubNames) { + int geoId; + Sketcher::PointPos posId; + getIdsFromName(subName, Obj, geoId, posId); + + bool alreadyAdded = std::any_of(elts.begin(), elts.end(), + [geoId](const Sketcher::GeoElementId& elem) { + return elem.GeoId == geoId; + }); + + if (geoId < 0 || alreadyAdded || Obj->getGeometryFacade(geoId)->isInternalAligned()) { + continue; + } + + elts.push_back(Sketcher::GeoElementId(geoId, Sketcher::PointPos::none)); + } + + if (elts.size() < 2) { + Base::Console().warning("Cannot create group : minimum 2 geometries must be selected.\n"); + return; + } + + openCommand(QT_TRANSLATE_NOOP("Command", "Add Group constraint")); + + if (!addListConstraint(Obj, elts, "Group")) { + abortCommand(); + return; + } + + tryAutoRecompute(Obj); + + commitCommand(); + + getSelection().clearSelection(); +} + +namespace { +/** + * @brief Escapes a string for safe embedding within a single-quoted Python string literal. + * + * This function handles backslashes and single quotes. + * + * @param input The raw string to escape. + * @return A new string with special characters escaped. + */ +std::string escapeForPython(const std::string& input) +{ + std::string result; + // Pre-allocating can be a small optimization if strings are long + result.reserve(input.length()); + + for (char c : input) { + if (c == '\\') { + result += "\\\\"; + } else if (c == '\'') { + result += "\\'"; + } else { + result += c; + } + } + return result; +} +} // anonymous namespace + +bool SketcherGui::addListConstraint(Sketcher::SketchObject* Obj, + std::vector& elts, + const std::string& constraintType, + Base::Vector2d frame_p1, + Base::Vector2d frame_p2, + bool isTextHeight, + const std::string& text, + const std::string& font) +{ + + std::vector geoIdsWithInternalGeos; + // The lambda defines the condition for REMOVAL. + // It returns 'true' if an element should be erased. + auto new_end = std::remove_if(elts.begin(), elts.end(), + [&](const Sketcher::GeoElementId& element) -> bool { + int geoId = element.GeoId; + + // Condition 1: Check for invalid or already-aligned geometries. + // If true, this element should be removed. + if (geoId < 0 || Obj->getGeometryFacade(geoId)->isInternalAligned()) { + return true; // Mark for removal + } + + // Condition 2: Check for internal geometries that need cleanup later. + // This does not mark the element for removal, but collects its ID. + const Part::Geometry* geo = Obj->getGeometry(geoId); + if (Obj->hasInternalGeometry(geo)) { + // Collect the ID for later processing. + geoIdsWithInternalGeos.push_back(geoId); + } + + // If we reached here, the element is valid and should be kept. + return false; // Do not remove + }); + + // Actually erase the elements that were moved to the end. + elts.erase(new_end, elts.end()); + + if (elts.size() < 2) { + Base::Console().warning("Cannot create %s constraint: minimum 2 geometries.\n", constraintType.c_str()); + return false; + } + + if ((frame_p1 - frame_p2).Length() < Precision::Confusion()) { + // --- 1. Calculate Bounding Box --- + Bnd_Box totalBBox; + for (const auto& element : elts) { + const Part::Geometry* geo = Obj->getGeometry(element.GeoId); + if (geo) { + BRepBndLib::Add(geo->toShape(), totalBBox, false); + } + } + + if (!totalBBox.HasFinitePart()) { + Base::Console().warning("Cannot create %s constraint: bounding box is infinite\n", constraintType.c_str()); + return false; + } + + gp_Pnt min_pnt = totalBBox.CornerMin(); + gp_Pnt max_pnt = totalBBox.CornerMax(); + + // --- 2. Define and create the Construction Line "Frame" --- + frame_p1 = Base::Vector2d(min_pnt.X(), min_pnt.Y()); + frame_p2 = Base::Vector2d(min_pnt.X(), max_pnt.Y()); + } + + Gui::cmdAppObjectArgs(Obj, + "addGeometry(Part.LineSegment(App.Vector(%f,%f,0), App.Vector(%f,%f,0)), True)", + frame_p1.x, frame_p1.y, frame_p2.x, frame_p2.y); + + int frameGeoId = Obj->getHighestCurveIndex(); + + // --- 3. Prepend the Frame to the Element List --- + elts.insert(elts.begin(), Sketcher::GeoElementId(frameGeoId, Sketcher::PointPos::none)); + + // --- 4. Create the Python list of elements as a string --- + std::stringstream elements_list_ss; + elements_list_ss << "["; + if (!elts.empty()) { + for (size_t i = 0; i < elts.size() - 1; ++i) { + elements_list_ss << elts[i].GeoId << ", " << elts[i].posIdAsInt() << ", "; + } + elements_list_ss << elts.back().GeoId << ", " << elts.back().posIdAsInt(); + } + elements_list_ss << "]"; + std::string elements_list_string = elements_list_ss.str(); + + // --- 5. Add the appropriate constraint via Python command --- + if (constraintType == "Group") { + Gui::cmdAppObjectArgs( + Obj, + "addConstraint(Sketcher.Constraint('Group', %s))", + elements_list_string.c_str()); + } + else if (constraintType == "Text") { + std::string escaped_text = escapeForPython(text); + std::string escaped_font = escapeForPython(font); + Gui::cmdAppObjectArgs( + Obj, + "addConstraint(Sketcher.Constraint('Text', %s, '%s', '%s', %s))", + elements_list_string.c_str(), + escaped_text.c_str(), + escaped_font.c_str(), + isTextHeight ? "True" : "False"); + } + else { + Base::Console().error("Unsupported list constraint type: %s\n", constraintType.c_str()); + return false; + } + + // We remove the internal alignment of the geometries that were grouped. + std::sort(geoIdsWithInternalGeos.begin(), geoIdsWithInternalGeos.end(), std::greater<>()); + for (auto& geoId : geoIdsWithInternalGeos) { + Obj->deleteUnusedInternalGeometry(geoId); + } + + return true; +} + +bool CmdSketcherConstrainGroup::isActive() +{ + return isCreateConstraintActive(getActiveGuiDocument()); +} + // ====================================================================================== DEF_STD_CMD_A(CmdSketcherChangeDimensionConstraint) @@ -10765,6 +11012,7 @@ void CreateSketcherCommandsConstraints() rcCmdMgr.addCommand(new CmdSketcherConstrainPointOnObject()); rcCmdMgr.addCommand(new CmdSketcherConstrainSymmetric()); rcCmdMgr.addCommand(new CmdSketcherConstrainSnellsLaw()); + rcCmdMgr.addCommand(new CmdSketcherConstrainGroup()); rcCmdMgr.addCommand(new CmdSketcherChangeDimensionConstraint()); rcCmdMgr.addCommand(new CmdSketcherToggleDrivingConstraint()); rcCmdMgr.addCommand(new CmdSketcherToggleActiveConstraint()); diff --git a/src/Mod/Sketcher/Gui/CommandConstraints.h b/src/Mod/Sketcher/Gui/CommandConstraints.h index 4d83b05958..8f8eefe26e 100644 --- a/src/Mod/Sketcher/Gui/CommandConstraints.h +++ b/src/Mod/Sketcher/Gui/CommandConstraints.h @@ -137,4 +137,16 @@ void doEndpointToEdgeTangency( /// notifications void notifyConstraintSubstitutions(const QString& message); + +bool addListConstraint( + Sketcher::SketchObject* Obj, + std::vector& elts, + const std::string& constraintType, + Base::Vector2d frame_p1 = Base::Vector2d(), + Base::Vector2d frame_p2 = Base::Vector2d(), + bool isTextHeight = true, + const std::string& text = "", + const std::string& font = "" +); + } // namespace SketcherGui diff --git a/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp b/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp index 74e27e6f78..46c7e24415 100644 --- a/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp +++ b/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp @@ -70,6 +70,7 @@ #include "DrawSketchHandlerRectangle.h" #include "DrawSketchHandlerSlot.h" #include "DrawSketchHandlerSplitting.h" +#include "DrawSketchHandlerText.h" #include "DrawSketchHandlerTrimming.h" @@ -1356,6 +1357,43 @@ public: } }; +// Text ================================================================ + +DEF_STD_CMD_AU(CmdSketcherCreateText) + +CmdSketcherCreateText::CmdSketcherCreateText() + : Command("Sketcher_CreateText") +{ + sAppModule = "Sketcher"; + sGroup = "Sketcher"; + sMenuText = QT_TR_NOOP("Text"); + sToolTipText = QT_TR_NOOP( + "Creates text geometries controlled by a Text constraint.\n" + "To Edit: Double-click the Text constraint to change the text content and font.\n" + "To Position/Size: Apply constraints to the group's construction line.\n" + "Note: While the Text constraint is active, any constraints applied directly to the text " + "geometries will be ignored.\n" + ); + sWhatsThis = "Sketcher_CreateText"; + sStatusTip = sToolTipText; + sPixmap = "Sketcher_CreateText"; + sAccel = "G, T"; + eType = ForEdit; +} + +CONSTRUCTION_UPDATE_ACTION(CmdSketcherCreateText, "Sketcher_CreateText") + +void CmdSketcherCreateText::activated(int iMsg) +{ + Q_UNUSED(iMsg); + ActivateHandler(getActiveGuiDocument(), std::make_unique()); +} + +bool CmdSketcherCreateText::isActive() +{ + return isCommandActive(getActiveGuiDocument()); +} + // B-spline ================================================================ DEF_STD_CMD_AU(CmdSketcherCreateBSpline) @@ -1936,6 +1974,7 @@ void CreateSketcherCommandsCreateGeo() rcCmdMgr.addCommand(new CmdSketcherCreateRegularPolygon()); rcCmdMgr.addCommand(new CmdSketcherCreateSlot()); rcCmdMgr.addCommand(new CmdSketcherCreateArcSlot()); + rcCmdMgr.addCommand(new CmdSketcherCreateText()); rcCmdMgr.addCommand(new CmdSketcherCreateFillet()); rcCmdMgr.addCommand(new CmdSketcherCreateChamfer()); // rcCmdMgr.addCommand(new CmdSketcherCreateText()); diff --git a/src/Mod/Sketcher/Gui/ConstraintFilters.h b/src/Mod/Sketcher/Gui/ConstraintFilters.h index 366465d4f7..d83c2a73be 100644 --- a/src/Mod/Sketcher/Gui/ConstraintFilters.h +++ b/src/Mod/Sketcher/Gui/ConstraintFilters.h @@ -54,18 +54,20 @@ enum class FilterValue Equality = 9, Symmetric = 10, Block = 11, - InternalAlignment = 12, - Datums = 13, - HorizontalDistance = 14, - VerticalDistance = 15, - Distance = 16, - Radius = 17, - Weight = 18, - Diameter = 19, - Angle = 20, - SnellsLaw = 21, - Named = 22, - NonDriving = 23, + Group = 12, + Text = 13, + InternalAlignment = 14, + Datums = 15, + HorizontalDistance = 16, + VerticalDistance = 17, + Distance = 18, + Radius = 19, + Weight = 20, + Diameter = 21, + Angle = 22, + SnellsLaw = 23, + Named = 24, + NonDriving = 25, NumFilterValue // SpecialFilterValue shall start at the same index as this }; @@ -75,8 +77,8 @@ constexpr auto FilterValueLength = static_cast filterAggregates { FilterValue::Equality, FilterValue::Symmetric, FilterValue::Block, + FilterValue::Group, + FilterValue::Text, FilterValue::Datums, FilterValue::Distance, FilterValue::HorizontalDistance, @@ -152,6 +156,8 @@ constexpr std::array filterAggregates { FilterValue::Equality, FilterValue::Symmetric, FilterValue::Block, + FilterValue::Group, + FilterValue::Text, FilterValue::InternalAlignment ), // Geometric = All others not being datums (1) @@ -165,6 +171,8 @@ constexpr std::array filterAggregates { buildBitset(FilterValue::Equality), // Equality = Just this (9) buildBitset(FilterValue::Symmetric), // Symmetric = Just this (10) buildBitset(FilterValue::Block), // Block = Just this (11) + buildBitset(FilterValue::Group), // Group = Just this (11) + buildBitset(FilterValue::Text), // Text = Just this (11) buildBitset(FilterValue::InternalAlignment), // InternalAlignment = Just this (12) buildBitset( diff --git a/src/Mod/Sketcher/Gui/DrawSketchDefaultWidgetController.h b/src/Mod/Sketcher/Gui/DrawSketchDefaultWidgetController.h index 357f7719af..f5674dadde 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchDefaultWidgetController.h +++ b/src/Mod/Sketcher/Gui/DrawSketchDefaultWidgetController.h @@ -52,6 +52,12 @@ class WidgetComboboxes: public ControlAmount { }; +/** @brief Type encapsulating the number of line edits in the widget*/ +template // Initial sizes for each mode +class WidgetLineEdits: public ControlAmount +{ +}; + namespace sp = std::placeholders; /** @brief Class defining a handler controller making use of parameters provided by a widget of type @@ -74,6 +80,7 @@ template< typename WidgetParametersT, // The number of parameter spinboxes in the default widget typename WidgetCheckboxesT, // The number of checkboxes in the default widget typename WidgetComboboxesT, // The number of comboboxes in the default widget + typename WidgetLineEditsT, // The number of line edits in the default widget typename ConstructionMethodT = ConstructionMethods::DefaultConstructionMethod, bool PFirstComboboxIsConstructionMethod = false> // The handler template or class having this // as inner class @@ -91,6 +98,7 @@ private: int nParameter = WidgetParametersT::defaultMethodSize(); int nCheckbox = WidgetCheckboxesT::defaultMethodSize(); int nCombobox = WidgetComboboxesT::defaultMethodSize(); + int nLineEdit = WidgetLineEditsT::defaultMethodSize(); SketcherToolDefaultWidget* toolWidget; @@ -100,12 +108,14 @@ private: Connection connectionParameterValueChanged; Connection connectionCheckboxCheckedChanged; Connection connectionComboboxSelectionChanged; + Connection connectionLineEditTextChanged; /** @name Named indices for controls of the default widget (SketcherToolDefaultWidget) */ //@{ using WParameter = SketcherToolDefaultWidget::Parameter; using WCheckbox = SketcherToolDefaultWidget::Checkbox; using WCombobox = SketcherToolDefaultWidget::Combobox; + using WLineEdit = SketcherToolDefaultWidget::LineEdit; //@} using SelectMode = SelectModeT; @@ -128,6 +138,7 @@ public: connectionParameterValueChanged.disconnect(); connectionCheckboxCheckedChanged.disconnect(); connectionComboboxSelectionChanged.disconnect(); + connectionLineEditTextChanged.disconnect(); } /** @name functions NOT intended for specialisation offering specialisation interface for @@ -179,6 +190,24 @@ public: } //@} + /** boost slot triggering when a line edit has changed in the widget + * It is intended to remote control the DrawSketchDefaultWidgetHandler + */ + void lineEditTextChanged(int lineeditindex, const QString& value) + { + adaptDrawingToLineEditTextChange(lineeditindex, value); // specialisation interface + + // Temporarily disable auto-passing focus to OVP. + // This prevents the focus from being stolen from the LineEdit when the text changes. + ControllerBase::focusAutoPassing = false; + + ControllerBase::finishControlsChanged(); + + // Restore the default behavior. + ControllerBase::focusAutoPassing = true; + } + //@} + /** @name Specialisation Interface */ /** These functions offer a specialisation interface. Non-virtual functions are specific to * this controller. Virtual functions may depend on input from a derived controller, and thus @@ -202,8 +231,6 @@ public: /// Change DSH to reflect a comboBox changed in the widget void adaptDrawingToComboboxChange(int comboboxindex, [[maybe_unused]] int value) { - Q_UNUSED(comboboxindex); - if constexpr (PFirstComboboxIsConstructionMethod == true) { if (comboboxindex == WCombobox::FirstCombo && handler->ConstructionMethodsCount() > 1) { @@ -212,6 +239,13 @@ public: } } + /// Change DSH to reflect a line edit changed in the widget + void adaptDrawingToLineEditTextChange(int lineeditindex, const QString& value) + { + Q_UNUSED(lineeditindex); + Q_UNUSED(value); + } + /// function to create constraints based on widget information. void addConstraints() override {} @@ -303,14 +337,19 @@ protected: void setFocusToParameter(unsigned int parameterindex) { // To be able to cycle through OVP and widget, we use a parameter index that goes from - // 0 to (onViewParameters.size() + nParameter) + // 0 to (onViewParameters.size() + nParameter + nLineEdit) if (!ControllerBase::setFocusToOnViewParameter(parameterindex)) { - parameterindex = parameterindex - ControllerBase::onViewParameters.size(); + unsigned int widgetIndex = parameterindex - ControllerBase::onViewParameters.size(); - if (parameterindex < static_cast(nParameter)) { - toolWidget->setParameterFocus(parameterindex); - ControllerBase::parameterWithFocus = ControllerBase::onViewParameters.size() - + parameterindex; + if (widgetIndex < static_cast(nParameter)) { + toolWidget->setParameterFocus(widgetIndex); + ControllerBase::parameterWithFocus = parameterindex; + } + // Check if the index corresponds to a LineEdit + else if (widgetIndex < static_cast(nParameter + nLineEdit)) { + unsigned int lineEditIndex = widgetIndex - nParameter; + toolWidget->setLineEditFocus(lineEditIndex); + ControllerBase::parameterWithFocus = parameterindex; } } } @@ -320,7 +359,8 @@ protected: { unsigned int index = ControllerBase::parameterWithFocus + 1; - if (index >= ControllerBase::onViewParameters.size() + nParameter) { + // The total number of focusable items now includes LineEdits. + if (index >= ControllerBase::onViewParameters.size() + nParameter + nLineEdit) { index = 0; } @@ -333,9 +373,21 @@ protected: } idx++; } + // Check SpinBoxes if (idx < ControllerBase::onViewParameters.size() + nParameter) { - setFocusToParameter(idx); - return true; + if (nParameter > 0) { + setFocusToParameter(idx); + return true; + } + // If no spinboxes, update index to check line edits + idx = ControllerBase::onViewParameters.size() + nParameter; + } + // Check LineEdits + if (idx < ControllerBase::onViewParameters.size() + nParameter + nLineEdit) { + if (nLineEdit > 0) { + setFocusToParameter(idx); + return true; + } } return false; }; @@ -372,6 +424,10 @@ private: connectionComboboxSelectionChanged = toolWidget->registerComboboxSelectionChanged( std::bind(&DrawSketchDefaultWidgetController::comboboxSelectionChanged, this, sp::_1, sp::_2) ); + + connectionLineEditTextChanged = toolWidget->registerLineEditTextChanged( + std::bind(&DrawSketchDefaultWidgetController::lineEditTextChanged, this, sp::_1, sp::_2) + ); } /// Resets the widget @@ -383,14 +439,17 @@ private: fastsignals::shared_connection_block parameter_block(connectionParameterValueChanged); fastsignals::shared_connection_block checkbox_block(connectionCheckboxCheckedChanged); fastsignals::shared_connection_block combobox_block(connectionComboboxSelectionChanged); + fastsignals::shared_connection_block lineedit_block(connectionLineEditTextChanged); nParameter = WidgetParametersT::size(handler->constructionMethod()); nCheckbox = WidgetCheckboxesT::size(handler->constructionMethod()); nCombobox = WidgetComboboxesT::size(handler->constructionMethod()); + nLineEdit = WidgetLineEditsT::size(handler->constructionMethod()); toolWidget->initNParameters(nParameter, ControllerBase::getKeyManager()); toolWidget->initNCheckboxes(nCheckbox); toolWidget->initNComboboxes(nCombobox); + toolWidget->initNLineEdits(nLineEdit); configureToolWidget(); diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h index c9ab4740ef..1b3e0086ec 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerArc.h @@ -62,7 +62,8 @@ using DSHArcController = DrawSketchDefaultWidgetController< /*OnViewParametersT =*/OnViewParameters<5, 6>, // NOLINT /*WidgetParametersT =*/WidgetParameters<0, 0>, // NOLINT /*WidgetCheckboxesT =*/WidgetCheckboxes<0, 0>, // NOLINT - /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, // NOLINT + /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, // NOLINT, + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0>, ConstructionMethods::CircleEllipseConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h index 3cb5036a76..94c391edba 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerArcSlot.h @@ -68,6 +68,7 @@ using DSHArcSlotController = DrawSketchDefaultWidgetController< /*WidgetParametersT =*/WidgetParameters<0, 0>, // NOLINT /*WidgetCheckboxesT =*/WidgetCheckboxes<0, 0>, // NOLINT /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0>, ConstructionMethods::ArcSlotConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h index e2cde8f747..6aa2b9bcce 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerBSpline.h @@ -65,6 +65,7 @@ using DSHBSplineController = DrawSketchDefaultWidgetController< /*WidgetParametersT =*/WidgetParameters<1, 1>, // NOLINT /*WidgetCheckboxesT =*/WidgetCheckboxes<1, 1>, // NOLINT /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0>, ConstructionMethods::BSplineConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h index f0ebb228a4..6e0d47934b 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerCircle.h @@ -57,6 +57,7 @@ using DSHCircleController = DrawSketchDefaultWidgetController< /*WidgetParametersT =*/WidgetParameters<0, 0>, // NOLINT /*WidgetCheckboxesT =*/WidgetCheckboxes<0, 0>, // NOLINT /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0>, ConstructionMethods::CircleEllipseConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h index b1b209ba58..936d81c4dd 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h @@ -61,6 +61,7 @@ using DSHEllipseController = DrawSketchDefaultWidgetController< /*WidgetParametersT =*/WidgetParameters<0, 0>, // NOLINT /*WidgetCheckboxesT =*/WidgetCheckboxes<0, 0>, // NOLINT /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0>, ConstructionMethods::CircleEllipseConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerFillet.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerFillet.h index 4b35166046..433d187baf 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerFillet.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerFillet.h @@ -112,6 +112,7 @@ using DSHFilletController = DrawSketchDefaultWidgetController< /*WidgetParametersT =*/WidgetParameters<0, 0>, // NOLINT /*WidgetCheckboxesT =*/WidgetCheckboxes<1, 1>, // NOLINT /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0>, ConstructionMethods::FilletConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h index 4c21b958fc..1fe60e1504 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerLine.h @@ -70,6 +70,7 @@ using DSHLineController = DrawSketchDefaultWidgetController< /*WidgetParametersT =*/WidgetParameters<0, 0, 0>, // NOLINT /*WidgetCheckboxesT =*/WidgetCheckboxes<0, 0, 0>, // NOLINT /*WidgetComboboxesT =*/WidgetComboboxes<1, 1, 1>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0, 0>, ConstructionMethods::LineConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerLineSet.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerLineSet.h index d6438f276c..1f941508da 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerLineSet.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerLineSet.h @@ -877,4 +877,5 @@ protected: dirVec.Normalize(); } }; + } // namespace SketcherGui diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h index dece54d653..201dfd87e2 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerOffset.h @@ -105,6 +105,7 @@ using DSHOffsetController = DrawSketchDefaultWidgetController< /*WidgetParametersT =*/WidgetParameters<0, 0>, /*WidgetCheckboxesT =*/WidgetCheckboxes<2, 2>, /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0>, ConstructionMethods::OffsetConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h index c34d56342a..06fe5a03c1 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h @@ -57,7 +57,8 @@ using DSHPolygonController = DrawSketchDefaultWidgetController< /*OnViewParametersT =*/OnViewParameters<4>, /*WidgetParametersT =*/WidgetParameters<1>, /*WidgetCheckboxesT =*/WidgetCheckboxes<0>, - /*WidgetComboboxesT =*/WidgetComboboxes<0>>; + /*WidgetComboboxesT =*/WidgetComboboxes<0>, + /*WidgetLineEditsT =*/WidgetLineEdits<0>>; using DSHPolygonControllerBase = DSHPolygonController::ControllerBase; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h index 419e9f4dcb..92f4a32988 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerRectangle.h @@ -69,6 +69,7 @@ using DSHRectangleController = DrawSketchDefaultWidgetController< /*WidgetParametersT =*/WidgetParameters<0, 0, 0, 0>, // NOLINT /*WidgetCheckboxesT =*/WidgetCheckboxes<2, 2, 2, 2>, // NOLINT /*WidgetComboboxesT =*/WidgetComboboxes<1, 1, 1, 1>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0, 0, 0, 0>, // NOLINT ConstructionMethods::RectangleConstructionMethod, /*bool PFirstComboboxIsConstructionMethod =*/true>; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h index 005e738238..e346acebf0 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerRotate.h @@ -53,7 +53,8 @@ using DSHRotateController = DrawSketchDefaultWidgetController< /*OnViewParametersT =*/OnViewParameters<4>, /*WidgetParametersT =*/WidgetParameters<1>, /*WidgetCheckboxesT =*/WidgetCheckboxes<1>, - /*WidgetComboboxesT =*/WidgetComboboxes<0>>; + /*WidgetComboboxesT =*/WidgetComboboxes<0>, + /*WidgetLineEditsT =*/WidgetLineEdits<0>>; using DSHRotateControllerBase = DSHRotateController::ControllerBase; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h index b0acd825ea..4f0550be9a 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerScale.h @@ -60,7 +60,8 @@ using DSHScaleController = DrawSketchDefaultWidgetController< /*OnViewParametersT =*/OnViewParameters<3>, /*WidgetParametersT =*/WidgetParameters<0>, /*WidgetCheckboxesT =*/WidgetCheckboxes<1>, - /*WidgetComboboxesT =*/WidgetComboboxes<0>>; + /*WidgetComboboxesT =*/WidgetComboboxes<0>, + /*WidgetLineEditsT =*/WidgetLineEdits<0>>; using DSHScaleControllerBase = DSHScaleController::ControllerBase; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h index 7c58b6e276..6c2f7bcc0d 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerSlot.h @@ -55,10 +55,11 @@ using DSHSlotController = DrawSketchDefaultWidgetController< DrawSketchHandlerSlot, StateMachines::ThreeSeekEnd, /*PAutoConstraintSize =*/2, - /*OnViewParametersT =*/OnViewParameters<5>, // NOLINT - /*WidgetParametersT =*/WidgetParameters<0>, // NOLINT - /*WidgetCheckboxesT =*/WidgetCheckboxes<0>, // NOLINT - /*WidgetComboboxesT =*/WidgetComboboxes<0>>; // NOLINT + /*OnViewParametersT =*/OnViewParameters<5>, // NOLINT + /*WidgetParametersT =*/WidgetParameters<0>, // NOLINT + /*WidgetCheckboxesT =*/WidgetCheckboxes<0>, // NOLINT + /*WidgetComboboxesT =*/WidgetComboboxes<0>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0>>; // NOLINT using DSHSlotControllerBase = DSHSlotController::ControllerBase; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h index bb05b471e8..6066093647 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerSymmetry.h @@ -54,10 +54,11 @@ using DSHSymmetryController = DrawSketchDefaultWidgetController< DrawSketchHandlerSymmetry, StateMachines::OneSeekEnd, /*PAutoConstraintSize =*/0, - /*OnViewParametersT =*/OnViewParameters<0>, - /*WidgetParametersT =*/WidgetParameters<0>, - /*WidgetCheckboxesT =*/WidgetCheckboxes<2>, - /*WidgetComboboxesT =*/WidgetComboboxes<0>>; + /*OnViewParametersT =*/OnViewParameters<0>, // NOLINT + /*WidgetParametersT =*/WidgetParameters<0>, // NOLINT + /*WidgetCheckboxesT =*/WidgetCheckboxes<2>, // NOLINT + /*WidgetComboboxesT =*/WidgetComboboxes<0>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0>>; // NOLINT using DSHSymmetryControllerBase = DSHSymmetryController::ControllerBase; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h new file mode 100644 index 0000000000..54ea9f2d11 --- /dev/null +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h @@ -0,0 +1,750 @@ +// SPDX - License - Identifier: LGPL - 2.1 - or -later +/**************************************************************************** + * * + * Copyright (c) 2025 Pierre-Louis Boyer * + * * + * 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 * + * . * + * * + ***************************************************************************/ + + +#ifndef SKETCHERGUI_DrawSketchHandlerText_H +#define SKETCHERGUI_DrawSketchHandlerText_H + +#include + +#include +#include +#include +#include +#include + +#include + +#include "DrawSketchDefaultWidgetController.h" +#include "DrawSketchControllableHandler.h" + +#include "GeometryCreationMode.h" +#include "Utils.h" +#include "CommandConstraints.h" + +#include +#include + +namespace SketcherGui +{ + +class DrawSketchHandlerText; + +namespace ConstructionMethods +{ + +enum class TextConstructionMethod +{ + Width, + Height, + End // Must be the last one +}; + +} // namespace ConstructionMethods + +using DSHTextController = DrawSketchDefaultWidgetController< + DrawSketchHandlerText, + /*SelectModeT*/ StateMachines::TwoSeekEnd, + /*PAutoConstraintSize =*/2, + /*OnViewParametersT =*/OnViewParameters<4, 4>, // NOLINT + /*WidgetParametersT =*/WidgetParameters<0, 0>, // NOLINT + /*WidgetCheckboxesT =*/WidgetCheckboxes<0, 0>, // NOLINT + /*WidgetComboboxesT =*/WidgetComboboxes<2, 2>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<1, 1>, // NOLINT + ConstructionMethods::TextConstructionMethod, + /*bool PFirstComboboxIsConstructionMethod =*/true>; + +using DSHTextControllerBase = DSHTextController::ControllerBase; + +using DrawSketchHandlerTextBase = DrawSketchControllableHandler; + + +class DrawSketchHandlerText: public DrawSketchHandlerTextBase +{ + friend DSHTextController; + friend DSHTextControllerBase; + +public: + explicit DrawSketchHandlerText(ConstructionMethod constrMethod = ConstructionMethod::Width) + : DrawSketchHandlerTextBase(constrMethod) + , length(0.0) + , handleId(0) + , text("AstoCAD") + , font("") + , cachedTextName("") + , cachedFontName("") + , cachedBaseShapes({}) {}; + ~DrawSketchHandlerText() override = default; + +private: + void updateDataAndDrawToPosition(Base::Vector2d onSketchPos) override + { + switch (state()) { + case SelectMode::SeekFirst: { + toolWidgetManager.drawPositionAtCursor(onSketchPos); + + startPoint = onSketchPos; + + seekAndRenderAutoConstraint(sugConstraints[0], onSketchPos, Base::Vector2d(0.f, 0.f)); + } break; + case SelectMode::SeekSecond: { + toolWidgetManager.drawDirectionAtCursor(onSketchPos, startPoint); + + endPoint = onSketchPos; + + try { + CreateAndDrawShapeGeometry(); + } + catch (const Base::ValueError&) { + } // equal points while hovering raise an objection that can be safely ignored + + seekAndRenderAutoConstraint(sugConstraints[1], onSketchPos, onSketchPos - startPoint); + } break; + default: + break; + } + } + + void executeCommands() override + { + try { + int firstCurve = getHighestCurveIndex() + 1; + + createShape(false); + + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add sketch Text")); + + commandAddShapeGeometryAndConstraints(); + + handleId = getHighestCurveIndex() + 1; // line is not added yet + + std::vector elts; + for (int i = firstCurve; i < handleId; ++i) { + elts.push_back(GeoElementId(i)); + } + bool isHeight = constructionMethod() == ConstructionMethod::Height; + if (!addListConstraint(getSketchObject(), elts, "Text", startPoint, endPoint, isHeight, text, font)) { + Gui::Command::abortCommand(); + return; + } + + Gui::Command::commitCommand(); + } + catch (const Base::Exception&) { + Gui::NotifyError( + sketchgui, + QT_TRANSLATE_NOOP("Notifications", "Error"), + QT_TRANSLATE_NOOP("Notifications", "Failed to add text") + ); + + Gui::Command::abortCommand(); + THROWM( + Base::RuntimeError, + QT_TRANSLATE_NOOP( + "Notifications", + "Tool execution aborted" + ) "\n" + ) // This prevents constraints from being + // applied on non existing geometry + } + } + + void generateAutoConstraints() override + { + // Generate temporary autoconstraints (but do not actually add them to the sketch) + if (avoidRedundants) { + removeRedundantHorizontalVertical(getSketchObject(), sugConstraints[0], sugConstraints[1]); + } + + auto& ac1 = sugConstraints[0]; + auto& ac2 = sugConstraints[1]; + + generateAutoConstraintsOnElement(ac1, handleId, Sketcher::PointPos::start); + generateAutoConstraintsOnElement(ac2, handleId, Sketcher::PointPos::end); + + // Ensure temporary autoconstraints do not generate a redundancy and that the geometry + // parameters are accurate This is particularly important for adding widget mandated + // constraints. + removeRedundantAutoConstraints(); + } + + void createAutoConstraints() override + { + // execute python command to create autoconstraints + createGeneratedAutoConstraints(true); + + sugConstraints[0].clear(); + sugConstraints[1].clear(); + } + + std::string getToolName() const override + { + return "DSH_Text"; + } + + QString getCrosshairCursorSVGName() const override + { + return QStringLiteral("Sketcher_Pointer_Text.svg"); + } + + std::unique_ptr createWidget() const override + { + return std::make_unique(); + } + + bool isWidgetVisible() const override + { + return true; // Text tool must show the line edit to make sense + }; + + QPixmap getToolIcon() const override + { + return Gui::BitmapFactory().pixmap("Sketcher_CreateText"); + } + + QString getToolWidgetText() const override + { + return QString(QObject::tr("Text parameters")); + } + + bool canGoToNextMode() override + { + if (state() == SelectMode::SeekSecond && length < Precision::Confusion()) { + // Prevent validation of null Text. + return false; + } + return true; + } + + void angleSnappingControl() override + { + if (state() == SelectMode::SeekSecond) { + setAngleSnapping(true, startPoint); + } + + else { + setAngleSnapping(false); + } + } + +private: + QMap fontPathMap; + Base::Vector2d startPoint, endPoint; + double length; + int handleId; + + std::string text; + std::string font; + std::string cachedTextName; + std::string cachedFontName; + std::vector cachedBaseShapes; + + void createShape(bool onlyeditoutline) override + { + ShapeGeometry.clear(); + + Base::Vector2d vecL = endPoint - startPoint; + length = vecL.Length(); + if (length < Precision::Confusion()) { + return; + } + + // 1. Check if the cache is valid. If the user selected a new file, + // or if the cache is empty, we need to re-load from the SVG. + if (cachedTextName != text || cachedFontName != font || cachedBaseShapes.empty()) { + if (!font.empty()) { + cachedTextName = text; + cachedFontName = font; + // This is the one-time slow operation to get the template shapes. + cachedBaseShapes = Part::makeTextWires(text, font); + } + else { + cachedBaseShapes.clear(); + } + } + + // 2. Call the generic helper to transform and create the final geometry. + transformAndConvertToGeometry( + ShapeGeometry, + cachedBaseShapes, + toVector3d(startPoint), + toVector3d(endPoint), + constructionMethod() == ConstructionMethod::Height + ); + + // 3. Set construction mode on the newly created geometry + if (isConstructionMode() && !onlyeditoutline) { + for (auto& geo : ShapeGeometry) { + Sketcher::GeometryFacade::setConstruction(geo.get(), true); + } + } + } + + std::list getToolHints() const override + { + return lookupTextHints(static_cast(constructionMethod()), static_cast(state())); + } + + struct HintEntry + { + int constructionMethod; + int state; + std::list hints; + }; + + using HintTable = std::vector; + + static Gui::InputHint switchModeHint(); + static HintTable getTextHintTable(); + static std::list lookupTextHints(int method, int state); +}; + +template<> +auto DSHTextControllerBase::getState(int labelindex) const +{ + switch (labelindex) { + case OnViewParameter::First: + case OnViewParameter::Second: + return SelectMode::SeekFirst; + break; + case OnViewParameter::Third: + case OnViewParameter::Fourth: + return SelectMode::SeekSecond; + break; + default: + THROWM(Base::ValueError, "Label index without an associated machine state") + } +} + +template<> +void DSHTextController::configureToolWidget() +{ + if (!init) { // Code to be executed only upon initialisation + QStringList names = { + QApplication::translate("TaskSketcherTool_c1_text", "Width"), + QApplication::translate("TaskSketcherTool_c1_text", "Height") + }; + toolWidget->setComboboxElements(WCombobox::FirstCombo, names); + + toolWidget->setLineEditLabel( + WLineEdit::FirstEdit, + QApplication::translate("TaskSketcherTool_Text", "Text") + ); + toolWidget->setLineEditText(WLineEdit::FirstEdit, QString::fromStdString(handler->text)); + + toolWidget->setComboboxLabel( + WCombobox::SecondCombo, + QApplication::translate("TaskSketcherTool_Text", "Font") + ); + + // 1. Scan for font files and store the map + handler->fontPathMap = findAvailableFontFiles(); + + // 2. Populate combobox with friendly names (the keys of the map) + QStringList fontNames = handler->fontPathMap.keys(); + fontNames.sort(Qt::CaseInsensitive); + toolWidget->setComboboxElements(WCombobox::SecondCombo, fontNames); + + // 3. Set a sensible default font + QString defaultFontName; + if (fontNames.contains(QString::fromUtf8("DejaVu Sans"), Qt::CaseInsensitive)) { + defaultFontName = QString::fromUtf8("DejaVu Sans"); + } + else if (fontNames.contains(QString::fromUtf8("Arial"), Qt::CaseInsensitive)) { + defaultFontName = QString::fromUtf8("Arial"); + } + else if (!fontNames.isEmpty()) { + defaultFontName = fontNames.first(); + } + + if (!defaultFontName.isEmpty()) { + // Find the actual case-sensitive key + for (const auto& key : fontNames) { + if (key.compare(defaultFontName, Qt::CaseInsensitive) == 0) { + handler->font = handler->fontPathMap.value(key).toStdString(); + toolWidget->setComboboxCurrentText(WCombobox::SecondCombo, key); + break; + } + } + } + + onViewParameters[OnViewParameter::First]->setLabelType(Gui::SoDatumLabel::DISTANCEX); + onViewParameters[OnViewParameter::Second]->setLabelType(Gui::SoDatumLabel::DISTANCEY); + + onViewParameters[OnViewParameter::Third]->setLabelType( + Gui::SoDatumLabel::DISTANCE, + Gui::EditableDatumLabel::Function::Dimensioning + ); + onViewParameters[OnViewParameter::Fourth]->setLabelType( + Gui::SoDatumLabel::ANGLE, + Gui::EditableDatumLabel::Function::Dimensioning + ); + } + + toolWidget->setLineEditText( + SketcherToolDefaultWidget::LineEdit::FirstEdit, + QString::fromStdString(handler->text) + ); +} + +template<> +void DSHTextController::adaptDrawingToLineEditTextChange(int lineeditindex, const QString& value) +{ + if (lineeditindex == WLineEdit::FirstEdit) { + handler->text = value.toStdString(); + // The redraw is handled by the controller's finishControlsChanged() + } +} + +template<> +void DSHTextController::adaptDrawingToComboboxChange(int comboboxindex, int value) +{ + if (comboboxindex == WCombobox::FirstCombo) { + handler->setConstructionMethod(static_cast(value)); + } + else if (comboboxindex == WCombobox::SecondCombo) { + // Get the selected friendly name + QString fontName = toolWidget->getComboboxCurrentText(WCombobox::SecondCombo); + // Look up the full path in our map and update the handler + if (handler->fontPathMap.contains(fontName)) { + handler->font = handler->fontPathMap.value(fontName).toStdString(); + } + // The redraw is handled by the controller's finishControlsChanged() + } +} + +template<> +void DSHTextControllerBase::doEnforceControlParameters(Base::Vector2d& onSketchPos) +{ + switch (handler->state()) { + case SelectMode::SeekFirst: { + auto& firstParam = onViewParameters[OnViewParameter::First]; + auto& secondParam = onViewParameters[OnViewParameter::Second]; + + if (firstParam->isSet) { + onSketchPos.x = firstParam->getValue(); + } + + if (secondParam->isSet) { + onSketchPos.y = secondParam->getValue(); + } + } break; + case SelectMode::SeekSecond: { + auto& thirdParam = onViewParameters[OnViewParameter::Third]; + auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; + + Base::Vector2d dir = onSketchPos - handler->startPoint; + if (dir.Length() < Precision::Confusion()) { + dir.x = 1.0; // if direction null, default to (1,0) + } + double length = dir.Length(); + + if (thirdParam->isSet) { + length = thirdParam->getValue(); + if (length < Precision::Confusion()) { + unsetOnViewParameter(thirdParam.get()); + return; + } + + onSketchPos = handler->startPoint + length * dir.Normalize(); + } + + if (fourthParam->isSet) { + double angle = Base::toRadians(fourthParam->getValue()); + if (handler->constructionMethod() == ConstructionMethod::Height) { + angle += M_PI * 0.5; + } + Base::Vector2d dir(cos(angle), sin(angle)); + onSketchPos.ProjectToLine(onSketchPos - handler->startPoint, dir); + onSketchPos += handler->startPoint; + } + + if (thirdParam->isSet && fourthParam->isSet + && (onSketchPos - handler->startPoint).Length() < Precision::Confusion()) { + unsetOnViewParameter(thirdParam.get()); + unsetOnViewParameter(fourthParam.get()); + } + } break; + default: + break; + } +} + +template<> +void DSHTextController::adaptParameters(Base::Vector2d onSketchPos) +{ + switch (handler->state()) { + case SelectMode::SeekFirst: { + auto& firstParam = onViewParameters[OnViewParameter::First]; + auto& secondParam = onViewParameters[OnViewParameter::Second]; + + if (!firstParam->isSet) { + setOnViewParameterValue(OnViewParameter::First, onSketchPos.x); + } + + if (!secondParam->isSet) { + setOnViewParameterValue(OnViewParameter::Second, onSketchPos.y); + } + + bool sameSign = onSketchPos.x * onSketchPos.y > 0.; + firstParam->setLabelAutoDistanceReverse(!sameSign); + secondParam->setLabelAutoDistanceReverse(sameSign); + firstParam->setPoints(Base::Vector3d(), toVector3d(onSketchPos)); + secondParam->setPoints(Base::Vector3d(), toVector3d(onSketchPos)); + } break; + case SelectMode::SeekSecond: { + auto& thirdParam = onViewParameters[OnViewParameter::Third]; + auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; + + Base::Vector3d start = toVector3d(handler->startPoint); + Base::Vector3d end = toVector3d(handler->endPoint); + Base::Vector3d vec = end - start; + + if (!thirdParam->isSet) { + setOnViewParameterValue(OnViewParameter::Third, vec.Length()); + } + + double range; + if (handler->constructionMethod() == ConstructionMethod::Height) { + Base::Vector2d norm(vec.y, -vec.x); + Base::Vector2d textAlignPoint = handler->startPoint + norm; + range = (textAlignPoint - handler->startPoint).Angle(); + } + else { + range = (handler->endPoint - handler->startPoint).Angle(); + } + + + if (!fourthParam->isSet) { + setOnViewParameterValue( + OnViewParameter::Fourth, + Base::toDegrees(range), + Base::Unit::Angle + ); + } + else if (vec.Length() > Precision::Confusion()) { + double ovpRange = Base::toRadians(fourthParam->getValue()); + if (fabs(range - ovpRange) > Precision::Confusion()) { + setOnViewParameterValue( + OnViewParameter::Fourth, + Base::toDegrees(range), + Base::Unit::Angle + ); + } + } + + thirdParam->setPoints(start, end); + fourthParam->setPoints(start, Base::Vector3d()); + fourthParam->setLabelRange(range); + } break; + default: + break; + } +} + +template<> +void DSHTextController::computeNextDrawSketchHandlerMode() +{ + switch (handler->state()) { + case SelectMode::SeekFirst: { + auto& firstParam = onViewParameters[OnViewParameter::First]; + auto& secondParam = onViewParameters[OnViewParameter::Second]; + + if (firstParam->isSet && secondParam->isSet) { + handler->setNextState(SelectMode::SeekSecond); + } + } break; + case SelectMode::SeekSecond: { + auto& thirdParam = onViewParameters[OnViewParameter::Third]; + auto& fourthParam = onViewParameters[OnViewParameter::Fourth]; + + if (thirdParam->hasFinishedEditing && fourthParam->hasFinishedEditing) { + handler->setNextState(SelectMode::End); + } + } break; + default: + break; + } +} + +template<> +void DSHTextController::addConstraints() +{ + App::DocumentObject* obj = handler->sketchgui->getObject(); + + int firstCurve = handler->handleId; + + auto x0 = onViewParameters[OnViewParameter::First]->getValue(); + auto y0 = onViewParameters[OnViewParameter::Second]->getValue(); + auto p3 = onViewParameters[OnViewParameter::Third]->getValue(); + auto p4 = onViewParameters[OnViewParameter::Fourth]->getValue(); + + auto x0set = onViewParameters[OnViewParameter::First]->isSet; + auto y0set = onViewParameters[OnViewParameter::Second]->isSet; + auto p3set = onViewParameters[OnViewParameter::Third]->isSet; + auto p4set = onViewParameters[OnViewParameter::Fourth]->isSet; + + using namespace Sketcher; + + auto constraintToOrigin = [&]() { + ConstraintToAttachment(GeoElementId(firstCurve, PointPos::start), GeoElementId::RtPnt, x0, obj); + }; + + auto constraintx0 = [&]() { + ConstraintToAttachment(GeoElementId(firstCurve, PointPos::start), GeoElementId::VAxis, x0, obj); + }; + + auto constrainty0 = [&]() { + ConstraintToAttachment(GeoElementId(firstCurve, PointPos::start), GeoElementId::HAxis, y0, obj); + }; + + auto constraintp3length = [&]() { + Gui::cmdAppObjectArgs( + obj, + "addConstraint(Sketcher.Constraint('Distance',%d,%f)) ", + firstCurve, + fabs(p3) + ); + }; + + auto constraintp4angle = [&]() { + double angle = Base::toRadians(p4); + if (handler->constructionMethod() == ConstructionMethod::Height) { + angle += M_PI * 0.5; + } + + ConstraintLineByAngle(firstCurve, angle, obj); + }; + + if (handler->AutoConstraints.empty()) { // No valid diagnosis. Every constraint can be added. + + if (x0set && y0set && x0 == 0. && y0 == 0.) { + constraintToOrigin(); + } + else { + if (x0set) { + constraintx0(); + } + + if (y0set) { + constrainty0(); + } + } + + if (p3set) { + constraintp3length(); + } + + if (p4set) { + constraintp4angle(); + } + } + else { // Valid diagnosis. Must check which constraints may be added. + auto startpointinfo = handler->getPointInfo(GeoElementId(firstCurve, PointPos::start)); + + if (x0set && startpointinfo.isXDoF()) { + constraintx0(); + + handler->diagnoseWithAutoConstraints(); // ensure we have recalculated parameters after + // each constraint addition + + startpointinfo = handler->getPointInfo( + GeoElementId(firstCurve, PointPos::start) + ); // get updated point position + } + + if (y0set && startpointinfo.isYDoF()) { + constrainty0(); + + handler->diagnoseWithAutoConstraints(); // ensure we have recalculated parameters after + // each constraint addition + + startpointinfo = handler->getPointInfo( + GeoElementId(firstCurve, PointPos::start) + ); // get updated point position + } + + auto endpointinfo = handler->getPointInfo(GeoElementId(firstCurve, PointPos::end)); + + int DoFs = startpointinfo.getDoFs(); + DoFs += endpointinfo.getDoFs(); + + if (p3set && DoFs > 0) { + constraintp3length(); + DoFs--; + } + + if (p4set && DoFs > 0) { + constraintp4angle(); + } + } +} + +Gui::InputHint DrawSketchHandlerText::switchModeHint() +{ + return {QObject::tr("%1 switch mode"), {Gui::InputHint::UserInput::KeyM}}; +} + +DrawSketchHandlerText::HintTable DrawSketchHandlerText::getTextHintTable() +{ + const auto switchHint = switchModeHint(); + return { + // Structure: {constructionMethod, state, {hints...}} + {static_cast(ConstructionMethod::Height), + 0, + {{QObject::tr("%1 pick bottom-left point"), {Gui::InputHint::UserInput::MouseLeft}}, + switchHint}}, + {static_cast(ConstructionMethod::Height), + 1, + {{QObject::tr("%1 pick top-left point"), {Gui::InputHint::UserInput::MouseLeft}}, + switchHint}}, + {static_cast(ConstructionMethod::Width), + 0, + {{QObject::tr("%1 pick bottom-left point"), {Gui::InputHint::UserInput::MouseLeft}}, + switchHint}}, + {static_cast(ConstructionMethod::Width), + 1, + {{QObject::tr("%1 pick bottom-right point"), {Gui::InputHint::UserInput::MouseLeft}}, + switchHint}} + }; +} + +std::list DrawSketchHandlerText::lookupTextHints(int method, int state) +{ + const auto TextHintTable = getTextHintTable(); + + auto it = std::find_if( + TextHintTable.begin(), + TextHintTable.end(), + [method, state](const HintEntry& entry) { + return entry.constructionMethod == method && entry.state == state; + } + ); + + return (it != TextHintTable.end()) ? it->hints : std::list {}; +} + +} // namespace SketcherGui + + +#endif // SKETCHERGUI_DrawSketchHandlerText_H diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h index ab54515084..8b58e4b24d 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerTranslate.h @@ -57,10 +57,11 @@ using DSHTranslateController = DrawSketchDefaultWidgetController< DrawSketchHandlerTranslate, StateMachines::ThreeSeekEnd, /*PAutoConstraintSize =*/0, - /*OnViewParametersT =*/OnViewParameters<6>, - /*WidgetParametersT =*/WidgetParameters<2>, - /*WidgetCheckboxesT =*/WidgetCheckboxes<1>, - /*WidgetComboboxesT =*/WidgetComboboxes<0>>; + /*OnViewParametersT =*/OnViewParameters<6>, // NOLINT + /*WidgetParametersT =*/WidgetParameters<2>, // NOLINT + /*WidgetCheckboxesT =*/WidgetCheckboxes<1>, // NOLINT + /*WidgetComboboxesT =*/WidgetComboboxes<0>, // NOLINT + /*WidgetLineEditsT =*/WidgetLineEdits<0>>; // NOLINT using DSHTranslateControllerBase = DSHTranslateController::ControllerBase; diff --git a/src/Mod/Sketcher/Gui/EditDatumDialog.cpp b/src/Mod/Sketcher/Gui/EditDatumDialog.cpp index 1b38619613..8c1db0273a 100644 --- a/src/Mod/Sketcher/Gui/EditDatumDialog.cpp +++ b/src/Mod/Sketcher/Gui/EditDatumDialog.cpp @@ -451,6 +451,14 @@ bool hasVisualFeature(App::DocumentObject* obj, App::DocumentObject* rootObj, Gu void EditDatumDialog::performAutoScale(double newDatum) { + const std::vector& constraints = sketch->Constraints.getValues(); + for (auto* constr : constraints) { + if (constr->Type == Sketcher::Group || constr->Type == Sketcher::Text) { + // Do not attempt to scale if there's a group + return; + } + } + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Sketcher/dimensioning" ); diff --git a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp index 687b94fe65..87bb532495 100644 --- a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -47,6 +48,8 @@ #include #include +#include + #include #include #include @@ -792,6 +795,87 @@ Restart: // Reference Position that is scaled according to zoom translation->translation = SbVec3f(relpos2.x - relpos1.x, relpos2.y - relpos1.y, 0); + } break; + case Text: + case Group: { + if (Constr->isElementsEmpty()) { + break; // Nothing to do if the group is empty + } + + Bnd_Box totalBBox; + int elementIndex = 0; + while (Constr->hasElement(elementIndex)) { + auto element = Constr->getElement(elementIndex); + if (element.GeoId < -extGeoCount || element.GeoId >= intGeoCount) { + elementIndex++; + continue; + } + const Part::Geometry* geo = geolistfacade.getGeometryFromGeoId(element.GeoId); + if (!geo) { + elementIndex++; + continue; + } + TopoDS_Shape shape = geo->toShape(); + if (!shape.IsNull()) { + BRepBndLib::Add(shape, totalBBox, false); + } + elementIndex++; + } + + if (!totalBBox.HasFinitePart() || totalBBox.IsVoid()) { + // If no valid box, hide the geometry by setting all points to the origin. + SoCoordinate3* coords = static_cast(sep->getChild(2)); + + // Use startEditing() to get a writable pointer to the internal array. + SbVec3f* points = coords->point.startEditing(); + for (int j = 0; j < 5; ++j) { + points[j].setValue(0.0f, 0.0f, 0.0f); + } + coords->point.finishEditing(); + } + + // 1. Get the original min/max points and dimensions + gp_Pnt min_pnt_orig = totalBBox.CornerMin(); + gp_Pnt max_pnt_orig = totalBBox.CornerMax(); + double width = max_pnt_orig.X() - min_pnt_orig.X(); + double height = max_pnt_orig.Y() - min_pnt_orig.Y(); + + // 2. Calculate the offset amount + // Using the average of width and height is a good heuristic for a uniform + // offset. + double offset = (width + height) / 2.0 * 0.05; // 5% of the average dimension + + // 3. Create new, "inflated" corner points by applying the offset + gp_Pnt min_pnt( + min_pnt_orig.X() - offset, + min_pnt_orig.Y() - offset, + min_pnt_orig.Z() + ); + gp_Pnt max_pnt( + max_pnt_orig.X() + offset, + max_pnt_orig.Y() + offset, + max_pnt_orig.Z() + ); + + // 4. Define the 4 corners of the rectangle using the inflated points + SbVec3f p0(min_pnt.X(), min_pnt.Y(), zConstrH); // bottom-left + SbVec3f p1(max_pnt.X(), min_pnt.Y(), zConstrH); // bottom-right + SbVec3f p2(max_pnt.X(), max_pnt.Y(), zConstrH); // top-right + SbVec3f p3(min_pnt.X(), max_pnt.Y(), zConstrH); // top-left + + // 3. Get the SoCoordinate3 node we created in rebuildConstraintNodes + // Index 0: SoMaterial, Index 1: SoDrawStyle, Index 2: SoCoordinate3 + SoCoordinate3* coords = static_cast(sep->getChild(2)); + + // 4. Update the points in the node to draw the rectangle + SbVec3f* points = coords->point.startEditing(); + points[0] = p0; + points[1] = p1; + points[2] = p2; + points[3] = p3; + points[4] = p0; // Repeat the first point to close the loop + coords->point.finishEditing(); + } break; case Distance: case DistanceX: @@ -1938,6 +2022,31 @@ void EditModeConstraintCoinManager::rebuildConstraintNodes( // remember the type of this constraint node vConstrType.push_back((*it)->Type); } break; + case Group: + case Text: { + // For a group, we will draw a dashed rectangle. + // We need a Material, a DrawStyle, Coordinates, and a LineSet. + + // 1. Material (for color, re-using the one already created) + sep->addChild(mat); + + // 2. DrawStyle (to make the line dashed) + SoDrawStyle* drawStyle = new SoDrawStyle(); + drawStyle->linePattern = 0x0F0F; // A standard 50% dashed pattern + sep->addChild(drawStyle); + + // 3. Coordinates (for the 4 corners + 1 to close the loop) + SoCoordinate3* coords = new SoCoordinate3(); + coords->point.setNum(5); // Pre-allocate 5 points for a closed rectangle + sep->addChild(coords); + + // 4. LineSet (to connect the coordinates) + SoLineSet* lineSet = new SoLineSet(); + lineSet->numVertices.set1Value(0, 5); // A single polyline of 5 vertices + sep->addChild(lineSet); + + vConstrType.push_back((*it)->Type); + } break; case Coincident: // no visual for coincident so far vConstrType.push_back(Coincident); break; diff --git a/src/Mod/Sketcher/Gui/EditModeGeometryCoinConverter.cpp b/src/Mod/Sketcher/Gui/EditModeGeometryCoinConverter.cpp index f7932b3c74..5a4f438875 100644 --- a/src/Mod/Sketcher/Gui/EditModeGeometryCoinConverter.cpp +++ b/src/Mod/Sketcher/Gui/EditModeGeometryCoinConverter.cpp @@ -27,6 +27,8 @@ #include #include +#include + #include "EditModeCoinManagerParameters.h" #include "EditModeGeometryCoinConverter.h" #include "Utils.h" @@ -89,85 +91,59 @@ void EditModeGeometryCoinConverter::convert(const Sketcher::GeoListFacade& geoli int coinLayer, EditModeGeometryCoinConverter::PointsMode pointmode, int numberCurves, - int sublayer + int sublayer, + bool isGroupMember = false ) { + // Determine how many vertices this geometry has. int numberPoints = 0; - if (pointmode == PointsMode::InsertSingle) { numberPoints = 1; - - coinMapping.GeoElementId2SetId.emplace( - std::piecewise_construct, - std::forward_as_tuple(geoId, Sketcher::PointPos::start), - std::forward_as_tuple(pointCounter[coinLayer]++, coinLayer) - ); } else if (pointmode == PointsMode::InsertStartEnd) { numberPoints = 2; - - coinMapping.GeoElementId2SetId.emplace( - std::piecewise_construct, - std::forward_as_tuple(geoId, Sketcher::PointPos::start), - std::forward_as_tuple(pointCounter[coinLayer]++, coinLayer) - ); - - coinMapping.GeoElementId2SetId.emplace( - std::piecewise_construct, - std::forward_as_tuple(geoId, Sketcher::PointPos::end), - std::forward_as_tuple(pointCounter[coinLayer]++, coinLayer) - ); } else if (pointmode == PointsMode::InsertMidOnly) { numberPoints = 1; - - coinMapping.GeoElementId2SetId.emplace( - std::piecewise_construct, - std::forward_as_tuple(geoId, Sketcher::PointPos::mid), - std::forward_as_tuple(pointCounter[coinLayer]++, coinLayer) - ); } else if (pointmode == PointsMode::InsertStartEndMid) { numberPoints = 3; - - coinMapping.GeoElementId2SetId.emplace( - std::piecewise_construct, - std::forward_as_tuple(geoId, Sketcher::PointPos::start), - std::forward_as_tuple(pointCounter[coinLayer]++, coinLayer) - ); - - coinMapping.GeoElementId2SetId.emplace( - std::piecewise_construct, - std::forward_as_tuple(geoId, Sketcher::PointPos::end), - std::forward_as_tuple(pointCounter[coinLayer]++, coinLayer) - ); - - coinMapping.GeoElementId2SetId.emplace( - std::piecewise_construct, - std::forward_as_tuple(geoId, Sketcher::PointPos::mid), - std::forward_as_tuple(pointCounter[coinLayer]++, coinLayer) - ); } + // This loop simulates the creation of vertices for THIS geometry. + // It runs for all geometries to keep vertexCounter in sync with SketchObject. for (int i = 0; i < numberPoints; i++) { - coinMapping.PointIdToGeoId[coinLayer].push_back(geoId); - Sketcher::PointPos pos; - if (i == 0) { - if (pointmode == PointsMode::InsertMidOnly) { - pos = Sketcher::PointPos::mid; + // If the point is NOT part of a group member, we add it to the physical + // Coin maps that are used for drawing and picking. + if (!isGroupMember) { + // Determine the PointPos for this specific vertex of the geometry. + Sketcher::PointPos pos; + if (i == 0) { + pos = (pointmode == PointsMode::InsertMidOnly) ? Sketcher::PointPos::mid + : Sketcher::PointPos::start; + } + else if (i == 1) { + pos = Sketcher::PointPos::end; } else { - pos = Sketcher::PointPos::start; + pos = Sketcher::PointPos::mid; } - } - else if (i == 1) { - pos = Sketcher::PointPos::end; - } - else { - pos = Sketcher::PointPos::mid; + + // Map: (GeoId, PosId) -> (physicalIndex, layer) + coinMapping.GeoElementId2SetId.emplace( + std::piecewise_construct, + std::forward_as_tuple(geoId, pos), + std::forward_as_tuple(pointCounter[coinLayer]++, coinLayer) + ); + + // Map: physicalIndex -> logical info + coinMapping.PointIdToGeoId[coinLayer].push_back(geoId); + coinMapping.PointIdToPosId[coinLayer].push_back(pos); + // This is the key: store the correct, globally-incremented logical VertexId. + coinMapping.PointIdToVertexId[coinLayer].push_back(vertexCounter); } - coinMapping.PointIdToPosId[coinLayer].push_back(pos); - coinMapping.PointIdToVertexId[coinLayer].push_back(vertexCounter++); + // ALWAYS increment the logical vertex counter to stay in sync with SketchObject. + vertexCounter++; } if (numberCurves > 0) { // insert the first segment of the curve into the map @@ -198,6 +174,9 @@ void EditModeGeometryCoinConverter::convert(const Sketcher::GeoListFacade& geoli auto coinLayer = geometryLayerParameters.getSafeCoinLayer(layerId); + auto* obj = viewProvider.getSketchObject(); + bool isGroupMember = GeoId >= 0 && obj->isInGroup(GeoId, false); + if (type == Part::GeomPoint::getClassTypeId()) { // add a point convert< Part::GeomPoint, @@ -217,13 +196,19 @@ void EditModeGeometryCoinConverter::convert(const Sketcher::GeoListFacade& geoli Part::GeomLineSegment, EditModeGeometryCoinConverter::PointsMode::InsertStartEnd, EditModeGeometryCoinConverter::CurveMode::StartEndPointsOnly, - EditModeGeometryCoinConverter::AnalyseMode::BoundingBoxMagnitude>(geom, GeoId, subLayerId); + EditModeGeometryCoinConverter::AnalyseMode::BoundingBoxMagnitude>( + geom, + GeoId, + subLayerId, + isGroupMember + ); setTracking( GeoId, coinLayer, EditModeGeometryCoinConverter::PointsMode::InsertStartEnd, 1, - subLayerId + subLayerId, + isGroupMember ); } else if (type.isDerivedFrom(Part::GeomConic::getClassTypeId())) { // add a closed curve conic @@ -231,13 +216,19 @@ void EditModeGeometryCoinConverter::convert(const Sketcher::GeoListFacade& geoli Part::GeomConic, EditModeGeometryCoinConverter::PointsMode::InsertMidOnly, EditModeGeometryCoinConverter::CurveMode::ClosedCurve, - EditModeGeometryCoinConverter::AnalyseMode::BoundingBoxMagnitude>(geom, GeoId, subLayerId); + EditModeGeometryCoinConverter::AnalyseMode::BoundingBoxMagnitude>( + geom, + GeoId, + subLayerId, + isGroupMember + ); setTracking( GeoId, coinLayer, EditModeGeometryCoinConverter::PointsMode::InsertMidOnly, 1, - subLayerId + subLayerId, + isGroupMember ); } else if (type.isDerivedFrom(Part::GeomArcOfConic::getClassTypeId())) { // add an arc of conic @@ -245,13 +236,19 @@ void EditModeGeometryCoinConverter::convert(const Sketcher::GeoListFacade& geoli Part::GeomArcOfConic, EditModeGeometryCoinConverter::PointsMode::InsertStartEndMid, EditModeGeometryCoinConverter::CurveMode::OpenCurve, - EditModeGeometryCoinConverter::AnalyseMode::BoundingBoxMagnitude>(geom, GeoId, subLayerId); + EditModeGeometryCoinConverter::AnalyseMode::BoundingBoxMagnitude>( + geom, + GeoId, + subLayerId, + isGroupMember + ); setTracking( GeoId, coinLayer, EditModeGeometryCoinConverter::PointsMode::InsertStartEndMid, 1, - subLayerId + subLayerId, + isGroupMember ); arcGeoIds.push_back(GeoId); } @@ -264,14 +261,16 @@ void EditModeGeometryCoinConverter::convert(const Sketcher::GeoListFacade& geoli EditModeGeometryCoinConverter::AnalyseMode::BoundingBoxMagnitudeAndBSplineCurvature>( geom, GeoId, - subLayerId + subLayerId, + isGroupMember ); setTracking( GeoId, coinLayer, EditModeGeometryCoinConverter::PointsMode::InsertStartEnd, 1, - subLayerId + subLayerId, + isGroupMember ); bsplineGeoIds.push_back(GeoId); } @@ -325,7 +324,8 @@ template< void EditModeGeometryCoinConverter::convert( const Sketcher::GeometryFacade* geometryfacade, [[maybe_unused]] int geoid, - [[maybe_unused]] int subLayer + [[maybe_unused]] int subLayer, + bool isGroupMember ) { auto geo = static_cast(geometryfacade->getGeometry()); @@ -343,21 +343,23 @@ void EditModeGeometryCoinConverter::convert( }; // Points - if constexpr (pointmode == PointsMode::InsertSingle) { - addPoint(Points[coinLayer], geo->getPoint()); - } - else if constexpr (pointmode == PointsMode::InsertStartEnd) { - addPoint(Points[coinLayer], geo->getStartPoint()); - addPoint(Points[coinLayer], geo->getEndPoint()); - } - else if constexpr (pointmode == PointsMode::InsertStartEndMid) { - // All in this group are Trimmed Curves (see Geometry.h) - addPoint(Points[coinLayer], geo->getStartPoint(/*emulateCCW=*/true)); - addPoint(Points[coinLayer], geo->getEndPoint(/*emulateCCW=*/true)); - addPoint(Points[coinLayer], geo->getCenter()); - } - else if constexpr (pointmode == PointsMode::InsertMidOnly) { - addPoint(Points[coinLayer], geo->getCenter()); + if (!isGroupMember) { + if constexpr (pointmode == PointsMode::InsertSingle) { + addPoint(Points[coinLayer], geo->getPoint()); + } + else if constexpr (pointmode == PointsMode::InsertStartEnd) { + addPoint(Points[coinLayer], geo->getStartPoint()); + addPoint(Points[coinLayer], geo->getEndPoint()); + } + else if constexpr (pointmode == PointsMode::InsertStartEndMid) { + // All in this group are Trimmed Curves (see Geometry.h) + addPoint(Points[coinLayer], geo->getStartPoint(/*emulateCCW=*/true)); + addPoint(Points[coinLayer], geo->getEndPoint(/*emulateCCW=*/true)); + addPoint(Points[coinLayer], geo->getCenter()); + } + else if constexpr (pointmode == PointsMode::InsertMidOnly) { + addPoint(Points[coinLayer], geo->getCenter()); + } } // Curves diff --git a/src/Mod/Sketcher/Gui/EditModeGeometryCoinConverter.h b/src/Mod/Sketcher/Gui/EditModeGeometryCoinConverter.h index c97b7bfdba..933aa6cab6 100644 --- a/src/Mod/Sketcher/Gui/EditModeGeometryCoinConverter.h +++ b/src/Mod/Sketcher/Gui/EditModeGeometryCoinConverter.h @@ -158,7 +158,8 @@ private: void convert( const Sketcher::GeometryFacade* geometryfacade, [[maybe_unused]] int geoId, - [[maybe_unused]] int subLayerId = 0 + [[maybe_unused]] int subLayerId = 0, + bool isGroupMember = false ); private: @@ -168,6 +169,9 @@ private: GeometryLayerNodes& geometryLayerNodes; std::vector> Points; + // To hide the points of geometries that are grouped, we make them transparent + // Just not adding the points would mess the indexes in Points + std::vector> PointsHidden; std::vector>> Coords; std::vector>> Index; diff --git a/src/Mod/Sketcher/Gui/EditModeGeometryCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeGeometryCoinManager.cpp index 2ebd9cb084..93bac177f7 100644 --- a/src/Mod/Sketcher/Gui/EditModeGeometryCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeGeometryCoinManager.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include "EditModeGeometryCoinConverter.h" #include "EditModeGeometryCoinManager.h" @@ -463,8 +464,18 @@ void EditModeGeometryCoinManager::updateGeometryColor( // edit->CurveSet->numVertices => [i] indicates number of vertex for line i. int indexes = (editModeScenegraphNodes.CurveSet[l][t]->numVertices[i]); - bool selected = ViewProviderSketchCoinAttorney::isCurveSelected(viewProvider, GeoId); bool preselected = (preselectcurve == GeoId); + + auto* obj = viewProvider.getSketchObject(); + bool isGroupMember = GeoId >= 0 && obj->isInGroup(GeoId, false); + if (isGroupMember) { + // We use the same color as group handle. + GeoId = obj->getGroupHandleIfInGroup(GeoId); + } + + bool selected = ViewProviderSketchCoinAttorney::isCurveSelected(viewProvider, GeoId); + // if a grouped edge is preselected we still want it to be shown + preselected = preselected ? true : (preselectcurve == GeoId); bool constrainedElement = isFullyConstraintElement(GeoId); bool isExternal = GeoId < -1; diff --git a/src/Mod/Sketcher/Gui/EditModeInformationOverlayCoinConverter.cpp b/src/Mod/Sketcher/Gui/EditModeInformationOverlayCoinConverter.cpp index 0793d91641..fe9d119cc6 100644 --- a/src/Mod/Sketcher/Gui/EditModeInformationOverlayCoinConverter.cpp +++ b/src/Mod/Sketcher/Gui/EditModeInformationOverlayCoinConverter.cpp @@ -36,6 +36,8 @@ #include #include +#include + #include "EditModeCoinManagerParameters.h" #include "EditModeInformationOverlayCoinConverter.h" #include "ViewProviderSketchCoinAttorney.h" @@ -59,6 +61,17 @@ EditModeInformationOverlayCoinConverter::EditModeInformationOverlayCoinConverter void EditModeInformationOverlayCoinConverter::convert(const Part::Geometry* geometry, int geoid) { + if (geoid >= 0) { + // Get the SketchObject from the ViewProvider. + auto* obj = viewProvider.getSketchObject(); + const bool isGroupMember = obj->isInGroup(geoid, false); + + if (obj) { + if (obj->isInGroup(geoid, false)) { + return; + } + } + } if (geometry->is()) { if (geoid < 0) { diff --git a/src/Mod/Sketcher/Gui/EditTextDialog.cpp b/src/Mod/Sketcher/Gui/EditTextDialog.cpp new file mode 100644 index 0000000000..341c1c904b --- /dev/null +++ b/src/Mod/Sketcher/Gui/EditTextDialog.cpp @@ -0,0 +1,108 @@ +// SPDX - License - Identifier: LGPL - 2.1 - or -later +/**************************************************************************** + * * + * Copyright (c) 2025 Pierre-Louis Boyer * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +# include +# include +#endif + +#include +#include + +#include "EditTextDialog.h" +#include "ViewProviderSketch.h" +#include "Utils.h" +#include "ui_EditTextDialog.h" + +using namespace SketcherGui; + +EditTextDialog::EditTextDialog(ViewProviderSketch* viewProvider, int constraintIndex, QWidget* parent) + : QDialog(parent) + , ui(new Ui::EditTextDialog) + , sketchView(viewProvider) + , constrIndex(constraintIndex) +{ + ui->setupUi(this); + + const Sketcher::SketchObject* sketch = sketchView->getSketchObject(); + const Sketcher::Constraint* constraint = sketch->Constraints[constrIndex]; + + // Initialize Text + ui->lineEdit_text->setText(QString::fromStdString(constraint->Text)); + + // Initialize Font + populateFontList(); + QString currentFontName = findFontNameFromPath(QString::fromStdString(constraint->Font)); + if (!currentFontName.isEmpty()) { + ui->comboBox_font->setCurrentText(currentFontName); + } +} + +EditTextDialog::~EditTextDialog() +{ + delete ui; +} + +void EditTextDialog::populateFontList() +{ + fontPathMap = findAvailableFontFiles(); + QStringList fontNames = fontPathMap.keys(); + fontNames.sort(Qt::CaseInsensitive); + ui->comboBox_font->addItems(fontNames); +} + +QString EditTextDialog::findFontNameFromPath(const QString& path) const +{ + return fontPathMap.key(path, QString()); +} + +void EditTextDialog::on_buttonBox_accepted() +{ + const Sketcher::SketchObject* sketch = sketchView->getSketchObject(); + + // Get new values from the dialog + std::string newText = ui->lineEdit_text->text().toStdString(); + QString selectedFontName = ui->comboBox_font->currentText(); + std::string newFontPath = fontPathMap.value(selectedFontName).toStdString(); + + const Sketcher::Constraint* constraint = sketch->Constraints[constrIndex]; + + // Check if anything changed + if (newText == constraint->Text && newFontPath == constraint->Font) { + return; // Nothing to do + } + + // Open a command to make the change undo-able + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Modify sketch text constraint")); + + try { + Gui::cmdAppObjectArgs(sketch, "setTextAndFont(%i, '%s', '%s')", constrIndex, newText, newFontPath); + + Gui::Command::commitCommand(); + } + catch (const Base::Exception& e) { + Gui::Command::abortCommand(); + Base::Console().error("Failed to modify text constraint: %s\n", e.what()); + } +} diff --git a/src/Mod/Sketcher/Gui/EditTextDialog.h b/src/Mod/Sketcher/Gui/EditTextDialog.h new file mode 100644 index 0000000000..4161f88cab --- /dev/null +++ b/src/Mod/Sketcher/Gui/EditTextDialog.h @@ -0,0 +1,71 @@ +// SPDX - License - Identifier: LGPL - 2.1 - or -later +/**************************************************************************** + * * + * Copyright (c) 2025 Pierre-Louis Boyer * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef SKETCHERGUI_EDITTEXTDIALOG_H +#define SKETCHERGUI_EDITTEXTDIALOG_H + +#include "PreCompiled.h" +#ifndef _PreComp_ +# include +# include +# include +#endif + +namespace Ui +{ +class EditTextDialog; +} + +namespace SketcherGui +{ + +class ViewProviderSketch; + +class EditTextDialog: public QDialog +{ + Q_OBJECT + +public: + explicit EditTextDialog( + ViewProviderSketch* viewProvider, + int constraintIndex, + QWidget* parent = nullptr + ); + ~EditTextDialog() override; + +private Q_SLOTS: + void on_buttonBox_accepted(); + +private: + Ui::EditTextDialog* ui; + ViewProviderSketch* sketchView; + int constrIndex; + QMap fontPathMap; + + void populateFontList(); + QString findFontNameFromPath(const QString& path) const; +}; + +} // namespace SketcherGui + +#endif // SKETCHERGUI_EDITTEXTDIALOG_H diff --git a/src/Mod/Sketcher/Gui/EditTextDialog.ui b/src/Mod/Sketcher/Gui/EditTextDialog.ui new file mode 100644 index 0000000000..1b29a238f8 --- /dev/null +++ b/src/Mod/Sketcher/Gui/EditTextDialog.ui @@ -0,0 +1,88 @@ + + + EditTextDialog + + + + 0 + 0 + 320 + 100 + + + + Edit Text + + + + + + + + Text: + + + + + + + + + + Font: + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + EditTextDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditTextDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc index 8a8d79a217..77415ab979 100644 --- a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc +++ b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc @@ -19,6 +19,8 @@ icons/constraints/Constraint_Ellipse_Radii.svg icons/constraints/Constraint_EqualLength.svg icons/constraints/Constraint_ExternalAngle.svg + icons/constraints/Constraint_Group.svg + icons/constraints/Constraint_Text.svg icons/constraints/Constraint_Horizontal.svg icons/constraints/Constraint_HorizontalDistance.svg icons/constraints/Constraint_HorizontalDistance_Driven.svg @@ -188,6 +190,7 @@ icons/geometry/Sketcher_CreateSquare.svg icons/geometry/Sketcher_CreateSquare_Constr.svg icons/geometry/Sketcher_CreateText.svg + icons/geometry/Sketcher_CreateText_Constr.svg icons/geometry/Sketcher_CreateTriangle.svg icons/geometry/Sketcher_CreateTriangle_Constr.svg icons/geometry/Sketcher_Extend.svg diff --git a/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Group.svg b/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Group.svg new file mode 100644 index 0000000000..1e64e5a1af --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Group.svg @@ -0,0 +1,443 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [Abdullah Tahiri] + + + 2015-05-26 + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_ToggleConstraint.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Text.svg b/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Text.svg new file mode 100644 index 0000000000..231658e932 --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Text.svg @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_CreateArc.svg + + + FreeCAD LGPL2+ + + + 2023-12-19 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreateText.svg b/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreateText.svg index 231658e932..7fed118b3e 100644 --- a/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreateText.svg +++ b/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreateText.svg @@ -7,12 +7,34 @@ id="svg2869" version="1.1" viewBox="0 0 64 64" + sodipodi:docname="Sketcher_CreateText.svg" + inkscape:version="1.4 (86a8ad7, 2024-10-11)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + + style="stop-color:#d3d7cf;stop-opacity:1;" /> + style="stop-color:#d3d7cf;stop-opacity:1;" /> - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreateText_Constr.svg b/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreateText_Constr.svg new file mode 100644 index 0000000000..2e4339ed95 --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/geometry/Sketcher_CreateText_Constr.svg @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_CreateArc.svg + + + FreeCAD LGPL2+ + + + 2023-12-19 + + + + + + + diff --git a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.cpp b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.cpp index 5874d33082..145fb5925f 100644 --- a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.cpp +++ b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.cpp @@ -26,7 +26,7 @@ #include #include #include - +#include #include "ui_SketcherToolDefaultWidget.h" #include @@ -66,6 +66,9 @@ SketcherToolDefaultWidget::SketcherToolDefaultWidget(QWidget* parent) ui->parameterFive->installEventFilter(this); ui->parameterSix->installEventFilter(this); + ui->lineEdit1->installEventFilter(this); + ui->lineEdit2->installEventFilter(this); + reset(); } @@ -155,6 +158,8 @@ void SketcherToolDefaultWidget::setupConnections() this, &SketcherToolDefaultWidget::comboBox3_currentIndexChanged ); + connect(ui->lineEdit1, &QLineEdit::textChanged, this, &SketcherToolDefaultWidget::lineEdit1_textChanged); + connect(ui->lineEdit2, &QLineEdit::textChanged, this, &SketcherToolDefaultWidget::lineEdit2_textChanged); } // preselect the number of the spinbox when it gets the focus. @@ -179,6 +184,13 @@ bool SketcherToolDefaultWidget::eventFilter(QObject* object, QEvent* event) return true; } } + + for (int i = 0; i < nLineEdit; i++) { + if (object == getLineEdit(i)) { + signalParameterTabOrEnterPressed(i); + return true; + } + } } } @@ -206,6 +218,11 @@ void SketcherToolDefaultWidget::reset() setComboboxIndex(i, 0); getComboBox(i)->clear(); } + for (int i = 0; i < nLineEdit; i++) { + setLineEditVisible(i, false); + QString str; + setLineEditText(i, str); + } setNoticeVisible(false); } @@ -735,21 +752,18 @@ void SketcherToolDefaultWidget::comboBox1_currentIndexChanged(int val) if (!blockParameterSlots) { signalComboboxSelectionChanged(Combobox::FirstCombo, val); } - ui->comboBox1->onSave(); } void SketcherToolDefaultWidget::comboBox2_currentIndexChanged(int val) { if (!blockParameterSlots) { signalComboboxSelectionChanged(Combobox::SecondCombo, val); } - ui->comboBox2->onSave(); } void SketcherToolDefaultWidget::comboBox3_currentIndexChanged(int val) { if (!blockParameterSlots) { signalComboboxSelectionChanged(Combobox::ThirdCombo, val); } - ui->comboBox3->onSave(); } void SketcherToolDefaultWidget::initNComboboxes(int ncombobox) @@ -833,6 +847,116 @@ int SketcherToolDefaultWidget::getComboboxIndex(int comboboxindex) THROWM(Base::IndexError, "ToolWidget combobox index out of range"); } +QString SketcherToolDefaultWidget::getComboboxCurrentText(int comboboxindex) +{ + if (comboboxindex < nCombobox) { + return getComboBox(comboboxindex)->currentText(); + } + THROWM(Base::IndexError, "ToolWidget combobox index out of range"); +} + +int SketcherToolDefaultWidget::setComboboxCurrentText(int comboboxindex, const QString& text) +{ + if (comboboxindex < nCombobox) { + int index = getComboBox(comboboxindex)->findText(text, Qt::MatchFixedString); + if (index != -1) { + getComboBox(comboboxindex)->setCurrentIndex(index); + } + return index; + } + THROWM(Base::IndexError, "ToolWidget combobox index out of range"); +} + +void SketcherToolDefaultWidget::lineEdit1_textChanged(const QString& text) +{ + if (!blockParameterSlots) { + signalLineEditTextChanged(LineEdit::FirstEdit, text); + } +} + +void SketcherToolDefaultWidget::lineEdit2_textChanged(const QString& text) +{ + if (!blockParameterSlots) { + signalLineEditTextChanged(LineEdit::SecondEdit, text); + } +} + +void SketcherToolDefaultWidget::initNLineEdits(int nlineedit) +{ + Base::StateLocker lock(blockParameterSlots, true); + for (int i = 0; i < nLineEdit; ++i) { + setLineEditVisible(i, i < nlineedit); + QString str; + setLineEditText(i, str); + } +} + +void SketcherToolDefaultWidget::setLineEditVisible(int lineeditindex, bool visible) +{ + if (lineeditindex < nLineEdit) { + getLineEdit(lineeditindex)->setVisible(visible); + getLineEditLabel(lineeditindex)->setVisible(visible); + } +} + +void SketcherToolDefaultWidget::setLineEditText(int lineeditindex, const QString& text) +{ + if (lineeditindex < nLineEdit) { + getLineEdit(lineeditindex)->setText(text); + return; + } + THROWM(Base::IndexError, "ToolWidget line edit index out of range"); +} + +void SketcherToolDefaultWidget::setLineEditLabel(int lineeditindex, const QString& string) +{ + if (lineeditindex < nLineEdit) { + getLineEditLabel(lineeditindex)->setText(string); + } +} + +void SketcherToolDefaultWidget::setLineEditFocus(int lineeditindex) +{ + if (lineeditindex < nLineEdit) { + QLineEdit* lineEdit = getLineEdit(lineeditindex); + lineEdit->setFocus(Qt::OtherFocusReason); + lineEdit->selectAll(); + return; + } + THROWM(Base::IndexError, "ToolWidget line edit index out of range"); +} + +QString SketcherToolDefaultWidget::getLineEditText(int lineeditindex) +{ + if (lineeditindex < nLineEdit) { + return getLineEdit(lineeditindex)->text(); + } + THROWM(Base::IndexError, "ToolWidget line edit index out of range"); +} + +QLabel* SketcherToolDefaultWidget::getLineEditLabel(int lineeditindex) +{ + switch (lineeditindex) { + case LineEdit::FirstEdit: + return ui->lineEditLabel1; + case LineEdit::SecondEdit: + return ui->lineEditLabel2; + default: + THROWM(Base::IndexError, "ToolWidget line edit index out of range"); + } +} + +QLineEdit* SketcherToolDefaultWidget::getLineEdit(int lineeditindex) +{ + switch (lineeditindex) { + case LineEdit::FirstEdit: + return ui->lineEdit1; + case LineEdit::SecondEdit: + return ui->lineEdit2; + default: + THROWM(Base::IndexError, "ToolWidget line edit index out of range"); + } +} void SketcherToolDefaultWidget::changeEvent(QEvent* ev) { diff --git a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h index d5946aa5b8..fafa5c77f5 100644 --- a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h +++ b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h @@ -33,6 +33,7 @@ class QComboBox; +class QLineEdit; namespace App { @@ -102,6 +103,14 @@ public: nCombobox // Must Always be the last one }; + /// LineEdit number/label + enum LineEdit + { + FirstEdit, + SecondEdit, + nLineEdit // Must Always be the last one + }; + explicit SketcherToolDefaultWidget(QWidget* parent = nullptr); ~SketcherToolDefaultWidget() override; @@ -149,11 +158,20 @@ public: void setComboboxIndex(int comboboxindex, int value); void setComboboxLabel(int comboboxindex, const QString& string); int getComboboxIndex(int comboboxindex); + QString getComboboxCurrentText(int comboboxindex); + int setComboboxCurrentText(int comboboxIndex, const QString& text); void setComboboxElements(int comboboxindex, const QStringList& names); void setComboboxItemIcon(int comboboxindex, int index, QIcon icon); void setComboboxPrefEntry(int comboboxindex, const std::string& prefEntry); void restoreComboboxPref(int comboboxindex); + void initNLineEdits(int nlineedit); + void setLineEditVisible(int lineeditindex, bool visible); + void setLineEditText(int lineeditindex, const QString& text); + void setLineEditLabel(int lineeditindex, const QString& string); + QString getLineEditText(int lineeditindex); + void setLineEditFocus(int lineeditindex); + template fastsignals::advanced_connection registerParameterTabOrEnterPressed(F&& fn) { @@ -181,6 +199,11 @@ public: return signalComboboxSelectionChanged.connect(std::forward(fn), fastsignals::advanced_tag()); } + template + boost::signals2::connection registerLineEditTextChanged(F&& fn) + { + return signalLineEditTextChanged.connect(std::forward(fn)); + } // Q_SIGNALS: protected Q_SLOTS: @@ -201,6 +224,8 @@ protected Q_SLOTS: void comboBox1_currentIndexChanged(int val); void comboBox2_currentIndexChanged(int val); void comboBox3_currentIndexChanged(int val); + void lineEdit1_textChanged(const QString& text); + void lineEdit2_textChanged(const QString& text); protected: void changeEvent(QEvent* ev) override; @@ -212,6 +237,8 @@ private: Gui::PrefCheckBox* getCheckBox(int checkboxindex); Gui::PrefComboBox* getComboBox(int comboboxindex); QLabel* getComboBoxLabel(int comboboxindex); + QLabel* getLineEditLabel(int lineeditindex); + QLineEdit* getLineEdit(int lineeditindex); void setParameterFontStyle(int parameterindex, FontStyle fontStyle); @@ -220,10 +247,11 @@ private: private: std::unique_ptr ui; - fastsignals::signal signalParameterTabOrEnterPressed; - fastsignals::signal signalParameterValueChanged; - fastsignals::signal signalCheckboxCheckedChanged; - fastsignals::signal signalComboboxSelectionChanged; + fastsignals signalParameterTabOrEnterPressed; + fastsignals signalParameterValueChanged; + fastsignals signalCheckboxCheckedChanged; + fastsignals signalComboboxSelectionChanged; + fastsignals signalLineEditTextChanged; /// lock to block QT slots bool blockParameterSlots; diff --git a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.ui b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.ui index 16bf2782ef..7c523b5b5e 100644 --- a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.ui +++ b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.ui @@ -86,6 +86,34 @@ + + + + + + Line edit 1 + + + + + + + + + + + + + + Line edit 2 + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp index 1ef0cf24fe..4b47bb9515 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp @@ -52,6 +52,7 @@ #include #include "EditDatumDialog.h" +#include "EditTextDialog.h" #include "TaskSketcherConstraints.h" #include "Utils.h" #include "ViewProviderSketch.h" @@ -318,6 +319,8 @@ public: // Gui::BitmapFactory().iconFromTheme("Constraint_Ellipse_Axis_Angle") ); static QIcon equal(Gui::BitmapFactory().iconFromTheme("Constraint_EqualLength")); static QIcon pntoo(Gui::BitmapFactory().iconFromTheme("Constraint_PointOnObject")); + static QIcon group(Gui::BitmapFactory().iconFromTheme("Constraint_Group")); + static QIcon text(Gui::BitmapFactory().iconFromTheme("Constraint_Text")); static QIcon symm(Gui::BitmapFactory().iconFromTheme("Constraint_Symmetric")); static QIcon snell(Gui::BitmapFactory().iconFromTheme("Constraint_SnellsLaw")); static QIcon iaellipseminoraxis(Gui::BitmapFactory().iconFromTheme( @@ -378,6 +381,10 @@ public: return selicon(constraint, block, block); case Sketcher::PointOnObject: return selicon(constraint, pntoo, pntoo); + case Sketcher::Group: + return selicon(constraint, group, group); + case Sketcher::Text: + return selicon(constraint, text, text); case Sketcher::Parallel: return selicon(constraint, para, para); case Sketcher::Perpendicular: @@ -461,6 +468,8 @@ public: case Sketcher::Tangent: case Sketcher::Equal: case Sketcher::Symmetric: + case Sketcher::Group: + case Sketcher::Text: return true; case Sketcher::Distance: case Sketcher::DistanceX: @@ -1261,6 +1270,11 @@ void TaskSketcherConstraints::onListWidgetConstraintsItemActivated(QListWidgetIt editDatumDialog->exec(false); delete editDatumDialog; } + else if (it->constraintType() == Sketcher::Text) { + auto* editDialog = new EditTextDialog(this->sketchView, it->ConstraintNbr); + editDialog->exec(); + delete editDialog; + } } void TaskSketcherConstraints::onListWidgetConstraintsItemChanged(QListWidgetItem* item) @@ -1716,6 +1730,12 @@ bool TaskSketcherConstraints::isConstraintFiltered(QListWidgetItem* item) case Sketcher::PointOnObject: visible = checkFilterBitset(multiFilterStatus, FilterValue::PointOnObject); break; + case Sketcher::Group: + visible = checkFilterBitset(multiFilterStatus, FilterValue::Group); + break; + case Sketcher::Text: + visible = checkFilterBitset(multiFilterStatus, FilterValue::Text); + break; case Sketcher::Parallel: visible = checkFilterBitset(multiFilterStatus, FilterValue::Parallel); break; diff --git a/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp b/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp index 095c67b632..0dbeb45751 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp @@ -54,6 +54,7 @@ #include #include "TaskSketcherElements.h" +#include "EditTextDialog.h" #include "Utils.h" #include "ViewProviderSketch.h" #include "ui_TaskSketcherElements.h" @@ -84,6 +85,8 @@ QT_TRANSLATE_NOOP("SketcherGui::ElementView", "Symmetric Constraint"); QT_TRANSLATE_NOOP("SketcherGui::ElementView", "Block Constraint"); +QT_TRANSLATE_NOOP("SketcherGui::ElementView", "Group Constraint"); + QT_TRANSLATE_NOOP("SketcherGui::ElementView", "Lock Position"); QT_TRANSLATE_NOOP("SketcherGui::ElementView", "Horizontal Dimension"); @@ -768,7 +771,8 @@ void ElementView::contextMenuEvent(QContextMenuEvent* event) true) CONTEXT_ITEM( "Constraint_Block", "Block Constraint", "Sketcher_ConstrainBlock", doBlockConstraint, true) - + CONTEXT_ITEM( + "Constraint_Group", "Group constraint", "Sketcher_ConstrainGroup", doGroupConstraint, true) CONTEXT_ITEM("Constraint_HorizontalDistance", "Horizontal Dimension", "Sketcher_ConstrainDistanceX", @@ -886,6 +890,7 @@ CONTEXT_MEMBER_DEF("Sketcher_ConstrainTangent", doTangentConstraint) CONTEXT_MEMBER_DEF("Sketcher_ConstrainEqual", doEqualConstraint) CONTEXT_MEMBER_DEF("Sketcher_ConstrainSymmetric", doSymmetricConstraint) CONTEXT_MEMBER_DEF("Sketcher_ConstrainBlock", doBlockConstraint) +CONTEXT_MEMBER_DEF("Sketcher_ConstrainGroup", doGroupConstraint) CONTEXT_MEMBER_DEF("Sketcher_ConstrainDistanceX", doHorizontalDistance) CONTEXT_MEMBER_DEF("Sketcher_ConstrainDistanceY", doVerticalDistance) @@ -1322,6 +1327,10 @@ void TaskSketcherElements::connectSignals() &ElementView::onItemHovered, this, &TaskSketcherElements::onListWidgetElementsMouseMoveOnItem); + QObject::connect(ui->listWidgetElements, + &ElementView::itemActivated, + this, + &TaskSketcherElements::onListWidgetItemActivated); QObject::connect(filterList, &QListWidget::itemChanged, this, @@ -1419,10 +1428,16 @@ void TaskSketcherElements::onListMultiFilterItemChanged(QListWidgetItem* item) updateVisibility(); } -void TaskSketcherElements::setItemVisibility(QListWidgetItem* it) +void TaskSketcherElements::setItemVisibility(QListWidgetItem* it, const std::set& groupedGeoIds) { auto* item = static_cast(it); + // First, check if the item is a member of a group. If so, it must be hidden. + if (groupedGeoIds.count(item->ElementNbr)) { + item->setHidden(true); + return; + } + if (ui->filterBox->checkState() == Qt::Unchecked) { item->setHidden(false); return; @@ -1477,8 +1492,20 @@ void TaskSketcherElements::setItemVisibility(QListWidgetItem* it) void TaskSketcherElements::updateVisibility() { + // Calculate the set of grouped geometries that should be hidden. + std::set groupedGeoIds; + const auto& constraints = sketchView->getSketchObject()->Constraints.getValues(); + for (const auto* c : constraints) { + if (c->Type == Sketcher::Group || c->Type == Sketcher::Text) { + // Member geometries start from index 1. + for (int j = 1; c->hasElement(j); ++j) { + groupedGeoIds.insert(c->getGeoId(j)); + } + } + } + for (int i = 0; i < ui->listWidgetElements->count(); i++) { - setItemVisibility(ui->listWidgetElements->item(i)); + setItemVisibility(ui->listWidgetElements->item(i), groupedGeoIds); } } @@ -1859,6 +1886,34 @@ void TaskSketcherElements::onListWidgetElementsMouseMoveOnItem(QListWidgetItem* previouslyHoveredType = item->hovered; } +void TaskSketcherElements::onListWidgetItemActivated(QListWidgetItem* item) +{ + auto* elementItem = static_cast(item); + if (!elementItem) { + return; + } + + Sketcher::SketchObject* sketch = sketchView->getSketchObject(); + int geoId = elementItem->ElementNbr; + + const auto& constraints = sketch->Constraints.getValues(); + for (size_t i = 0; i < constraints.size(); ++i) { + const auto* constraint = constraints[i]; + + // Check if the constraint is a Text constraint and if the activated element + // is its handle (the geometry at index 0). + if (constraint->Type == Sketcher::Text && constraint->hasElement(0)) { + if (constraint->getGeoId(0) == geoId) { + // The item is a handle for a text constraint. Open the edit dialog. + auto* editDialog = new EditTextDialog(this->sketchView, i); + editDialog->exec(); + delete editDialog; + return; + } + } + } +} + void TaskSketcherElements::leaveEvent(QEvent* event) { Q_UNUSED(event); @@ -1871,6 +1926,23 @@ void TaskSketcherElements::slotElementsChanged() assert(sketchView); // Build up ListView with the elements Sketcher::SketchObject* sketch = sketchView->getSketchObject(); + + // Pre-process constraints to identify grouped elements and their handles + const auto& constraints = sketch->Constraints.getValues(); + std::set groupedGeoIds; + std::map handleIdToType; + for (const auto* c : constraints) { + if (c->Type == Sketcher::Group || c->Type == Sketcher::Text) { + if (c->hasElement(0)) { + handleIdToType[c->getGeoId(0)] = c->Type; + } + // Elements from index 1 onwards are the members. + for (int j = 1; c->hasElement(j); ++j) { + groupedGeoIds.insert(c->getGeoId(j)); + } + } + } + const std::vector& vals = sketch->Geometry.getValues(); ui->listWidgetElements->clear(); @@ -1880,15 +1952,14 @@ void TaskSketcherElements::slotElementsChanged() using GeometryState = ElementItem::GeometryState; int i = 1; - for (std::vector::const_iterator it = vals.begin(); it != vals.end(); - ++it, ++i) { - Base::Type type = (*it)->getTypeId(); + for (auto* geo : vals) { + Base::Type type = geo->getTypeId(); GeometryState state = GeometryState::Normal; - bool construction = Sketcher::GeometryFacade::getConstruction(*it); - bool internalAligned = Sketcher::GeometryFacade::isInternalAligned(*it); + bool construction = Sketcher::GeometryFacade::getConstruction(geo); + bool internalAligned = Sketcher::GeometryFacade::isInternalAligned(geo); - auto layerId = getSafeGeomLayerId(*it); + auto layerId = getSafeGeomLayerId(geo); if (internalAligned) state = GeometryState::InternalAlignment; @@ -1903,6 +1974,67 @@ void TaskSketcherElements::slotElementsChanged() return QStringLiteral("(Edge%1#ID%2)").arg(i).arg(i - 1); }; + QString label; + // This is a regular geometry. Get its type name. + QString baseName; + if (type == Part::GeomPoint::getClassTypeId()) { + baseName = tr("Point"); + } + else if (type == Part::GeomLineSegment::getClassTypeId()) { + int geoId = i - 1; + auto handle_it = handleIdToType.find(geoId); + if (handle_it != handleIdToType.end()) { + // This is a group/text handle + if (handle_it->second == Sketcher::Group) { + baseName = tr("Group"); + } + else { + baseName = tr("Text"); + } + } + else { + baseName = tr("Line"); + } + } + else if (type == Part::GeomArcOfCircle::getClassTypeId()) { + baseName = tr("Arc"); + } + else if (type == Part::GeomCircle::getClassTypeId()) { + baseName = tr("Circle"); + } + else if (type == Part::GeomEllipse::getClassTypeId()) { + baseName = tr("Ellipse"); + } + else if (type == Part::GeomArcOfEllipse::getClassTypeId()) { + baseName = tr("Elliptical Arc"); + } + else if (type == Part::GeomArcOfHyperbola::getClassTypeId()) { + baseName = tr("Hyperbolic Arc"); + } + else if (type == Part::GeomArcOfParabola::getClassTypeId()) { + baseName = tr("Parabolic arc"); + } + else if (type == Part::GeomBSplineCurve::getClassTypeId()) { + baseName = tr("B-spline"); + } + else { + baseName = tr("Other"); + } + + // Reconstruct the label using the baseName. + if (isNamingBoxChecked) { + label = baseName + IdInformation(); + if (state == GeometryState::Construction) { + label += QStringLiteral("-") + tr("Construction"); + } + else if (state == GeometryState::InternalAlignment) { + label += QStringLiteral("-") + tr("Internal"); + } + } + else { + label = QStringLiteral("%1-").arg(i) + baseName; + } + auto* itemN = new ElementItem( i - 1, sketchView->getSketchObject()->getVertexIndexGeoPos(i - 1, Sketcher::PointPos::start), @@ -1910,82 +2042,15 @@ void TaskSketcherElements::slotElementsChanged() sketchView->getSketchObject()->getVertexIndexGeoPos(i - 1, Sketcher::PointPos::end), type, state, - type == Part::GeomPoint::getClassTypeId() - ? (isNamingBoxChecked ? (tr("Point") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Point"))) - : type == Part::GeomLineSegment::getClassTypeId() - ? (isNamingBoxChecked ? (tr("Line") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Line"))) - : type == Part::GeomArcOfCircle::getClassTypeId() - ? (isNamingBoxChecked ? (tr("Arc") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Arc"))) - : type == Part::GeomCircle::getClassTypeId() - ? (isNamingBoxChecked ? (tr("Circle") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Circle"))) - : type == Part::GeomEllipse::getClassTypeId() - ? (isNamingBoxChecked ? (tr("Ellipse") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Ellipse"))) - : type == Part::GeomArcOfEllipse::getClassTypeId() - ? (isNamingBoxChecked ? (tr("Elliptical Arc") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Elliptical arc"))) - : type == Part::GeomArcOfHyperbola::getClassTypeId() - ? (isNamingBoxChecked ? (tr("Hyperbolic Arc") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Hyperbolic arc"))) - : type == Part::GeomArcOfParabola::getClassTypeId() - ? (isNamingBoxChecked ? (tr("Parabolic Arc") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Parabolic arc"))) - : type == Part::GeomBSplineCurve::getClassTypeId() - ? (isNamingBoxChecked ? (tr("B-spline") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("B-spline"))) - : (isNamingBoxChecked ? (tr("Other") + IdInformation()) - + (construction - ? (QStringLiteral("-") + tr("Construction")) - : (internalAligned ? (QStringLiteral("-") + tr("Internal")) - : QStringLiteral(""))) - : (QStringLiteral("%1-").arg(i) + tr("Other"))), + label, sketchView); ui->listWidgetElements->addItem(itemN); elementMap[itemN->ElementNbr] = itemN; - setItemVisibility(itemN); + setItemVisibility(itemN, groupedGeoIds); + ++i; } const std::vector& ext_vals = @@ -2091,7 +2156,7 @@ void TaskSketcherElements::slotElementsChanged() elementMap[itemN->ElementNbr] = itemN; - setItemVisibility(itemN); + setItemVisibility(itemN, groupedGeoIds); } } } diff --git a/src/Mod/Sketcher/Gui/TaskSketcherElements.h b/src/Mod/Sketcher/Gui/TaskSketcherElements.h index 0d68df6e14..365c5caf36 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherElements.h +++ b/src/Mod/Sketcher/Gui/TaskSketcherElements.h @@ -84,6 +84,7 @@ protected Q_SLOTS: void doEqualConstraint(); void doSymmetricConstraint(); void doBlockConstraint(); + void doGroupConstraint(); void doLockConstraint(); void doHorizontalConstraint(); @@ -133,7 +134,7 @@ public: private: void slotElementsChanged(); void updateVisibility(); - void setItemVisibility(QListWidgetItem* item); + void setItemVisibility(QListWidgetItem* item, const std::set& groupedGeoIds); void clearWidget(); void createFilterButtonActions(); void createSettingsButtonActions(); @@ -143,6 +144,7 @@ public Q_SLOTS: void onListWidgetElementsItemPressed(QListWidgetItem* item); void onListWidgetElementsItemEntered(QListWidgetItem* item); void onListWidgetElementsMouseMoveOnItem(QListWidgetItem* item); + void onListWidgetItemActivated(QListWidgetItem* item); void onSettingsExtendedInformationChanged(); void onFilterBoxStateChanged(int val); void onListMultiFilterItemChanged(QListWidgetItem* item); diff --git a/src/Mod/Sketcher/Gui/Utils.cpp b/src/Mod/Sketcher/Gui/Utils.cpp index 1a79db76d7..7f6e4cfbfb 100644 --- a/src/Mod/Sketcher/Gui/Utils.cpp +++ b/src/Mod/Sketcher/Gui/Utils.cpp @@ -25,6 +25,9 @@ #include #include #include +#include +#include +#include #include #include @@ -994,3 +997,40 @@ int SketcherGui::indexOfGeoId(const std::vector& vec, int elem) } return -1; } + +QMap SketcherGui::findAvailableFontFiles() +{ + QMap fontMap; + QStringList fontPaths; + +#if defined(Q_OS_WIN) + fontPaths << QString::fromUtf8("C:/Windows/Fonts"); +#elif defined(Q_OS_MACOS) + fontPaths << QString::fromUtf8("/System/Library/Fonts") << QString::fromUtf8("/Library/Fonts") + << QDir::homePath() + QString::fromUtf8("/Library/Fonts"); +#else // Linux and other Unix-like systems + fontPaths << QString::fromUtf8("/usr/share/fonts") << QString::fromUtf8("/usr/local/share/fonts") + << QDir::homePath() + QString::fromUtf8("/.fonts"); +#endif + + for (const QString& path : fontPaths) { + if (!QDir(path).exists()) { + continue; + } + + QDirIterator it( + path, + QStringList() << QString::fromUtf8("*.ttf") << QString::fromUtf8("*.otf"), + QDir::Files, + QDirIterator::Subdirectories + ); + while (it.hasNext()) { + QString filePath = it.next(); + QFileInfo fileInfo(filePath); + // Use the base name as a "friendly name". + // We store in a map to avoid duplicates from different paths (e.g. ttf vs otf). + fontMap[fileInfo.baseName()] = filePath; + } + } + return fontMap; +} diff --git a/src/Mod/Sketcher/Gui/Utils.h b/src/Mod/Sketcher/Gui/Utils.h index cde12e77df..93538d4e21 100644 --- a/src/Mod/Sketcher/Gui/Utils.h +++ b/src/Mod/Sketcher/Gui/Utils.h @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include "AutoConstraint.h" #include "ViewProviderSketchGeometryExtension.h" @@ -248,6 +250,8 @@ inline void scrollTo(QListWidget* list, int i, bool select) } } +QMap findAvailableFontFiles(); + } // namespace SketcherGui /// converts a 2D vector into a 3D vector in the XY plane diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp index d13804cdde..f9e289f903 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp @@ -73,6 +73,7 @@ #include "DrawSketchHandler.h" #include "EditDatumDialog.h" +#include "EditTextDialog.h" #include "EditModeCoinManager.h" #include "SnapManager.h" #include "StyleParameters.h" @@ -1360,6 +1361,12 @@ void ViewProviderSketch::editDoubleClicked() EditDatumDialog editDatumDialog(this, id); editDatumDialog.exec(); } + else if (Constr->Type == Sketcher::Text) { + Gui::Command::openCommand( + QT_TRANSLATE_NOOP("Command", "Modify Text constraint")); + EditTextDialog editTextDialog(this, id); + editTextDialog.exec(); + } } } } @@ -1601,16 +1608,27 @@ void ViewProviderSketch::initDragging(int geoId, Sketcher::PointPos pos, Gui::Vi return; // don't drag externals } + // If we are trying to drag an edge that is in a group, we drag the group handle instead. + int oldgeoId = geoId; + geoId = getSketchObject()->getGroupHandleIfInGroup(geoId); + if (oldgeoId != geoId) { + // if replaced then we want to move the edge of the handle, not a point. + pos = PointPos::none; + } + drag.reset(); setSketchMode(STATUS_SKETCH_Drag); drag.Dragged.emplace_back(geoId, pos); // Adding selected geos that should be dragged as well. - for (auto& geoIdi : selection.SelCurvSet) { + for (auto geoIdi : selection.SelCurvSet) { if (geoIdi < 0) { continue; //skip externals } + // If in a group, we drag the group handle instead. + geoIdi = getSketchObject()->getGroupHandleIfInGroup(geoIdi); + if (geoIdi == geoId) { // geoId is already added because it was the preselected. // 2 cases : either the edge was added or a point of it. @@ -2281,6 +2299,16 @@ void ViewProviderSketch::onSelectionChanged(const Gui::SelectionChanges& msg) if (shapetype.size() > 4 && shapetype.substr(0, 4) == "Edge") { int GeoId = std::atoi(&shapetype[4]) - 1; selection.SelCurvSet.insert(GeoId); + + // Check if this is in a group. + // If so we cancel this addition and select the group instead + int handleId = getSketchObject()->getGroupHandleIfInGroup(GeoId); + if (handleId != GeoId) { + // Remove the selected edge + Gui::Selection().rmvSelection(msg.pDocName, msg.pObjectName, msg.pSubName); + std::string sub = "Edge" + std::to_string(handleId + 1); + Gui::Selection().addSelection(msg.pDocName, msg.pObjectName, sub.c_str()); + } } else if (shapetype.size() > 12 && shapetype.substr(0, 12) == "ExternalEdge") { int GeoId = std::atoi(&shapetype[12]) - 1; @@ -4120,6 +4148,8 @@ bool ViewProviderSketch::onDelete(const std::vector& subList) Gui::Selection().clearSelection(); resetPreselectPoint(); + const auto& constraints = getSketchObject()->Constraints.getValues(); + std::set delInternalGeometries, delExternalGeometries, delCoincidents, delConstraints; // go through the selected subelements for (std::vector::const_iterator it = SubNames.begin(); it != SubNames.end(); @@ -4128,6 +4158,18 @@ bool ViewProviderSketch::onDelete(const std::vector& subList) int GeoId = std::atoi(it->substr(4, 4000).c_str()) - 1; if (GeoId >= 0) { delInternalGeometries.insert(GeoId); + + // Handle group deletion + for (const auto* c : constraints) { + if ((c->Type == Sketcher::Text || c->Type == Sketcher::Group) + && c->hasElement(0) && c->getGeoId(0) == GeoId) { + // This is a group handle. Add all members to the delete list. + for (int j = 1; c->hasElement(j); ++j) { + delInternalGeometries.insert(c->getGeoId(j)); + } + break; // A geo can only be a handle for one constraint. + } + } } else delExternalGeometries.insert(Sketcher::GeoEnum::RefExt - GeoId); diff --git a/src/Mod/Sketcher/Gui/Workbench.cpp b/src/Mod/Sketcher/Gui/Workbench.cpp index a03a41802f..99dd8a3229 100644 --- a/src/Mod/Sketcher/Gui/Workbench.cpp +++ b/src/Mod/Sketcher/Gui/Workbench.cpp @@ -455,7 +455,8 @@ inline void SketcherAddWorkbenchGeometries(T& geom) SketcherAddWorkspaceRectangles(geom); SketcherAddWorkspaceRegularPolygon(geom); SketcherAddWorkspaceslots(geom); - geom << "Separator" + geom << "Sketcher_CreateText" + << "Separator" << "Sketcher_ToggleConstruction"; /*<< "Sketcher_CreateText"*/ /*<< "Sketcher_CreateDraftLine"*/; @@ -487,6 +488,7 @@ inline void SketcherAddWorkbenchConstraints(Gui::MenuItem& cons) << "Sketcher_ConstrainEqual" << "Sketcher_ConstrainSymmetric" << "Sketcher_ConstrainBlock" + << "Sketcher_ConstrainGroup" << "Separator" << "Sketcher_Dimension" << "Sketcher_ConstrainDistanceX" @@ -553,7 +555,8 @@ inline void SketcherAddWorkbenchConstraints(Gui::ToolBarItem& << "Sketcher_ConstrainTangent" << "Sketcher_ConstrainEqual" << "Sketcher_ConstrainSymmetric" - << "Sketcher_ConstrainBlock"; + << "Sketcher_ConstrainBlock" + << "Sketcher_ConstrainGroup"; cons << "Separator" << "Sketcher_CompToggleConstraints"; From fed11359d9631c1484f42ec562b73c40a99f3154 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Fri, 9 Jan 2026 10:22:22 +0100 Subject: [PATCH 2/8] Update SketcherToolDefaultWidget.h --- src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h index fafa5c77f5..9e30987fc5 100644 --- a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h +++ b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.h @@ -200,9 +200,9 @@ public: } template - boost::signals2::connection registerLineEditTextChanged(F&& fn) + fastsignals::advanced_connection registerLineEditTextChanged(F&& fn) { - return signalLineEditTextChanged.connect(std::forward(fn)); + return signalLineEditTextChanged.connect(std::forward(fn), fastsignals::advanced_tag()); } // Q_SIGNALS: @@ -247,11 +247,11 @@ private: private: std::unique_ptr ui; - fastsignals signalParameterTabOrEnterPressed; - fastsignals signalParameterValueChanged; - fastsignals signalCheckboxCheckedChanged; - fastsignals signalComboboxSelectionChanged; - fastsignals signalLineEditTextChanged; + fastsignals::signal signalParameterTabOrEnterPressed; + fastsignals::signal signalParameterValueChanged; + fastsignals::signal signalCheckboxCheckedChanged; + fastsignals::signal signalComboboxSelectionChanged; + fastsignals::signal signalLineEditTextChanged; /// lock to block QT slots bool blockParameterSlots; From 320bd611561eecb4e3ced9bafc42f69cbfc54c4a Mon Sep 17 00:00:00 2001 From: paddle Date: Mon, 9 Feb 2026 16:41:14 +0100 Subject: [PATCH 3/8] Sketcher: Text tool - review changes --- src/Mod/Part/App/Geometry.cpp | 99 +-- src/Mod/Sketcher/App/CMakeLists.txt | 4 + src/Mod/Sketcher/App/Constraint.cpp | 111 +++- src/Mod/Sketcher/App/Constraint.h | 11 +- src/Mod/Sketcher/App/ConstraintPyImp.cpp | 10 +- src/Mod/Sketcher/App/Sketch.cpp | 28 +- src/Mod/Sketcher/App/Sketch.h | 2 +- src/Mod/Sketcher/App/SketchObject.cpp | 92 ++- src/Mod/Sketcher/App/SketchObject.h | 9 +- src/Mod/Sketcher/App/SketchObjectPyImp.cpp | 10 +- src/Mod/Sketcher/Gui/CommandConstraints.cpp | 4 +- src/Mod/Sketcher/Gui/CommandConstraints.h | 3 +- src/Mod/Sketcher/Gui/ConstraintFilters.h | 28 +- src/Mod/Sketcher/Gui/DrawSketchHandlerText.h | 64 +- .../Gui/EditModeConstraintCoinManager.cpp | 19 +- src/Mod/Sketcher/Gui/EditTextDialog.cpp | 8 +- src/Mod/Sketcher/Gui/Resources/Sketcher.qrc | 2 + .../icons/constraints/Constraint_Group.svg | 138 +++- .../icons/constraints/Constraint_Text.svg | 151 +++-- .../Sketcher_Element_Text_EndPoint.svg | 587 +++++++++++++++++ .../Sketcher_Element_Text_StartPoint.svg | 597 ++++++++++++++++++ .../Gui/SketcherToolDefaultWidget.cpp | 4 + .../Sketcher/Gui/TaskSketcherConstraints.cpp | 6 +- src/Mod/Sketcher/Gui/TaskSketcherElements.cpp | 71 ++- src/Mod/Sketcher/Gui/TaskSketcherElements.h | 1 + src/Mod/Sketcher/Gui/ViewProviderSketch.cpp | 5 + src/Mod/Sketcher/Gui/ViewProviderSketch.h | 3 + .../Gui/ViewProviderSketchCoinAttorney.h | 12 + 28 files changed, 1831 insertions(+), 248 deletions(-) create mode 100644 src/Mod/Sketcher/Gui/Resources/icons/elements/Sketcher_Element_Text_EndPoint.svg create mode 100644 src/Mod/Sketcher/Gui/Resources/icons/elements/Sketcher_Element_Text_StartPoint.svg diff --git a/src/Mod/Part/App/Geometry.cpp b/src/Mod/Part/App/Geometry.cpp index 77d40a8a19..6d465f5551 100644 --- a/src/Mod/Part/App/Geometry.cpp +++ b/src/Mod/Part/App/Geometry.cpp @@ -105,6 +105,7 @@ #include #include #include +#include #if OCC_VERSION_HEX < 0x070600 # include @@ -7344,6 +7345,7 @@ struct FTDC_Ctx std::vector Wires; std::vector Edges; FT_Vector LastVert; + FT_Vector StartVert; Handle(Geom_Surface) surf; }; @@ -7360,6 +7362,11 @@ TopoDS_Wire edgesToWire(std::vector& Edges) if (mkWire.IsDone()) { TopoDS_Wire wire = mkWire.Wire(); BRepLib::BuildCurves3d(wire); + // Ensure the wire is topologically closed and valid + ShapeFix_Wire sfw; + sfw.Load(wire); + sfw.FixClosed(); + wire = sfw.Wire(); return wire; } else { @@ -7367,18 +7374,37 @@ TopoDS_Wire edgesToWire(std::vector& Edges) return TopoDS_Wire(); } } +// Helper to close the current contour if needed and flush to Wires list +void flushContour(FTDC_Ctx* dc) +{ + if (dc->Edges.empty()) { + return; + } + + // Check if the contour is geometrically closed. + // If not, add a closing segment from LastVert to StartVert. + gp_Pnt2d pStart(dc->StartVert.x, dc->StartVert.y); + gp_Pnt2d pEnd(dc->LastVert.x, dc->LastVert.y); + + if (!pStart.IsEqual(pEnd, Precision::Confusion())) { + Handle(Geom2d_TrimmedCurve) lseg = GCE2d_MakeSegment(pEnd, pStart); + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(lseg, dc->surf); + dc->Edges.push_back(edge); + } + + TopoDS_Wire newWire = edgesToWire(dc->Edges); + if (!newWire.IsNull()) { + dc->Wires.push_back(newWire); + } + dc->Edges.clear(); +} // FT Decompose callbacks int move_cb(const FT_Vector* pt, void* p) { FTDC_Ctx* dc = static_cast(p); - if (!dc->Edges.empty()) { - TopoDS_Wire newWire = edgesToWire(dc->Edges); - if (!newWire.IsNull()) { - dc->Wires.push_back(newWire); - } - dc->Edges.clear(); - } + flushContour(dc); + dc->StartVert = *pt; dc->LastVert = *pt; return 0; } @@ -7622,12 +7648,19 @@ std::vector makeTextWires( return allWires; } - FT_Set_Char_Size(ftFace, 0, 48 * 64 * 10, 0, 0); - double scaleFactor = (height / static_cast(ftFace->height)) / 10.0; + // Use the font's native units for maximum precision + double unitsPerEM = static_cast(ftFace->units_per_EM); + if (unitsPerEM < 1.0) { + unitsPerEM = 2048.0; // Fallback + } + + // We want a nominal height of 1.0 for the base shapes + double scaleFactor = (height / unitsPerEM); FT_Outline_Funcs ftCallbacks = {move_cb, line_cb, quad_cb, cubic_cb, 0, 0}; - FT_UInt ftLoadFlags = FT_LOAD_DEFAULT | FT_LOAD_NO_BITMAP; + FT_UInt ftLoadFlags = FT_LOAD_NO_SCALE | FT_LOAD_NO_BITMAP; double penPos = 0.0; + double currentTracking = 0.0; FT_ULong prevCharcode = 0; std::wstring_convert, char32_t> converter; @@ -7635,13 +7668,6 @@ std::vector makeTextWires( for (size_t i = 0; i < wide_text.length(); ++i) { FT_ULong charcode = wide_text[i]; - if (charcode == ' ') { - if (FT_Load_Char(ftFace, charcode, ftLoadFlags) == 0) { - penPos += ftFace->glyph->advance.x; - } - prevCharcode = charcode; - continue; - } if (FT_Load_Char(ftFace, charcode, ftLoadFlags) != 0) { continue; @@ -7653,37 +7679,38 @@ std::vector makeTextWires( ftFace, FT_Get_Char_Index(ftFace, prevCharcode), FT_Get_Char_Index(ftFace, charcode), - FT_KERNING_DEFAULT, + FT_KERNING_UNSCALED, &kern ); penPos += kern.x; } - FTDC_Ctx ctx; - ctx.surf = new Geom_Plane(gp::Origin(), gp::DZ()); - FT_Outline_Decompose(&ftFace->glyph->outline, &ftCallbacks, &ctx); + if (ftFace->glyph->format == FT_GLYPH_FORMAT_OUTLINE + && ftFace->glyph->outline.n_contours > 0) { + FTDC_Ctx ctx; + ctx.surf = new Geom_Plane(gp::Origin(), gp::DZ()); + FT_Outline_Decompose(&ftFace->glyph->outline, &ftCallbacks, &ctx); - if (!ctx.Edges.empty()) { - TopoDS_Wire lastWire = edgesToWire(ctx.Edges); - if (!lastWire.IsNull()) { - ctx.Wires.push_back(lastWire); - } - } + flushContour(&ctx); - gp_Trsf charTransform; - charTransform.SetScale(gp::Origin(), scaleFactor); - gp_Vec translation(penPos * scaleFactor + i * tracking, 0.0, 0.0); - charTransform.SetTranslationPart(translation); + if (!ctx.Wires.empty()) { + gp_Trsf charTransform; + charTransform.SetScale(gp::Origin(), scaleFactor); + gp_Vec translation(penPos * scaleFactor + currentTracking, 0.0, 0.0); + charTransform.SetTranslationPart(translation); - for (const auto& wire : ctx.Wires) { - BRepBuilderAPI_Transform performer(charTransform); - performer.Perform(wire, true); // true = create a copy - if (performer.IsDone()) { - allWires.push_back(performer.Shape()); + for (const auto& wire : ctx.Wires) { + BRepBuilderAPI_Transform performer(charTransform); + performer.Perform(wire, true); // true = create a copy + if (performer.IsDone()) { + allWires.push_back(performer.Shape()); + } + } } } penPos += ftFace->glyph->advance.x; + currentTracking += tracking; prevCharcode = charcode; } diff --git a/src/Mod/Sketcher/App/CMakeLists.txt b/src/Mod/Sketcher/App/CMakeLists.txt index cd8d86c0bc..54ddd26ac8 100644 --- a/src/Mod/Sketcher/App/CMakeLists.txt +++ b/src/Mod/Sketcher/App/CMakeLists.txt @@ -1,4 +1,8 @@ # SPDX-License-Identifier: LGPL-2.1-or-later +include_directories( + SYSTEM + ${CMAKE_SOURCE_DIR}/src/3rdParty/json/single_include/nlohmann/ +) add_library(Sketcher SHARED) diff --git a/src/Mod/Sketcher/App/Constraint.cpp b/src/Mod/Sketcher/App/Constraint.cpp index fb69d03a82..505080ac31 100644 --- a/src/Mod/Sketcher/App/Constraint.cpp +++ b/src/Mod/Sketcher/App/Constraint.cpp @@ -31,6 +31,8 @@ #include #include +#include "json.hpp" + #include #include @@ -88,9 +90,9 @@ Constraint* Constraint::copy() const temp->isInVirtualSpace = this->isInVirtualSpace; temp->isVisible = this->isVisible; temp->isActive = this->isActive; - temp->isTextHeight = this->isTextHeight; temp->elements = this->elements; // Do not copy tag, otherwise it is considered a clone, and a "rename" by the expression engine. + temp->MetaData = this->MetaData; #if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS temp->First = this->First; @@ -150,12 +152,10 @@ unsigned int Constraint::getMemSize() const void Constraint::Save(Writer& writer) const { std::string encodeName = encodeAttribute(Name); - std::string encodeText = encodeAttribute(Text); - std::string encodeFont = encodeAttribute(Font); + std::string encodeMetaData = encodeAttribute(MetaData); writer.Stream() << writer.ind() << "Type == InternalAlignment) { writer.Stream() << "InternalAlignmentType=\"" << (int)AlignmentType << "\" " @@ -167,8 +167,7 @@ void Constraint::Save(Writer& writer) const << "IsDriving=\"" << (int)isDriving << "\" " << "IsInVirtualSpace=\"" << (int)isInVirtualSpace << "\" " << "IsVisible=\"" << (int)isVisible << "\" " - << "IsActive=\"" << (int)isActive << "\" " - << "IsTextHeight=\"" << (int)isTextHeight << "\" "; + << "IsActive=\"" << (int)isActive << "\" "; // Save elements { @@ -201,8 +200,7 @@ void Constraint::Restore(XMLReader& reader) { reader.readElement("Constrain"); Name = reader.getAttribute("Name"); - Text = reader.hasAttribute("Text") ? reader.getAttribute("Text") : ""; - Font = reader.hasAttribute("Font") ? reader.getAttribute("Font") : ""; + MetaData = reader.hasAttribute("MetaData") ? reader.getAttribute("MetaData") : ""; Type = reader.getAttribute("Type"); Value = reader.getAttribute("Value"); @@ -242,10 +240,6 @@ void Constraint::Restore(XMLReader& reader) isActive = reader.getAttribute("IsActive"); } - if (reader.hasAttribute("IsTextHeight")) { - isTextHeight = reader.getAttribute("IsTextHeight"); - } - if (reader.hasAttribute("ElementIds") && reader.hasAttribute("ElementPositions")) { auto splitAndClean = [](std::string_view input) { const char delimiter = ' '; @@ -600,3 +594,94 @@ void Constraint::truncateElements(size_t newSize) elements.resize(newSize); } } + +std::string Constraint::getText() const +{ + if (MetaData.empty()) { + return {}; + } + try { + auto j = nlohmann::json::parse(MetaData); + if (j.contains("text")) { + return j["text"].get(); + } + } + catch (...) { + // Handle JSON parsing errors or type mismatches silently + } + return {}; +} + +void Constraint::setText(const std::string& text) +{ + nlohmann::json j; + if (!MetaData.empty()) { + try { + j = nlohmann::json::parse(MetaData); + } + catch (...) { + } + } + j["text"] = text; + MetaData = j.dump(); +} + +std::string Constraint::getFont() const +{ + if (MetaData.empty()) { + return {}; + } + try { + auto j = nlohmann::json::parse(MetaData); + if (j.contains("font")) { + return j["font"].get(); + } + } + catch (...) { + } + return {}; +} + +void Constraint::setFont(const std::string& font) +{ + nlohmann::json j; + if (!MetaData.empty()) { + try { + j = nlohmann::json::parse(MetaData); + } + catch (...) { + } + } + j["font"] = font; + MetaData = j.dump(); +} + +bool Constraint::getIsTextHeight() const +{ + if (MetaData.empty()) { + return true; // Default value + } + try { + auto j = nlohmann::json::parse(MetaData); + if (j.contains("isTextHeight")) { + return j["isTextHeight"].get(); + } + } + catch (...) { + } + return true; // Default value +} + +void Constraint::setIsTextHeight(bool isHeight) +{ + nlohmann::json j; + if (!MetaData.empty()) { + try { + j = nlohmann::json::parse(MetaData); + } + catch (...) { + } + } + j["isTextHeight"] = isHeight; + MetaData = j.dump(); +} diff --git a/src/Mod/Sketcher/App/Constraint.h b/src/Mod/Sketcher/App/Constraint.h index 42feb9c494..6b0d2c4a07 100644 --- a/src/Mod/Sketcher/App/Constraint.h +++ b/src/Mod/Sketcher/App/Constraint.h @@ -216,8 +216,7 @@ public: ConstraintType Type {None}; InternalAlignmentType AlignmentType {Undef}; std::string Name; - std::string Text; - std::string Font; + std::string MetaData; float LabelDistance {10.F}; float LabelPosition {0.F}; bool isDriving {true}; @@ -228,7 +227,6 @@ public: bool isVisible {true}; bool isActive {true}; - bool isTextHeight {true}; GeoElementId getElement(size_t index) const; void setElement(size_t index, GeoElementId element); @@ -246,6 +244,13 @@ public: void swapElements(int index1, int index2); bool ensureElementExists(int index); + std::string getText() const; + void setText(const std::string& text); + std::string getFont() const; + void setFont(const std::string& font); + bool getIsTextHeight() const; + void setIsTextHeight(bool val); + #ifdef SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS // Deprecated, use getElement/setElement instead int First {GeoEnum::GeoUndef}; diff --git a/src/Mod/Sketcher/App/ConstraintPyImp.cpp b/src/Mod/Sketcher/App/ConstraintPyImp.cpp index 6935f0cbce..a547c45115 100644 --- a/src/Mod/Sketcher/App/ConstraintPyImp.cpp +++ b/src/Mod/Sketcher/App/ConstraintPyImp.cpp @@ -120,6 +120,8 @@ int ConstraintPy::PyInit(PyObject* args, PyObject* /*kwd*/) return false; // Failure } + constr_ptr->truncateElements(0); + for (Py_ssize_t i = 0; i < list_size; i += 2) { PyObject* py_geoId_obj = PyList_GetItem(list, i); PyObject* py_posId_obj = PyList_GetItem(list, i + 1); @@ -177,15 +179,15 @@ int ConstraintPy::PyInit(PyObject* args, PyObject* /*kwd*/) } // Set the specific members for the Text constraint - constraint->Text = text_str; - constraint->Font = font_str; + constraint->setText(text_str); + constraint->setFont(font_str); // Check and set the optional boolean if (py_is_height && PyBool_Check(py_is_height)) { - constraint->isTextHeight = (py_is_height == Py_True); + constraint->setIsTextHeight(py_is_height == Py_True); } else { - constraint->isTextHeight = true; + constraint->setIsTextHeight(true); } return 0; // Success! diff --git a/src/Mod/Sketcher/App/Sketch.cpp b/src/Mod/Sketcher/App/Sketch.cpp index 058dbbb5e3..e3839b3b3f 100644 --- a/src/Mod/Sketcher/App/Sketch.cpp +++ b/src/Mod/Sketcher/App/Sketch.cpp @@ -198,12 +198,12 @@ int Sketch::setUpSketch( clear(); // The geometries that are in groups are going to be ignored by the solver. - std::set slaveGeoIds; + std::set inGroupGeoIds; for (const auto& c : ConstraintList) { if (c->Type == Group || c->Type == Text) { // Start from index 1, as 0 is the frame. for (int i = 1; c->hasElement(i); ++i) { - slaveGeoIds.insert(c->getGeoId(i)); + inGroupGeoIds.insert(c->getGeoId(i)); } } } @@ -259,7 +259,7 @@ int Sketch::setUpSketch( buildInternalAlignmentGeometryMap(ConstraintList); - addGeometry(intGeoList, onlyBlockedGeometry, slaveGeoIds); + addGeometry(intGeoList, onlyBlockedGeometry, inGroupGeoIds); int extStart = Geoms.size(); addGeometry(extGeoList, true); int extEnd = Geoms.size() - 1; @@ -277,9 +277,8 @@ int Sketch::setUpSketch( continue; } - bool hasSlaveReference = false; for (int j = 0; c->hasElement(j); ++j) { - if (slaveGeoIds.count(c->getGeoId(j))) { + if (inGroupGeoIds.count(c->getGeoId(j))) { unenforceableConstraints[i] = true; break; } @@ -762,7 +761,7 @@ int Sketch::addGeometry(const std::vector& geos, bool fixed) int Sketch::addGeometry( const std::vector& geos, const std::vector& blockedGeometry, - const std::set& slaveGeoIds + const std::set& inGroupGeoIds ) { assert(geos.size() == blockedGeometry.size()); @@ -777,8 +776,8 @@ int Sketch::addGeometry( ++it, ++bit, ++geoIdCounter) { // Check if the current geometry is in group. - bool isSlave = slaveGeoIds.count(geoIdCounter); - if (isSlave) { + bool isInGroup = inGroupGeoIds.count(geoIdCounter); + if (isInGroup) { GeoDef def; def.geo = (*it)->clone(); Geoms.push_back(def); @@ -1909,6 +1908,7 @@ int Sketch::checkGeoId(int geoId) const geoId += Geoms.size(); // convert negative external-geometry index to index into Geoms } if (!(geoId >= 0 && geoId < int(Geoms.size()))) { + Base::Console().warning("geoId %d Geoms.size %d\n", geoId, int(Geoms.size())); throw Base::IndexError("Sketch::checkGeoId. GeoId index out range."); } return geoId; @@ -5665,18 +5665,20 @@ void Sketch::applyGroupTransformations() T2[2][3] = postSolveFrame.startPoint.z; // 5. Combine the matrices in the correct order: T_final = T2 * R * S * T1 - // The * operator is overloaded for matrix multiplication. Base::Matrix4D transform = T2 * R * S * T1; - // --- Loop through slave elements and apply the transform --- + // --- Loop through grouped elements and apply the transform --- for (int i = 1; c->hasElement(i); ++i) { - int slaveGeoId = c->getGeoId(i); + int groupedGeoId = c->getGeoId(i); + if (groupedGeoId == GeoEnum::GeoUndef) { + continue; + } // Get the slave's current (pre-solve) state - Part::Geometry* slaveGeo = Geoms[checkGeoId(slaveGeoId)].geo; + Part::Geometry* groupedGeo = Geoms[checkGeoId(groupedGeoId)].geo; // Apply the calculated transformation - slaveGeo->transform(transform); + groupedGeo->transform(transform); } } diff --git a/src/Mod/Sketcher/App/Sketch.h b/src/Mod/Sketcher/App/Sketch.h index bfd175d5aa..99a4a44a2e 100644 --- a/src/Mod/Sketcher/App/Sketch.h +++ b/src/Mod/Sketcher/App/Sketch.h @@ -91,7 +91,7 @@ public: int addGeometry( const std::vector& geos, const std::vector& blockedGeometry, - const std::set& slaveGeoIds + const std::set& inGroupGeoIds ); /// get boolean list indicating whether the geometry is to be blocked or not void getBlockedGeometry( diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 1f072e79a3..46603eabb8 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -906,7 +906,7 @@ double SketchObject::getDatum(int ConstrId) const return this->Constraints[ConstrId]->getValue(); } -int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string& newFont) +int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string& newFont, bool isConstruction) { ; // no need to check input data validity as this is an sketchobject managed operation. Base::StateLocker lock(managedoperation, true); @@ -921,32 +921,39 @@ int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string } auto* constr = vals[ConstrId]; - if (constr->Type != Text || !constr->hasElement(1)) { + if (constr->Type != Text || !constr->hasElement(0)) { return -1; } // First we replace the old geometries by the new text. - const bool isHeight = constr->isTextHeight; - const std::string oldText = constr->Text; - const std::string oldFont = constr->Font; + const bool isHeight = constr->getIsTextHeight(); + const std::string oldText = constr->getText(); + const std::string oldFont = constr->getFont(); int handleGeoId = constr->getGeoId(0); int firstTextGeoId = constr->getGeoId(1); + bool hasExistingText = firstTextGeoId != GeoEnum::GeoUndef; bool handleLast = handleGeoId > firstTextGeoId; - // Check if text is construction or normal geos - auto* geo1 = getGeometry(firstTextGeoId); - bool isConstruction = GeometryFacade::getConstruction(geo1); + if (hasExistingText) { + // Check if text is construction or normal geos + auto* geo1 = getGeometry(firstTextGeoId); + isConstruction = GeometryFacade::getConstruction(geo1); - // Delete all the old text geos. Not the handle! - std::vector geoIdsToDelete; - for (int i = 1; constr->hasElement(i); ++i) { - geoIdsToDelete.push_back(constr->getGeoId(i)); - if (handleLast) { - --handleGeoId; // handle line is added after all text geos. + // Delete all the old text geos. Not the handle! + std::vector geoIdsToDelete; + for (int i = 1; constr->hasElement(i); ++i) { + if (constr->getGeoId(i) == GeoEnum::GeoUndef) { + continue; + } + geoIdsToDelete.push_back(constr->getGeoId(i)); + if (handleLast) { + --handleGeoId; // handle line is added after all text geos. + } } + + delGeometries(geoIdsToDelete); } - delGeometries(geoIdsToDelete); auto* line = dynamic_cast(getGeometry(handleGeoId)); if (!line) { return -1; @@ -960,11 +967,6 @@ int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string line->getStartPoint(), line->getEndPoint(), isHeight); - if (isConstruction) { - for (size_t i = 0; i < newGeos.size(); ++i) { - Sketcher::GeometryFacade::setConstruction(newGeos[i].get(), isConstruction); - } - } // Add the geometries to sketch int lastGeoid = getHighestCurveIndex(); @@ -984,26 +986,32 @@ int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string newGeos.clear(); addGeometry(newGeosRawPtrs); - // Create a new constraint to replace the one that was deleted. int newLastGeoid = getHighestCurveIndex(); - constr = new Constraint(); - constr->Type = Text; - constr->truncateElements(0); // remove the First/Second/Third that are created automatically - constr->addElement(GeoElementId(handleGeoId)); + + // If there was text geos, they were deleted, which deleted the text constraint. + // In this case create a new constraint to replace it. + if (hasExistingText) { + constr = new Constraint(); + constr->Type = Text; + constr->truncateElements(0); // remove the First/Second/Third that are created automatically + constr->addElement(GeoElementId(handleGeoId)); + } for (int i = lastGeoid + 1; i <= newLastGeoid; ++i) { constr->addElement(GeoElementId(i)); } - constr->Text = newText; - constr->Font = newFont; - constr->isTextHeight = isHeight; + constr->setText(newText); + constr->setFont(newFont); + constr->setIsTextHeight(isHeight); - addConstraint(constr); + if (hasExistingText) { + addConstraint(constr); + } int err = solve(); if (err) { - constr->Text = oldText; - constr->Font = oldFont; + constr->setText(oldText); + constr->setFont(oldFont); } return err; @@ -1109,6 +1117,26 @@ int SketchObject::getActive(int ConstrId, bool& isactive) return 0; } +bool SketchObject::isConstraintActiveInSketch(const Sketcher::Constraint* cstr) const +{ + // If the constraint is deactivated then it's over + if (!cstr || !cstr->isActive) { + return false; + } + + if (cstr->Type == Group || cstr->Type == Text) { + return true; + } + + // If the constraint is not deactivated, it could still constraint something in a group + for (int j = 0; cstr->hasElement(j); ++j) { + if (isInGroup(cstr->getGeoId(j), false)) { + return false; + } + } + return true; +} + int SketchObject::toggleActive(int ConstrId) { // no need to check input data validity as this is an sketchobject managed operation. @@ -10215,10 +10243,10 @@ bool SketchObject::evaluateConstraint(const Constraint* constraint) const case Equal: case PointOnObject: case Angle: + case Text: break; case Tangent: case Group: - case Text: requireSecond = true; break; case Symmetric: diff --git a/src/Mod/Sketcher/App/SketchObject.h b/src/Mod/Sketcher/App/SketchObject.h index dba65ceb69..ee87d55ca5 100644 --- a/src/Mod/Sketcher/App/SketchObject.h +++ b/src/Mod/Sketcher/App/SketchObject.h @@ -354,7 +354,12 @@ public: /// get the datum of a Distance or Angle constraint double getDatum(int ConstrId) const; /// set the text and font of a text constraint - int setTextAndFont(int ConstrId, std::string& newText, std::string& newFont); + int setTextAndFont( + int ConstrId, + std::string& newText, + std::string& newFont, + bool isConstruction = false + ); /// set the driving status of this constraint and solve int setDriving(int ConstrId, bool isdriving); /// get the driving status of this constraint @@ -369,6 +374,8 @@ public: int setActive(int ConstrId, bool isactive); /// get the driving status of this constraint int getActive(int ConstrId, bool& isactive); + // Return true if the constraint is active, includes checking if it's not in a group + bool isConstraintActiveInSketch(const Sketcher::Constraint* cstr) const; /// toggle the driving status of this constraint int toggleActive(int ConstrId); diff --git a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp index 6851756d51..9d68597d7b 100644 --- a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp +++ b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp @@ -762,17 +762,19 @@ PyObject* SketchObjectPy::setTextAndFont(PyObject* args, PyObject* kwd) char* textStr; char* fontStr; char* constrName = nullptr; + PyObject* isConstrObj = Py_False; // Default to null (parameter not provided) - // Try to parse (int, str, str) - if (PyArg_ParseTuple(args, "iss", &constrIndex, &textStr, &fontStr)) { - // This format is valid, proceed. + // "iss|O" means: int, string, string, | optional Object + if (!PyArg_ParseTuple(args, "iss|O!", &constrIndex, &textStr, &fontStr, &PyBool_Type, &isConstrObj)) { + return nullptr; } std::string text(textStr); std::string font(fontStr); // Call the C++ implementation - int err = this->getSketchObjectPtr()->setTextAndFont(constrIndex, text, font); + int err = this->getSketchObjectPtr() + ->setTextAndFont(constrIndex, text, font, Base::asBoolean(isConstrObj)); // Handle errors returned from the C++ function if (err) { diff --git a/src/Mod/Sketcher/Gui/CommandConstraints.cpp b/src/Mod/Sketcher/Gui/CommandConstraints.cpp index 15e01d3800..3fb5733492 100644 --- a/src/Mod/Sketcher/Gui/CommandConstraints.cpp +++ b/src/Mod/Sketcher/Gui/CommandConstraints.cpp @@ -10535,7 +10535,6 @@ void CmdSketcherConstrainGroup::activated(int iMsg) getSelection().clearSelection(); } -namespace { /** * @brief Escapes a string for safe embedding within a single-quoted Python string literal. * @@ -10544,7 +10543,7 @@ namespace { * @param input The raw string to escape. * @return A new string with special characters escaped. */ -std::string escapeForPython(const std::string& input) +std::string SketcherGui::escapeForPython(const std::string& input) { std::string result; // Pre-allocating can be a small optimization if strings are long @@ -10561,7 +10560,6 @@ std::string escapeForPython(const std::string& input) } return result; } -} // anonymous namespace bool SketcherGui::addListConstraint(Sketcher::SketchObject* Obj, std::vector& elts, diff --git a/src/Mod/Sketcher/Gui/CommandConstraints.h b/src/Mod/Sketcher/Gui/CommandConstraints.h index 8f8eefe26e..0739a7cc99 100644 --- a/src/Mod/Sketcher/Gui/CommandConstraints.h +++ b/src/Mod/Sketcher/Gui/CommandConstraints.h @@ -137,7 +137,7 @@ void doEndpointToEdgeTangency( /// notifications void notifyConstraintSubstitutions(const QString& message); - +std::string escapeForPython(const std::string& input); bool addListConstraint( Sketcher::SketchObject* Obj, std::vector& elts, @@ -148,5 +148,4 @@ bool addListConstraint( const std::string& text = "", const std::string& font = "" ); - } // namespace SketcherGui diff --git a/src/Mod/Sketcher/Gui/ConstraintFilters.h b/src/Mod/Sketcher/Gui/ConstraintFilters.h index d83c2a73be..3a18605ebc 100644 --- a/src/Mod/Sketcher/Gui/ConstraintFilters.h +++ b/src/Mod/Sketcher/Gui/ConstraintFilters.h @@ -54,20 +54,20 @@ enum class FilterValue Equality = 9, Symmetric = 10, Block = 11, - Group = 12, - Text = 13, - InternalAlignment = 14, - Datums = 15, - HorizontalDistance = 16, - VerticalDistance = 17, - Distance = 18, - Radius = 19, - Weight = 20, - Diameter = 21, - Angle = 22, - SnellsLaw = 23, - Named = 24, - NonDriving = 25, + InternalAlignment = 12, + Datums = 13, + HorizontalDistance = 14, + VerticalDistance = 15, + Distance = 16, + Radius = 17, + Weight = 18, + Diameter = 19, + Angle = 20, + SnellsLaw = 21, + Named = 22, + NonDriving = 23, + Group = 24, + Text = 25, NumFilterValue // SpecialFilterValue shall start at the same index as this }; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h index 54ea9f2d11..b078962fc4 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h @@ -128,29 +128,53 @@ private: void executeCommands() override { try { - int firstCurve = getHighestCurveIndex() + 1; - - createShape(false); - Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add sketch Text")); - commandAddShapeGeometryAndConstraints(); + // Add the Handle Line + Gui::cmdAppObjectArgs( + getSketchObject(), + "addGeometry(Part.LineSegment(App.Vector(%f, %f,0), App.Vector(%f, %f,0)), True)", + startPoint.x, + startPoint.y, + endPoint.x, + endPoint.y + ); + handleId = getHighestCurveIndex(); - handleId = getHighestCurveIndex() + 1; // line is not added yet - - std::vector elts; - for (int i = firstCurve; i < handleId; ++i) { - elts.push_back(GeoElementId(i)); - } + std::string escText = escapeForPython(text); + std::string escFont = escapeForPython(font); bool isHeight = constructionMethod() == ConstructionMethod::Height; - if (!addListConstraint(getSketchObject(), elts, "Text", startPoint, endPoint, isHeight, text, font)) { - Gui::Command::abortCommand(); - return; - } + const char* constrBoolStr = isConstructionMode() ? "True" : "False"; + + // Add the 'Text' Constraint (Empty) + // We initialize the constraint containing ONLY the handle (element 0). + // We do not add the text geometry manually to avoid floating-point precision loss + // associated with Python serialization. + Gui::cmdAppObjectArgs( + getSketchObject(), + "addConstraint(Sketcher.Constraint('Text', [%d, 0], '%s', '%s', %s))", + handleId, + escText.c_str(), + escFont.c_str(), + isHeight ? "True" : "False" + ); + + // Generate Text Geometry by calling setTextAndFont on the new constraint. + // This triggers the C++ logic to generate the exact geometry and insert it + // into the sketch, ensuring closed wires and perfect precision. + Gui::cmdAppObjectArgs( + getSketchObject(), + "setTextAndFont(len(App.ActiveDocument.getObject('%s').Constraints)-1, '%s', '%s', " + "%s)", + getSketchObject()->getNameInDocument(), + escText.c_str(), + escFont.c_str(), + constrBoolStr + ); Gui::Command::commitCommand(); } - catch (const Base::Exception&) { + catch (const Base::Exception& e) { Gui::NotifyError( sketchgui, QT_TRANSLATE_NOOP("Notifications", "Error"), @@ -158,14 +182,6 @@ private: ); Gui::Command::abortCommand(); - THROWM( - Base::RuntimeError, - QT_TRANSLATE_NOOP( - "Notifications", - "Tool execution aborted" - ) "\n" - ) // This prevents constraints from being - // applied on non existing geometry } } diff --git a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp index 87bb532495..283a5d617c 100644 --- a/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp +++ b/src/Mod/Sketcher/Gui/EditModeConstraintCoinManager.cpp @@ -1879,12 +1879,16 @@ void EditModeConstraintCoinManager::updateConstraintColor( } } else { + bool isActive = ViewProviderSketchCoinAttorney::isConstraintActiveInSketch( + viewProvider, + constraint + ); if (hasDatumLabel) { SoDatumLabel* l = static_cast( s->getChild(static_cast(ConstraintNodePosition::DatumLabelIndex)) ); - l->textColor = constraint->isActive + l->textColor = isActive ? ViewProviderSketchCoinAttorney::constraintHasExpression(viewProvider, i) ? drawingParameters.ExprBasedConstrDimColor : (constraint->isDriving ? drawingParameters.ConstrDimColor @@ -1892,7 +1896,7 @@ void EditModeConstraintCoinManager::updateConstraintColor( : drawingParameters.DeactivatedConstrDimColor; } else if (hasMaterial) { - m->diffuseColor = constraint->isActive + m->diffuseColor = isActive ? (constraint->isDriving ? drawingParameters.ConstrDimColor : drawingParameters.NonDrivingConstrDimColor) : drawingParameters.DeactivatedConstrDimColor; @@ -1968,7 +1972,8 @@ void EditModeConstraintCoinManager::rebuildConstraintNodes( // every constrained visual node gets its own material for preselection and selection SoMaterial* mat = new SoMaterial; mat->ref(); - mat->diffuseColor = (*it)->isActive + bool isActive = ViewProviderSketchCoinAttorney::isConstraintActiveInSketch(viewProvider, *it); + mat->diffuseColor = isActive ? ((*it)->isDriving ? drawingParameters.ConstrDimColor : drawingParameters.NonDrivingConstrDimColor) : drawingParameters.DeactivatedConstrDimColor; @@ -1986,7 +1991,7 @@ void EditModeConstraintCoinManager::rebuildConstraintNodes( SoDatumLabel* text = new SoDatumLabel(); text->norm.setValue(norm); text->string = ""; - text->textColor = (*it)->isActive + text->textColor = isActive ? ((*it)->isDriving ? drawingParameters.ConstrDimColor : drawingParameters.NonDrivingConstrDimColor) : drawingParameters.DeactivatedConstrDimColor; @@ -2970,6 +2975,10 @@ QColor EditModeConstraintCoinManager::constrColor(int constraintId) }; const auto constraints = ViewProviderSketchCoinAttorney::getConstraints(viewProvider); + bool isActive = ViewProviderSketchCoinAttorney::isConstraintActiveInSketch( + viewProvider, + constraints[constraintId] + ); if (ViewProviderSketchCoinAttorney::isConstraintPreselected(viewProvider, constraintId)) { return toQColor(drawingParameters.PreselectColor); @@ -2977,7 +2986,7 @@ QColor EditModeConstraintCoinManager::constrColor(int constraintId) else if (ViewProviderSketchCoinAttorney::isConstraintSelected(viewProvider, constraintId)) { return toQColor(drawingParameters.SelectColor); } - else if (!constraints[constraintId]->isActive) { + else if (!isActive) { return toQColor(drawingParameters.DeactivatedConstrDimColor); } else if (!constraints[constraintId]->isDriving) { diff --git a/src/Mod/Sketcher/Gui/EditTextDialog.cpp b/src/Mod/Sketcher/Gui/EditTextDialog.cpp index 341c1c904b..2b968239bb 100644 --- a/src/Mod/Sketcher/Gui/EditTextDialog.cpp +++ b/src/Mod/Sketcher/Gui/EditTextDialog.cpp @@ -45,15 +45,17 @@ EditTextDialog::EditTextDialog(ViewProviderSketch* viewProvider, int constraintI { ui->setupUi(this); + ui->comboBox_font->setMaxVisibleItems(20); + const Sketcher::SketchObject* sketch = sketchView->getSketchObject(); const Sketcher::Constraint* constraint = sketch->Constraints[constrIndex]; // Initialize Text - ui->lineEdit_text->setText(QString::fromStdString(constraint->Text)); + ui->lineEdit_text->setText(QString::fromStdString(constraint->getText())); // Initialize Font populateFontList(); - QString currentFontName = findFontNameFromPath(QString::fromStdString(constraint->Font)); + QString currentFontName = findFontNameFromPath(QString::fromStdString(constraint->getFont())); if (!currentFontName.isEmpty()) { ui->comboBox_font->setCurrentText(currentFontName); } @@ -89,7 +91,7 @@ void EditTextDialog::on_buttonBox_accepted() const Sketcher::Constraint* constraint = sketch->Constraints[constrIndex]; // Check if anything changed - if (newText == constraint->Text && newFontPath == constraint->Font) { + if (newText == constraint->getText() && newFontPath == constraint->getFont()) { return; // Nothing to do } diff --git a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc index 77415ab979..3739a0a744 100644 --- a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc +++ b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc @@ -93,6 +93,8 @@ icons/elements/Sketcher_Element_Line_Edge.svg icons/elements/Sketcher_Element_Line_EndPoint.svg icons/elements/Sketcher_Element_Line_StartingPoint.svg + icons/elements/Sketcher_Element_Text_EndPoint.svg + icons/elements/Sketcher_Element_Text_StartPoint.svg icons/elements/Sketcher_Element_Parabolic_Arc_Centre_Point.svg icons/elements/Sketcher_Element_Parabolic_Arc_Edge.svg icons/elements/Sketcher_Element_Parabolic_Arc_End_Point.svg diff --git a/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Group.svg b/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Group.svg index 1e64e5a1af..8eaea74fca 100644 --- a/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Group.svg +++ b/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Group.svg @@ -6,8 +6,8 @@ height="64px" id="svg2816" version="1.1" - sodipodi:docname="Sketcher_ToggleConstraint.svg" - inkscape:version="1.4 (86a8ad7, 2024-10-11)" + sodipodi:docname="Constraint_Group.svg" + inkscape:version="1.4.2 (f4327f4, 2025-05-13)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" @@ -25,9 +25,9 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:zoom="14.948679" - inkscape:cx="39.836295" - inkscape:cy="23.11241" + inkscape:zoom="2.6425781" + inkscape:cx="79.467851" + inkscape:cy="69.818183" inkscape:window-width="3840" inkscape:window-height="1571" inkscape:window-x="-9" @@ -419,25 +419,111 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Text.svg b/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Text.svg index 231658e932..e99d3566ee 100644 --- a/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Text.svg +++ b/src/Mod/Sketcher/Gui/Resources/icons/constraints/Constraint_Text.svg @@ -7,12 +7,34 @@ id="svg2869" version="1.1" viewBox="0 0 64 64" + sodipodi:docname="Constraint_Text.svg" + inkscape:version="1.4.2 (f4327f4, 2025-05-13)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + + + + + + @@ -218,76 +260,71 @@ + id="g2" + transform="translate(-2.1206965,1.6845105)"> + id="g6" + transform="translate(-2.0747215,12.048077)"> + style="baseline-shift:baseline;display:inline;overflow:visible;fill:#cc0000;fill-opacity:1;stroke:#280000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;enable-background:accumulate;stop-color:#000000" + d="M 10.691188,-10.079592 A 2.7002703,2.7002703 0 0 0 7.990017,-7.3784198 V 4.1625958 a 2.7000003,2.7000003 0 0 0 2.701171,2.6992188 2.7000003,2.7000003 0 0 0 2.699219,-2.6992188 v -8.8417969 h 8.179688 a 2.7000003,2.7000003 0 0 0 2.699218,-2.6992187 2.7000003,2.7000003 0 0 0 -2.699218,-2.7011722 z" + id="path5" /> - + style="fill:none;fill-opacity:1;stroke:#ef2929;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" + d="M 9.8336448,4.6826881 V -8.2281518 H 22.108977" + id="path6-23" /> + id="g6-8" + transform="rotate(90,29.356805,23.056368)"> + style="baseline-shift:baseline;display:inline;overflow:visible;fill:#cc0000;fill-opacity:1;stroke:#280000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;enable-background:accumulate;stop-color:#000000" + d="M 10.691188,-10.079592 A 2.7002703,2.7002703 0 0 0 7.990017,-7.3784198 V 4.1625958 a 2.7000003,2.7000003 0 0 0 2.701171,2.6992188 2.7000003,2.7000003 0 0 0 2.699219,-2.6992188 v -8.8417969 h 8.179688 a 2.7000003,2.7000003 0 0 0 2.699218,-2.6992187 2.7000003,2.7000003 0 0 0 -2.699218,-2.7011722 z" + id="path5-8" /> - + style="fill:none;fill-opacity:1;stroke:#ef2929;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" + d="M 9.8336448,4.6826881 V -8.2281518 H 22.108977" + id="path6-2" /> + id="g6-8-5" + transform="rotate(180,35.318587,24.377746)"> + style="baseline-shift:baseline;display:inline;overflow:visible;fill:#cc0000;fill-opacity:1;stroke:#280000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;enable-background:accumulate;stop-color:#000000" + d="M 10.691188,-10.079592 A 2.7002703,2.7002703 0 0 0 7.990017,-7.3784198 V 4.1625958 a 2.7000003,2.7000003 0 0 0 2.701171,2.6992188 2.7000003,2.7000003 0 0 0 2.699219,-2.6992188 v -8.8417969 h 8.179688 a 2.7000003,2.7000003 0 0 0 2.699218,-2.6992187 2.7000003,2.7000003 0 0 0 -2.699218,-2.7011722 z" + id="path5-8-1" /> + style="fill:none;fill-opacity:1;stroke:#ef2929;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" + d="M 9.8336448,4.6826881 V -8.2281518 H 22.108977" + id="path6-2-7" /> + + + + - - - - - - - + id="g3" + transform="matrix(0.67985712,0,0,0.67985712,3.8942753,5.1844051)"> + + + diff --git a/src/Mod/Sketcher/Gui/Resources/icons/elements/Sketcher_Element_Text_EndPoint.svg b/src/Mod/Sketcher/Gui/Resources/icons/elements/Sketcher_Element_Text_EndPoint.svg new file mode 100644 index 0000000000..0e20763e4e --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/elements/Sketcher_Element_Text_EndPoint.svg @@ -0,0 +1,587 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_CreateArc.svg + + + FreeCAD LGPL2+ + + + 2023-12-19 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/Resources/icons/elements/Sketcher_Element_Text_StartPoint.svg b/src/Mod/Sketcher/Gui/Resources/icons/elements/Sketcher_Element_Text_StartPoint.svg new file mode 100644 index 0000000000..4a98670110 --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/elements/Sketcher_Element_Text_StartPoint.svg @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [maxwxyz] + + + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_CreateArc.svg + + + FreeCAD LGPL2+ + + + 2023-12-19 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.cpp b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.cpp index 145fb5925f..ac62edc1e6 100644 --- a/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.cpp +++ b/src/Mod/Sketcher/Gui/SketcherToolDefaultWidget.cpp @@ -56,6 +56,10 @@ SketcherToolDefaultWidget::SketcherToolDefaultWidget(QWidget* parent) { ui->setupUi(this); + ui->comboBox1->setMaxVisibleItems(25); + ui->comboBox2->setMaxVisibleItems(25); + ui->comboBox3->setMaxVisibleItems(25); + // connecting the needed signals setupConnections(); diff --git a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp index 4b47bb9515..f12f4135c0 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp @@ -352,7 +352,7 @@ public: auto selicon = [this](const Sketcher::Constraint* constr, const QIcon& normal, const QIcon& driven) -> QIcon { - if (!constr->isActive) { + if (!sketch->isConstraintActiveInSketch(constr)) { QIcon darkIcon; int w = listWidget()->style()->pixelMetric(QStyle::PM_ListViewIconSize); darkIcon.addPixmap(normal.pixmap(w, w, QIcon::Disabled, QIcon::Off), @@ -1708,6 +1708,10 @@ bool TaskSketcherConstraints::isConstraintFiltered(QListWidgetItem* item) ConstraintItem* it = static_cast(item); const Sketcher::Constraint* constraint = vals[it->ConstraintNbr]; + // Text constraint is hidden from the list widget. + if (constraint->Type == Sketcher::Text) { + return true; + } ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Sketcher"); diff --git a/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp b/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp index 0dbeb45751..7e5a358213 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp @@ -204,7 +204,8 @@ public: Base::Type geometryType, GeometryState state, const QString& lab, - ViewProviderSketch* sketchView + ViewProviderSketch* sketchView, + bool isTextHandle = false ) : ElementNbr(elementnr) , StartingVertex(startingVertex) @@ -219,6 +220,7 @@ public: , clickedOn(SubElementType::none) , hovered(SubElementType::none) , rightClicked(false) + , isTextHandle(isTextHandle) , label(lab) , sketchView(sketchView) { @@ -319,6 +321,7 @@ public: SubElementType clickedOn; SubElementType hovered; bool rightClicked; + bool isTextHandle; QString label; @@ -377,11 +380,12 @@ public: static const QIcon& getIcon(Base::Type type, Sketcher::PointPos pos, - ElementItem::GeometryState icontype = ElementItem::GeometryState::Normal) + ElementItem::GeometryState icontype = ElementItem::GeometryState::Normal, + bool isTextHandle = false) { static ElementWidgetIcons elementicons; - return elementicons.getIconImpl(type, pos, icontype); + return elementicons.getIconImpl(type, pos, icontype, isTextHandle); } private: @@ -505,11 +509,34 @@ private: {Sketcher::PointPos::none, getMultIcon("Sketcher_Element_SelectionTypeInvalid")}, })); + + // Text Handle Icons + textIcons[Sketcher::PointPos::none] = Gui::BitmapFactory().iconFromTheme("Sketcher_CreateText"); + textIcons[Sketcher::PointPos::start] = Gui::BitmapFactory().iconFromTheme("Sketcher_Element_Text_StartPoint"); + textIcons[Sketcher::PointPos::end] = Gui::BitmapFactory().iconFromTheme("Sketcher_Element_Text_EndPoint"); } const QIcon& getIconImpl(Base::Type type, Sketcher::PointPos pos, - ElementItem::GeometryState icontype) + ElementItem::GeometryState icontype, + bool isTextHandle) { + if (isTextHandle) { + auto it = textIcons.find(pos); + if (it != textIcons.end()) { + return it->second; + } + // Fallback for Midpoint or other positions: use normal line icon without blue filter + if (type == Part::GeomLineSegment::getClassTypeId()) { + auto typekey = icons.find(type); + if (typekey != icons.end()) { + auto poskey = typekey->second.find(pos); + if (poskey != typekey->second.end()) { + // Return the 'Normal' variant (index 0) to avoid blue construction color + return std::get<0>(poskey->second); + } + } + } + } auto typekey = icons.find(type); @@ -585,6 +612,7 @@ private: private: std::map>> icons; + std::map textIcons; }; ElementView::ElementView(QWidget* parent) @@ -720,6 +748,14 @@ void ElementView::contextMenuEvent(QContextMenuEvent* event) QMenu menu; QList items = selectedItems(); + if (items.size() == 1) { + ElementItem* item = static_cast(items.first()); + if (item->isTextHandle) { + menu.addSeparator(); + menu.addAction(tr("Convert to geometries"), this, SLOT(doConvertToGeometries())); + } + } + // NOTE: If extending this context menu, be sure to add the items to the translation block at // the top of this file @@ -944,6 +980,25 @@ ElementItem* ElementView::itemFromIndex(const QModelIndex& index) return static_cast(QListWidget::itemFromIndex(index)); } +void ElementView::doConvertToGeometries() +{ + QList items = selectedItems(); + if (items.isEmpty()) + return; + + ElementItem* item = static_cast(items.first()); + if (!item->isTextHandle) + return; + + int geoId = item->ElementNbr; + Sketcher::SketchObject* sketch = item->getSketchObject(); + + // Deleting the handle geometry will automatically remove the Text constraint + // (turning it to None), leaving the generated text geometries intact. + Gui::Selection().clearSelection(); + sketch->delGeometry(geoId); +} + // clang-format on /* ElementItem delegate ---------------------------------------------------- */ ElementItemDelegate::ElementItemDelegate(ElementView* parent) @@ -1038,7 +1093,8 @@ void ElementItemDelegate::drawSubControl( auto isHovered = rect.contains(mousePos); auto drawSelectIcon = [&](Sketcher::PointPos pos) { - auto icon = ElementWidgetIcons::getIcon(item->GeometryType, pos, item->State); + auto icon + = ElementWidgetIcons::getIcon(item->GeometryType, pos, item->State, item->isTextHandle); auto isOptionSelected = option.state & QStyle::State_Selected; auto isOptionHovered = option.state & QStyle::State_MouseOver; @@ -1977,6 +2033,7 @@ void TaskSketcherElements::slotElementsChanged() QString label; // This is a regular geometry. Get its type name. QString baseName; + bool isTextHandle = false; if (type == Part::GeomPoint::getClassTypeId()) { baseName = tr("Point"); } @@ -1990,6 +2047,7 @@ void TaskSketcherElements::slotElementsChanged() } else { baseName = tr("Text"); + isTextHandle = true; } } else { @@ -2043,7 +2101,8 @@ void TaskSketcherElements::slotElementsChanged() type, state, label, - sketchView); + sketchView, + isTextHandle); ui->listWidgetElements->addItem(itemN); diff --git a/src/Mod/Sketcher/Gui/TaskSketcherElements.h b/src/Mod/Sketcher/Gui/TaskSketcherElements.h index 365c5caf36..974cc93b72 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherElements.h +++ b/src/Mod/Sketcher/Gui/TaskSketcherElements.h @@ -85,6 +85,7 @@ protected Q_SLOTS: void doSymmetricConstraint(); void doBlockConstraint(); void doGroupConstraint(); + void doConvertToGeometries(); void doLockConstraint(); void doHorizontalConstraint(); diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp index f9e289f903..bfcdde5601 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp @@ -4464,6 +4464,11 @@ Sketcher::Constraint* ViewProviderSketch::getConstraint(int constid) const return nullptr; } +bool ViewProviderSketch::isConstraintActiveInSketch(const Sketcher::Constraint* cstr) const +{ + return getSketchObject()->isConstraintActiveInSketch(cstr); +} + const GeoList ViewProviderSketch::getGeoList() const { const std::vector tempGeo = diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.h b/src/Mod/Sketcher/Gui/ViewProviderSketch.h index 33d87b0f9f..d8484e667d 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.h +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.h @@ -888,6 +888,9 @@ private: /// or null if it doesn't exist. Sketcher::Constraint* getConstraint(int constid) const; + // Return true if the constraint is active, includes checking if it's not in a group + bool isConstraintActiveInSketch(const Sketcher::Constraint* cstr) const; + // gets the list of geometry of the sketchobject or of the solver instance const GeoList getGeoList() const; diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketchCoinAttorney.h b/src/Mod/Sketcher/Gui/ViewProviderSketchCoinAttorney.h index 4025f7199b..8e183185bc 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketchCoinAttorney.h +++ b/src/Mod/Sketcher/Gui/ViewProviderSketchCoinAttorney.h @@ -85,6 +85,10 @@ class ViewProviderSketchCoinAttorney private: static inline bool constraintHasExpression(const ViewProviderSketch& vp, int constrid); static inline const std::vector getConstraints(const ViewProviderSketch& vp); + static inline bool isConstraintActiveInSketch( + const ViewProviderSketch& vp, + const Sketcher::Constraint* cstr + ); static inline const GeoList getGeoList(const ViewProviderSketch& vp); static inline const GeoListFacade getGeoListFacade(const ViewProviderSketch& vp); static inline Base::Placement getEditingPlacement(const ViewProviderSketch& vp); @@ -144,6 +148,14 @@ inline const std::vector ViewProviderSketchCoinAttorney:: return vp.getConstraints(); } +inline bool ViewProviderSketchCoinAttorney::isConstraintActiveInSketch( + const ViewProviderSketch& vp, + const Sketcher::Constraint* cstr +) +{ + return vp.isConstraintActiveInSketch(cstr); +} + inline const GeoList ViewProviderSketchCoinAttorney::getGeoList(const ViewProviderSketch& vp) { return vp.getGeoList(); From d842e4151c0be708f8d440781c541ab00ee8592b Mon Sep 17 00:00:00 2001 From: paddle Date: Fri, 13 Feb 2026 12:09:13 +0100 Subject: [PATCH 4/8] Sketcher: Text: Fixes --- src/Mod/Part/App/Geometry.cpp | 4 +- src/Mod/Sketcher/App/Sketch.cpp | 2 +- src/Mod/Sketcher/App/SketchObject.cpp | 5 ++- src/Mod/Sketcher/App/SketchObject.h | 1 + src/Mod/Sketcher/App/SketchObject.pyi | 8 +++- src/Mod/Sketcher/App/SketchObjectPyImp.cpp | 25 +++++++++--- src/Mod/Sketcher/Gui/ConstraintFilters.h | 28 ++++++------- src/Mod/Sketcher/Gui/DrawSketchHandlerText.h | 11 ++++-- src/Mod/Sketcher/Gui/EditTextDialog.cpp | 27 ++++++++++++- src/Mod/Sketcher/Gui/EditTextDialog.ui | 18 +++++++++ .../Sketcher/Gui/TaskSketcherConstraints.cpp | 4 ++ .../Sketcher/Gui/TaskSketcherConstraints.h | 2 + src/Mod/Sketcher/Gui/Utils.cpp | 5 +++ src/Mod/Sketcher/Gui/ViewProviderSketch.cpp | 39 +++++++++++++++++-- 14 files changed, 145 insertions(+), 34 deletions(-) diff --git a/src/Mod/Part/App/Geometry.cpp b/src/Mod/Part/App/Geometry.cpp index 6d465f5551..b716190a0d 100644 --- a/src/Mod/Part/App/Geometry.cpp +++ b/src/Mod/Part/App/Geometry.cpp @@ -7507,7 +7507,7 @@ void transformAndConvertToGeometry( double baseHeight = ymax - ymin; // This transform will move the geometry's bottom-left corner to the origin (0,0,0) - gp_Vec initialTranslationVec(-xmin, -ymin, -zmin); + gp_Vec initialTranslationVec(-xmin, -ymin, 0.0); // 2. Determine scale and rotation double angle; @@ -7536,7 +7536,7 @@ void transformAndConvertToGeometry( gp_Trsf rotateTrsf; rotateTrsf.SetRotation(gp::XOY().Axis(), angle); gp_Trsf finalTranslate; - finalTranslate.SetTranslation(gp_Vec(p1.x, p1.y, p1.z)); + finalTranslate.SetTranslation(gp_Vec(p1.x, p1.y, 0.0)); gp_Trsf finalTrsf = finalTranslate * rotateTrsf * scaleTrsf * initialTranslate; // 4. Apply transformation and convert to Sketcher geometry diff --git a/src/Mod/Sketcher/App/Sketch.cpp b/src/Mod/Sketcher/App/Sketch.cpp index e3839b3b3f..72811c6e98 100644 --- a/src/Mod/Sketcher/App/Sketch.cpp +++ b/src/Mod/Sketcher/App/Sketch.cpp @@ -5662,7 +5662,7 @@ void Sketch::applyGroupTransformations() Base::Matrix4D T2; // Identity T2[0][3] = postSolveFrame.startPoint.x; T2[1][3] = postSolveFrame.startPoint.y; - T2[2][3] = postSolveFrame.startPoint.z; + T2[2][3] = 0; // 5. Combine the matrices in the correct order: T_final = T2 * R * S * T1 Base::Matrix4D transform = T2 * R * S * T1; diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 46603eabb8..afc31b5c3f 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -906,7 +906,7 @@ double SketchObject::getDatum(int ConstrId) const return this->Constraints[ConstrId]->getValue(); } -int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string& newFont, bool isConstruction) +int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string& newFont, bool isHeight, bool isConstruction) { ; // no need to check input data validity as this is an sketchobject managed operation. Base::StateLocker lock(managedoperation, true); @@ -926,9 +926,9 @@ int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string } // First we replace the old geometries by the new text. - const bool isHeight = constr->getIsTextHeight(); const std::string oldText = constr->getText(); const std::string oldFont = constr->getFont(); + const bool oldIsHeight = constr->getIsTextHeight(); int handleGeoId = constr->getGeoId(0); int firstTextGeoId = constr->getGeoId(1); bool hasExistingText = firstTextGeoId != GeoEnum::GeoUndef; @@ -1012,6 +1012,7 @@ int SketchObject::setTextAndFont(int ConstrId, std::string& newText, std::string if (err) { constr->setText(oldText); constr->setFont(oldFont); + constr->setIsTextHeight(oldIsHeight); } return err; diff --git a/src/Mod/Sketcher/App/SketchObject.h b/src/Mod/Sketcher/App/SketchObject.h index ee87d55ca5..87f8f2277e 100644 --- a/src/Mod/Sketcher/App/SketchObject.h +++ b/src/Mod/Sketcher/App/SketchObject.h @@ -358,6 +358,7 @@ public: int ConstrId, std::string& newText, std::string& newFont, + bool isHeight, bool isConstruction = false ); /// set the driving status of this constraint and solve diff --git a/src/Mod/Sketcher/App/SketchObject.pyi b/src/Mod/Sketcher/App/SketchObject.pyi index 7d41ac7d84..f6c0eaafa8 100644 --- a/src/Mod/Sketcher/App/SketchObject.pyi +++ b/src/Mod/Sketcher/App/SketchObject.pyi @@ -409,16 +409,20 @@ class SketchObject(Part2DObject): """ ... - def setTextAndFont(self, constraint: int, text: str, font: str) -> None: + def setTextAndFont( + self, constraint: int, text: str, font: str, isheight: bool, isConstruction: bool + ) -> None: """ Set the text and font of a Text constraint. - setTextAndFont(constraint: int, text: str, font: str) + setTextAndFont(constraint: int, text: str, font: str, isHeight: bool, isConstruction: bool) Args: constraint: The index of the Text constraint. text: The text string to display. font: The full path to the font file (.ttf, .otf, etc.). + isHeight: Is the line handle of the group the height of the text. + isConstruction: Are text geometry construction of not. """ ... diff --git a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp index 9d68597d7b..119fc787e1 100644 --- a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp +++ b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp @@ -761,11 +761,21 @@ PyObject* SketchObjectPy::setTextAndFont(PyObject* args, PyObject* kwd) int constrIndex = -1; char* textStr; char* fontStr; - char* constrName = nullptr; + PyObject* isHeightObj = Py_True; PyObject* isConstrObj = Py_False; // Default to null (parameter not provided) - // "iss|O" means: int, string, string, | optional Object - if (!PyArg_ParseTuple(args, "iss|O!", &constrIndex, &textStr, &fontStr, &PyBool_Type, &isConstrObj)) { + // "iss|O!O!" (int, str, str, | bool, bool) + if (!PyArg_ParseTuple( + args, + "iss|O!O!", + &constrIndex, + &textStr, + &fontStr, + &PyBool_Type, + &isHeightObj, + &PyBool_Type, + &isConstrObj + )) { return nullptr; } @@ -773,8 +783,13 @@ PyObject* SketchObjectPy::setTextAndFont(PyObject* args, PyObject* kwd) std::string font(fontStr); // Call the C++ implementation - int err = this->getSketchObjectPtr() - ->setTextAndFont(constrIndex, text, font, Base::asBoolean(isConstrObj)); + int err = this->getSketchObjectPtr()->setTextAndFont( + constrIndex, + text, + font, + Base::asBoolean(isHeightObj), + Base::asBoolean(isConstrObj) + ); // Handle errors returned from the C++ function if (err) { diff --git a/src/Mod/Sketcher/Gui/ConstraintFilters.h b/src/Mod/Sketcher/Gui/ConstraintFilters.h index 3a18605ebc..d83c2a73be 100644 --- a/src/Mod/Sketcher/Gui/ConstraintFilters.h +++ b/src/Mod/Sketcher/Gui/ConstraintFilters.h @@ -54,20 +54,20 @@ enum class FilterValue Equality = 9, Symmetric = 10, Block = 11, - InternalAlignment = 12, - Datums = 13, - HorizontalDistance = 14, - VerticalDistance = 15, - Distance = 16, - Radius = 17, - Weight = 18, - Diameter = 19, - Angle = 20, - SnellsLaw = 21, - Named = 22, - NonDriving = 23, - Group = 24, - Text = 25, + Group = 12, + Text = 13, + InternalAlignment = 14, + Datums = 15, + HorizontalDistance = 16, + VerticalDistance = 17, + Distance = 18, + Radius = 19, + Weight = 20, + Diameter = 21, + Angle = 22, + SnellsLaw = 23, + Named = 24, + NonDriving = 25, NumFilterValue // SpecialFilterValue shall start at the same index as this }; diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h index b078962fc4..4f6a9f3e9a 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h @@ -145,6 +145,7 @@ private: std::string escFont = escapeForPython(font); bool isHeight = constructionMethod() == ConstructionMethod::Height; const char* constrBoolStr = isConstructionMode() ? "True" : "False"; + const char* heightBoolStr = isHeight ? "True" : "False"; // Add the 'Text' Constraint (Empty) // We initialize the constraint containing ONLY the handle (element 0). @@ -156,7 +157,7 @@ private: handleId, escText.c_str(), escFont.c_str(), - isHeight ? "True" : "False" + heightBoolStr ); // Generate Text Geometry by calling setTextAndFont on the new constraint. @@ -165,10 +166,11 @@ private: Gui::cmdAppObjectArgs( getSketchObject(), "setTextAndFont(len(App.ActiveDocument.getObject('%s').Constraints)-1, '%s', '%s', " - "%s)", + "%s, %s)", getSketchObject()->getNameInDocument(), escText.c_str(), escFont.c_str(), + heightBoolStr, constrBoolStr ); @@ -383,7 +385,10 @@ void DSHTextController::configureToolWidget() // 3. Set a sensible default font QString defaultFontName; - if (fontNames.contains(QString::fromUtf8("DejaVu Sans"), Qt::CaseInsensitive)) { + if (fontNames.contains(QString::fromUtf8("osifont-lgpl3fe"), Qt::CaseInsensitive)) { + defaultFontName = QString::fromUtf8("osifont-lgpl3fe"); + } + else if (fontNames.contains(QString::fromUtf8("DejaVu Sans"), Qt::CaseInsensitive)) { defaultFontName = QString::fromUtf8("DejaVu Sans"); } else if (fontNames.contains(QString::fromUtf8("Arial"), Qt::CaseInsensitive)) { diff --git a/src/Mod/Sketcher/Gui/EditTextDialog.cpp b/src/Mod/Sketcher/Gui/EditTextDialog.cpp index 2b968239bb..d17442fe8a 100644 --- a/src/Mod/Sketcher/Gui/EditTextDialog.cpp +++ b/src/Mod/Sketcher/Gui/EditTextDialog.cpp @@ -53,6 +53,9 @@ EditTextDialog::EditTextDialog(ViewProviderSketch* viewProvider, int constraintI // Initialize Text ui->lineEdit_text->setText(QString::fromStdString(constraint->getText())); + ui->radioButton_height->setChecked(constraint->getIsTextHeight()); + ui->radioButton_width->setChecked(!constraint->getIsTextHeight()); + // Initialize Font populateFontList(); QString currentFontName = findFontNameFromPath(QString::fromStdString(constraint->getFont())); @@ -87,11 +90,13 @@ void EditTextDialog::on_buttonBox_accepted() std::string newText = ui->lineEdit_text->text().toStdString(); QString selectedFontName = ui->comboBox_font->currentText(); std::string newFontPath = fontPathMap.value(selectedFontName).toStdString(); + bool newIsHeight = ui->radioButton_height->isChecked(); const Sketcher::Constraint* constraint = sketch->Constraints[constrIndex]; // Check if anything changed - if (newText == constraint->getText() && newFontPath == constraint->getFont()) { + if (newText == constraint->getText() && newFontPath == constraint->getFont() + && newIsHeight == constraint->getIsTextHeight()) { return; // Nothing to do } @@ -99,7 +104,25 @@ void EditTextDialog::on_buttonBox_accepted() Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Modify sketch text constraint")); try { - Gui::cmdAppObjectArgs(sketch, "setTextAndFont(%i, '%s', '%s')", constrIndex, newText, newFontPath); + // Find if it was construction geometry to preserve that state + int firstTextGeoId = constraint->getGeoId(1); + bool isConstruction = false; + if (firstTextGeoId != Sketcher::GeoEnum::GeoUndef) { + isConstruction = Sketcher::GeometryFacade::getConstruction( + sketch->getGeometry(firstTextGeoId) + ); + } + + // Send the updated 5-parameter call to Python + Gui::cmdAppObjectArgs( + sketch, + "setTextAndFont(%i, '%s', '%s', %s, %s)", + constrIndex, + newText.c_str(), + newFontPath.c_str(), + newIsHeight ? "True" : "False", + isConstruction ? "True" : "False" + ); Gui::Command::commitCommand(); } diff --git a/src/Mod/Sketcher/Gui/EditTextDialog.ui b/src/Mod/Sketcher/Gui/EditTextDialog.ui index 1b29a238f8..87938d664f 100644 --- a/src/Mod/Sketcher/Gui/EditTextDialog.ui +++ b/src/Mod/Sketcher/Gui/EditTextDialog.ui @@ -36,6 +36,24 @@ + + + + + + Height + + + + + + + Width + + + + + diff --git a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp index f12f4135c0..ccf48a033d 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp @@ -834,6 +834,10 @@ ConstraintFilterList::ConstraintFilterList(QWidget* parent) it->setCheckState(isChecked ? Qt::Checked : Qt::Unchecked); filterState = filterState >> 1;// shift right to get rid of the used bit. } + + // Text constraint filter is hidden from the user + item(static_cast(ConstraintFilter::FilterValue::Text))->setHidden(true); + languageChange(); setPartiallyChecked(); diff --git a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.h b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.h index 55d14ba827..2c362f6335 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.h +++ b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.h @@ -119,6 +119,8 @@ private: {QT_TR_NOOP("Equality"), 1}, {QT_TR_NOOP("Symmetric"), 1}, {QT_TR_NOOP("Block"), 1}, + {QT_TR_NOOP("Group"), 1}, + {QT_TR_NOOP("Text"), 1}, {QT_TR_NOOP("Internal Alignment"), 1}, {QT_TR_NOOP("Datums"), 0}, {QT_TR_NOOP("Horizontal Distance"), 1}, diff --git a/src/Mod/Sketcher/Gui/Utils.cpp b/src/Mod/Sketcher/Gui/Utils.cpp index 7f6e4cfbfb..52fdb7c4c0 100644 --- a/src/Mod/Sketcher/Gui/Utils.cpp +++ b/src/Mod/Sketcher/Gui/Utils.cpp @@ -1003,6 +1003,11 @@ QMap SketcherGui::findAvailableFontFiles() QMap fontMap; QStringList fontPaths; + // 0. Include FreeCAD bundled fonts + fontPaths << QString::fromStdString( + App::Application::getResourceDir() + "Mod/TechDraw/Resources/fonts/" + ); + #if defined(Q_OS_WIN) fontPaths << QString::fromUtf8("C:/Windows/Fonts"); #elif defined(Q_OS_MACOS) diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp index bfcdde5601..4978ffc988 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp @@ -1336,9 +1336,32 @@ void ViewProviderSketch::editDoubleClicked() Base::Console().log("double click point:%d\n", preselection.PreselectPoint); } else if (preselection.isPreselectCurveValid()) { - // We cannot do toggleWireSelelection directly here because the released event with - //STATUS_NONE return false which clears the selection. - setSketchMode(STATUS_SELECT_Wire); + int geoId = preselection.PreselectCurve; + Sketcher::SketchObject* sketch = getSketchObject(); + + // Check if the preselected edge is the handle of a Text constraint + int textConstrId = -1; + const auto& constraints = sketch->Constraints.getValues(); + for (int i = 0; i < static_cast(constraints.size()); ++i) { + if (constraints[i]->Type == Sketcher::Text && constraints[i]->hasElement(0)) { + if (constraints[i]->getGeoId(0) == geoId) { + textConstrId = i; + break; + } + } + } + + if (textConstrId != -1) { + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Modify Text constraint")); + EditTextDialog editTextDialog(this, textConstrId); + editTextDialog.exec(); + setSketchMode(STATUS_NONE); + } + else { + // We cannot do toggleWireSelelection directly here because the released event with + //STATUS_NONE return false which clears the selection. + setSketchMode(STATUS_SELECT_Wire); + } } else if (preselection.isCrossPreselected()) { Base::Console().log("double click cross:%d\n", @@ -2499,6 +2522,16 @@ bool ViewProviderSketch::detectAndShowPreselection(SoPickedPoint* Point) } else if (result.GeoIndex != -1 && result.GeoIndex != preselection.PreselectCurve) {// if a new curve is hit + + // If the picked edge is part of a text/group, treat the handle as the preselected item + int handleId = getSketchObject()->getGroupHandleIfInGroup(result.GeoIndex); + if (handleId != result.GeoIndex) { + if (handleId == preselection.PreselectCurve) { + return false; + } + result.GeoIndex = handleId; + } + std::stringstream ss; if (result.GeoIndex >= 0) ss << "Edge" << result.GeoIndex + 1; From e7e5e39e7bcb6ec3ee28a0116bf99415c577bf6d Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Mon, 2 Mar 2026 13:02:27 +0100 Subject: [PATCH 5/8] Sketcher: Change default text to Text --- src/Mod/Sketcher/Gui/DrawSketchHandlerText.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h index 4f6a9f3e9a..1ca1ab89d9 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerText.h @@ -89,7 +89,7 @@ public: : DrawSketchHandlerTextBase(constrMethod) , length(0.0) , handleId(0) - , text("AstoCAD") + , text(QObject::tr("Text").toStdString()) , font("") , cachedTextName("") , cachedFontName("") From a0c97a102e5f926323ab144dd532c4e6b8a7811f Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Sun, 8 Mar 2026 20:56:23 +0100 Subject: [PATCH 6/8] Part: Add HarfBuzz text shaping for proper kerning in makeTextWires Replace the character-by-character positioning loop with HarfBuzz text shaping. The old code used FT_Get_Kerning which only reads legacy kern tables. HarfBuzz handles both GPOS kerning and legacy kern tables, as well as ligatures and complex script shaping. --- cMake/FreeCAD_Helpers/SetupFreeType.cmake | 16 ++++++ src/Mod/Part/App/CMakeLists.txt | 2 + src/Mod/Part/App/Geometry.cpp | 62 ++++++++++++++--------- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/cMake/FreeCAD_Helpers/SetupFreeType.cmake b/cMake/FreeCAD_Helpers/SetupFreeType.cmake index a4b669628d..e4440f21cd 100644 --- a/cMake/FreeCAD_Helpers/SetupFreeType.cmake +++ b/cMake/FreeCAD_Helpers/SetupFreeType.cmake @@ -7,6 +7,22 @@ macro(SetupFreetype) message("===============================================================\n" "FreeType2 not found. Part module will lack of makeWireString().\n" "===============================================================\n") + else() + # find_package(harfbuzz CONFIG) fails on windows + # lets do it the complicated way instead. + find_path(HARFBUZZ_INCLUDE_DIR hb.h PATH_SUFFIXES harfbuzz) + find_library(HARFBUZZ_LIBRARY NAMES harfbuzz) + if(HARFBUZZ_INCLUDE_DIR AND HARFBUZZ_LIBRARY) + if(NOT TARGET harfbuzz::harfbuzz) + add_library(harfbuzz::harfbuzz UNKNOWN IMPORTED) + set_target_properties(harfbuzz::harfbuzz PROPERTIES + IMPORTED_LOCATION "${HARFBUZZ_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${HARFBUZZ_INCLUDE_DIR}" + ) + endif() + else() + message(FATAL_ERROR "HarfBuzz not found") + endif() endif(NOT FREETYPE_FOUND) endif(FREECAD_USE_FREETYPE) diff --git a/src/Mod/Part/App/CMakeLists.txt b/src/Mod/Part/App/CMakeLists.txt index 23b487ff07..5b4aa24989 100644 --- a/src/Mod/Part/App/CMakeLists.txt +++ b/src/Mod/Part/App/CMakeLists.txt @@ -41,6 +41,8 @@ if(FREETYPE_FOUND) ${Part_LIBS} ${FREETYPE_LIBRARIES} ) + + set(Part_LIBS ${Part_LIBS} harfbuzz::harfbuzz) endif(FREETYPE_FOUND) generate_from_py(Arc) diff --git a/src/Mod/Part/App/Geometry.cpp b/src/Mod/Part/App/Geometry.cpp index b716190a0d..22f96b4eaf 100644 --- a/src/Mod/Part/App/Geometry.cpp +++ b/src/Mod/Part/App/Geometry.cpp @@ -126,8 +126,8 @@ #include FT_FREETYPE_H #include FT_GLYPH_H #include FT_OUTLINE_H -#include -#include + +#include // headers to scale text correctly #include @@ -7659,32 +7659,42 @@ std::vector makeTextWires( FT_Outline_Funcs ftCallbacks = {move_cb, line_cb, quad_cb, cubic_cb, 0, 0}; FT_UInt ftLoadFlags = FT_LOAD_NO_SCALE | FT_LOAD_NO_BITMAP; + // Use HarfBuzz for text shaping. Positions are in font design units (upem), + // matching our FT_LOAD_NO_SCALE outline decomposition. + auto* hbBlob = hb_blob_create( + fontBuffer.data(), + static_cast(fontBuffer.size()), + HB_MEMORY_MODE_READONLY, + nullptr, + nullptr + ); + auto* hbFace = hb_face_create(hbBlob, 0); + auto* hbFont = hb_font_create(hbFace); + + auto* hbBuf = hb_buffer_create(); + hb_buffer_add_utf8(hbBuf, text.c_str(), -1, 0, -1); + hb_buffer_guess_segment_properties(hbBuf); + + hb_shape(hbFont, hbBuf, nullptr, 0); + + unsigned int glyphCount = 0; + auto* glyphInfos = hb_buffer_get_glyph_infos(hbBuf, &glyphCount); + auto* glyphPositions = hb_buffer_get_glyph_positions(hbBuf, &glyphCount); + double penPos = 0.0; double currentTracking = 0.0; - FT_ULong prevCharcode = 0; - std::wstring_convert, char32_t> converter; - std::u32string wide_text = converter.from_bytes(text); + for (unsigned int i = 0; i < glyphCount; ++i) { + FT_UInt glyphIndex = glyphInfos[i].codepoint; + double xOffset = glyphPositions[i].x_offset; + double xAdvance = glyphPositions[i].x_advance; - for (size_t i = 0; i < wide_text.length(); ++i) { - FT_ULong charcode = wide_text[i]; - - if (FT_Load_Char(ftFace, charcode, ftLoadFlags) != 0) { + if (FT_Load_Glyph(ftFace, glyphIndex, ftLoadFlags) != 0) { + penPos += xAdvance; + currentTracking += tracking; continue; } - if (prevCharcode != 0 && FT_HAS_KERNING(ftFace)) { - FT_Vector kern; - FT_Get_Kerning( - ftFace, - FT_Get_Char_Index(ftFace, prevCharcode), - FT_Get_Char_Index(ftFace, charcode), - FT_KERNING_UNSCALED, - &kern - ); - penPos += kern.x; - } - if (ftFace->glyph->format == FT_GLYPH_FORMAT_OUTLINE && ftFace->glyph->outline.n_contours > 0) { FTDC_Ctx ctx; @@ -7696,7 +7706,7 @@ std::vector makeTextWires( if (!ctx.Wires.empty()) { gp_Trsf charTransform; charTransform.SetScale(gp::Origin(), scaleFactor); - gp_Vec translation(penPos * scaleFactor + currentTracking, 0.0, 0.0); + gp_Vec translation((penPos + xOffset) * scaleFactor + currentTracking, 0.0, 0.0); charTransform.SetTranslationPart(translation); for (const auto& wire : ctx.Wires) { @@ -7709,11 +7719,15 @@ std::vector makeTextWires( } } - penPos += ftFace->glyph->advance.x; + penPos += xAdvance; currentTracking += tracking; - prevCharcode = charcode; } + hb_buffer_destroy(hbBuf); + hb_font_destroy(hbFont); + hb_face_destroy(hbFace); + hb_blob_destroy(hbBlob); + FT_Done_Face(ftFace); FT_Done_FreeType(ftLib); return allWires; From ca4660167e0a9d3187d24e15b3f9c9b68c0dfec7 Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Sun, 8 Mar 2026 21:02:07 +0100 Subject: [PATCH 7/8] Sketcher: Add .ttc (TrueType Collection) to font file discovery --- src/Mod/Sketcher/Gui/Utils.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mod/Sketcher/Gui/Utils.cpp b/src/Mod/Sketcher/Gui/Utils.cpp index 52fdb7c4c0..66f852e1e7 100644 --- a/src/Mod/Sketcher/Gui/Utils.cpp +++ b/src/Mod/Sketcher/Gui/Utils.cpp @@ -1025,7 +1025,8 @@ QMap SketcherGui::findAvailableFontFiles() QDirIterator it( path, - QStringList() << QString::fromUtf8("*.ttf") << QString::fromUtf8("*.otf"), + QStringList() << QString::fromUtf8("*.ttf") << QString::fromUtf8("*.otf") + << QString::fromUtf8("*.ttc"), QDir::Files, QDirIterator::Subdirectories ); From cd5876dc547a882491be89e8b56a6695b4ffaaa5 Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Mon, 9 Mar 2026 21:37:41 +0100 Subject: [PATCH 8/8] Sketcher: Escape text and font path in EditTextDialog Escape user-provided text and font path strings before inserting them into the Python command string, preventing breakage or injection from strings containing quotes or backslashes. --- src/Mod/Sketcher/Gui/EditTextDialog.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Mod/Sketcher/Gui/EditTextDialog.cpp b/src/Mod/Sketcher/Gui/EditTextDialog.cpp index d17442fe8a..4d7c1d9901 100644 --- a/src/Mod/Sketcher/Gui/EditTextDialog.cpp +++ b/src/Mod/Sketcher/Gui/EditTextDialog.cpp @@ -30,6 +30,7 @@ #include #include +#include "CommandConstraints.h" #include "EditTextDialog.h" #include "ViewProviderSketch.h" #include "Utils.h" @@ -114,12 +115,14 @@ void EditTextDialog::on_buttonBox_accepted() } // Send the updated 5-parameter call to Python + std::string escText = escapeForPython(newText); + std::string escFont = escapeForPython(newFontPath); Gui::cmdAppObjectArgs( sketch, "setTextAndFont(%i, '%s', '%s', %s, %s)", constrIndex, - newText.c_str(), - newFontPath.c_str(), + escText.c_str(), + escFont.c_str(), newIsHeight ? "True" : "False", isConstruction ? "True" : "False" );