Bug Summary

File:librecad/src/lib/engine/document/entities/rs_hatch.cpp
Warning:line 899, column 5
Called C++ object pointer is null

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-pc-linux-gnu -O2 -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name rs_hatch.cpp -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=deadcode -analyzer-checker=cplusplus -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -mrelocation-model pic -pic-level 2 -fhalf-no-semantic-interposition -mframe-pointer=none -fmath-errno -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/home/runner/work/LibreCAD/LibreCAD/librecad/src -fcoverage-compilation-dir=/home/runner/work/LibreCAD/LibreCAD/librecad/src -resource-dir /usr/lib/llvm-21/lib/clang/21 -D _REENTRANT -D DWGSUPPORT -D MUPARSER_STATIC -D LC_WITH_OPENTYPE=0 -D QC_APPDIR=librecad -D LC_VERSION=2.2.2_alpha1-853-ge2a8c2931 -D LC_PRERELEASE=true; -D QT_NO_DEBUG -D QT_SVG_LIB -D QT_PRINTSUPPORT_LIB -D QT_WIDGETS_LIB -D QT_GUI_LIB -D QT_NETWORK_LIB -D QT_CORE_LIB -I . -I /usr/include -I ../../libraries/lciconengine -I ../../libraries/libdxfrw/src -I ../../libraries/libdxfrw/src/intern -I ../../libraries/jwwlib/src -I ../../libraries/shapelib/src -I ../../libraries/qtbidi -I cmd -I lib/actions -I lib/actions/visual_snap -I lib/actions/options -I lib/creation -I lib/debug -I lib/engine -I lib/engine/document -I lib/engine/document/blocks -I lib/engine/document/container -I lib/engine/document/dimstyles -I lib/engine/document/entities -I lib/engine/document/entities/support -I lib/engine/document/fonts -I lib/engine/document/io -I lib/engine/document/layers -I lib/engine/document/linetypes -I lib/engine/document/patterns -I lib/engine/document/selection -I lib/engine/document/textstyles -I lib/engine/document/ucs -I lib/engine/document/variables -I lib/engine/document/views -I lib/engine/clipboard -I lib/engine/overlays -I lib/engine/overlays/angles_base -I lib/engine/overlays/highlight -I lib/engine/overlays/preview -I lib/engine/overlays/references -I lib/engine/overlays/crosshair -I lib/engine/overlays/info_cursor -I lib/engine/overlays/overlay_box -I lib/engine/overlays/ucs_mark -I lib/engine/undo -I lib/engine/utils -I lib/engine/settings -I lib/fileio -I lib/filters -I lib/generators -I lib/generators/makercamsvg -I lib/generators/layers -I lib/generators/image -I lib/gui -I lib/gui/grid -I lib/gui/render -I lib/gui/render/headless -I lib/gui/render/widget -I lib/information -I lib/math -I lib/modification -I lib/selection -I lib/selection/metaentity -I lib/selection/metaentity/entities -I lib/printing -I lib/properties -I actions -I actions/dock_widgets -I actions/dock_widgets/block -I actions/dock_widgets/entity_info -I actions/dock_widgets/layer -I actions/dock_widgets/library -I actions/dock_widgets/ucs_list -I actions/drawing -I actions/drawing/draw -I actions/drawing/draw/arc -I actions/drawing/draw/circle -I actions/drawing/draw/curve -I actions/drawing/draw/spline -I actions/drawing/draw/dimensions -I actions/drawing/draw/ellipse -I actions/drawing/draw/hatch -I actions/drawing/draw/image -I actions/drawing/draw/line -I actions/drawing/draw/point -I actions/drawing/draw/rect -I actions/drawing/draw/polygon -I actions/drawing/draw/misc -I actions/drawing/draw/line/shapes -I actions/drawing/draw/line/misc -I actions/drawing/draw/line/shapes/rect -I actions/drawing/draw/line/shapes/polygon -I actions/drawing/draw/polyline -I actions/drawing/draw/text -I actions/drawing/edit -I actions/drawing/info -I actions/drawing/pick -I actions/drawing/modify -I actions/drawing/pen -I actions/drawing/rel_zero -I actions/drawing/selection -I actions/drawing/snap -I actions/drawing/zoom -I actions/file -I actions/options -I actions/print_preview -I ui -I ui/action_options -I ui/action_options/circle -I ui/action_options/curve -I ui/action_options/spline -I ui/action_options/dimensions -I ui/action_options/edit -I ui/action_options/image -I ui/action_options/info -I ui/action_options/insert -I ui/action_options/line -I ui/action_options/rect -I ui/action_options/polygon -I ui/action_options/misc -I ui/action_options/modify -I ui/action_options/ellipse -I ui/action_options/other -I ui/action_options/polyline -I ui/action_options/point -I ui/action_options/print_preview -I ui/action_options/selection -I ui/action_options/snap -I ui/action_options/text -I ui/actions -I ui/components -I ui/components/relative_position_assistant -I ui/components/comboboxes -I ui/components/containers -I ui/components/creators -I ui/components/layouts -I ui/components/pen -I ui/components/status_bar -I ui/components/toolbars -I ui/components/utils -I ui/dialogs -I ui/dialogs/actions -I ui/dialogs/actions/modify -I ui/dialogs/actions/quick_selection -I ui/dialogs/modify -I ui/dialogs/entity -I ui/dialogs/creators -I ui/dialogs/file -I ui/dialogs/file/export -I ui/dialogs/file/export/layers -I ui/dialogs/file/export/image -I ui/dialogs/file/export/makercam -I ui/dialogs/main -I ui/dialogs/settings -I ui/dialogs/settings/dimstyles -I ui/dialogs/settings/dimstyles/dimstyle_manager -I ui/dialogs/settings/dimstyles/dimstyle_manager/support -I ui/dialogs/settings/options_device -I ui/dialogs/settings/options_drawing -I ui/dialogs/settings/options_general -I ui/dialogs/settings/options_widget -I ui/dialogs/settings/shortcuts -I ui/dock_widgets -I ui/dock_widgets/block_widget -I ui/dock_widgets/command_line -I ui/dock_widgets/entity_info -I ui/dock_widgets/layer_widget -I ui/dock_widgets/layers_tree -I ui/dock_widgets/library_widget -I ui/dock_widgets/pen_palette -I ui/dock_widgets/pen_wizard -I ui/dock_widgets/property_sheet -I ui/dock_widgets/property_sheet/lib -I ui/dock_widgets/property_sheet/lib/properties -I ui/dock_widgets/property_sheet/lib/view -I ui/dock_widgets/property_sheet/lib/view/edit -I ui/dock_widgets/property_sheet/lib/widgets -I ui/dock_widgets/property_sheet/lib/widgets/sheet -I ui/dock_widgets/property_sheet/metaentity -I ui/dock_widgets/property_sheet/metaentity/entities -I ui/dock_widgets/property_sheet/metaentity/entities/document -I ui/dock_widgets/property_sheet/properties -I ui/dock_widgets/property_sheet/properties/action -I ui/dock_widgets/property_sheet/properties/bool -I ui/dock_widgets/property_sheet/properties/color -I ui/dock_widgets/property_sheet/properties/double -I ui/dock_widgets/property_sheet/properties/enum -I ui/dock_widgets/property_sheet/properties/int -I ui/dock_widgets/property_sheet/properties/layer -I ui/dock_widgets/property_sheet/properties/linetype -I ui/dock_widgets/property_sheet/properties/linewidth -I ui/dock_widgets/property_sheet/properties/rect -I ui/dock_widgets/property_sheet/properties/rsvector -I ui/dock_widgets/property_sheet/properties/string -I ui/dock_widgets/views_list -I ui/dock_widgets/ucs_list -I ui/dock_widgets/workspaces -I ui/dock_widgets/cad -I ui/main -I ui/main/init -I ui/main/persistence -I ui/main/release_check -I ui/main/support -I ui/main/fontviewer -I ui/main/workspaces -I ui/view -I main -I main/console_dxf2pdf -I test -I plugins -I ../res -I ../res/arrows -I ../res/controls -I ../res/dxf -I ../res/gdt -I ../res/icons -I ../res/images -I ../../../Qt/6.9.0/gcc_64/include -I ../../../Qt/6.9.0/gcc_64/include/QtSvg -I ../../../Qt/6.9.0/gcc_64/include/QtPrintSupport -I ../../../Qt/6.9.0/gcc_64/include/QtWidgets -I ../../../Qt/6.9.0/gcc_64/include/QtGui -I ../../../Qt/6.9.0/gcc_64/include/QtNetwork -I ../../../Qt/6.9.0/gcc_64/include/QtCore -I ../../generated/librecad/moc -I ../../generated/librecad/ui -I ../../../Qt/6.9.0/gcc_64/mkspecs/linux-g++ -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/c++/15 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/x86_64-linux-gnu/c++/15 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/c++/15/backward -internal-isystem /usr/lib/llvm-21/lib/clang/21/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -std=gnu++1z -fdeprecated-macro -ferror-limit 19 -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -vectorize-loops -vectorize-slp -analyzer-output=html -faddrsig -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o /home/runner/work/LibreCAD/LibreCAD/out/2026-11-06-031751-4837-1 -x c++ lib/engine/document/entities/rs_hatch.cpp
1/****************************************************************************
2**
3** This file is part of the LibreCAD project, a 2D CAD program
4**
5** Copyright (C) 2010 R. van Twisk (librecad@rvt.dds.nl)
6** Copyright (C) 2001-2003 RibbonSoft. All rights reserved.
7**
8**
9** This file may be distributed and/or modified under the terms of the
10** GNU General Public License version 2 as published by the Free Software
11** Foundation and appearing in the file gpl-2.0.txt included in the
12** packaging of this file.
13**
14** This program is distributed in the hope that it will be useful,
15** but WITHOUT ANY WARRANTY; without even the implied warranty of
16** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17** GNU General Public License for more details.
18**
19** You should have received a copy of the GNU General Public License
20** along with this program; if not, write to the Free Software
21** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22**
23** This copyright notice MUST APPEAR in all copies of the script!
24**
25**********************************************************************/
26
27#include "rs_hatch.h"
28
29#include <QPainterPath>
30#include <algorithm>
31#include <cmath>
32#include <iostream>
33#include <limits>
34#include <memory>
35#include <unordered_set>
36#include <vector>
37
38#include "lc_containertraverser.h"
39#include "lc_hatchdensity.h"
40#include "lc_loopclassifier.h"
41#include "lc_looputils.h"
42#include "lc_patternlinededup.h"
43#include "lc_splinepoints.h"
44#include "rs_arc.h"
45#include "rs_circle.h"
46#include "rs_debug.h"
47#include "rs_ellipse.h"
48#include "rs_information.h"
49#include "rs_math.h"
50#include "rs_painter.h"
51#include "rs_pattern.h"
52#include "rs_patternlist.h"
53#include "rs_pen.h"
54
55namespace {
56
57// Removes entities whose length is at or below the per-hatch tolerance.
58void avoidZeroLength(std::vector<RS_Entity*>& container, double eps) {
59 container.erase(std::remove_if(container.begin(), container.end(),
60 [eps](RS_Entity* e) {
61 return e == nullptr || !(e->getLength() > eps);
62 }),
63 container.end());
64}
65
66/** True if entity is a closed atomic curve with no valid vertex endpoints. */
67bool isClosedAtomic(const RS_Entity& e) {
68 switch (e.rtti()) {
69 case RS2::EntityCircle:
70 return true;
71 case RS2::EntityEllipse: {
72 const auto& ell = static_cast<const RS_Ellipse&>(e);
73 return !ell.isArc() || std::abs(ell.getAngleLength() - 2.0 * M_PI3.14159265358979323846) < RS_TOLERANCE_ANGLE1.0e-8;
74 }
75 case RS2::EntityArc: {
76 const auto& arc = static_cast<const RS_Arc&>(e);
77 return std::abs(arc.getAngleLength() - 2.0 * M_PI3.14159265358979323846) < RS_TOLERANCE_ANGLE1.0e-8;
78 }
79 case RS2::EntitySplinePoints: {
80 const auto& sp = static_cast<const LC_SplinePoints&>(e);
81 return sp.isClosed();
82 }
83 default:
84 return false;
85 }
86}
87
88void collectSingle(std::vector<std::shared_ptr<RS_EntityContainer>>& collected,
89 const LC_LoopUtils::LC_Loops& lcLoops)
90{
91 collected.push_back(lcLoops.getOuterLoop());
92 for(const LC_LoopUtils::LC_Loops& child: lcLoops.getChildren()) {
93 collectSingle(collected, child);
94 }
95}
96
97std::vector<std::shared_ptr<RS_EntityContainer>> collectLoops(const std::shared_ptr<std::vector<LC_LoopUtils::LC_Loops>>& lcLoopsVPtr)
98{
99 if (lcLoopsVPtr==nullptr) {
100 return {};
101 }
102 std::vector<std::shared_ptr<RS_EntityContainer>> collected;
103 for(const LC_LoopUtils::LC_Loops& loop: *lcLoopsVPtr) {
104 collectSingle(collected, loop);
105 }
106 return collected;
107}
108} // anonymous namespace
109
110std::ostream& operator<<(std::ostream& os, const RS_HatchData& td) {
111 os << "(" << td.pattern.toLatin1().data() << ")";
112 return os;
113}
114
115// RS_Hatch implementation
116RS_Hatch::RS_Hatch(RS_EntityContainer* parent, const RS_HatchData& d)
117 : RS_EntityContainer(parent)
118 , m_data(d)
119 , m_area(RS_MAXDOUBLE1.0E+10)
120 , m_updateError(HATCH_UNDEFINED)
121 , m_updateRunning(false)
122 , m_needOptimization(true)
123 , m_updated(false)
124 , m_orderedLoops{std::make_shared<std::vector<LC_LoopUtils::LC_Loops>>()}
125 , m_boundaryContainers()
126{
127 setOwner(true);
128}
129
130RS_Hatch::~RS_Hatch() = default;
131
132RS_Entity* RS_Hatch::clone() const {
133 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_DEBUGGING, "RS_Hatch::clone()");
134 auto* cloneHatch = new RS_Hatch(*this);
135
136 // Force re-optimization and update to deep-copy caches and subcontainers
137 cloneHatch->m_needOptimization = true;
138 cloneHatch->m_area = RS_MAXDOUBLE1.0E+10;
139 cloneHatch->m_centroid = RS_Vector(false);
140 cloneHatch->m_centralMoment = {};
141 cloneHatch->m_secondMomentValid = false;
142 cloneHatch->m_updated = false;
143 cloneHatch->m_boundaryContainers.clear(); // Will be regenerated
144 cloneHatch->m_awaitingPlacement = true;
145 cloneHatch->update();
146 cloneHatch->m_awaitingPlacement = false;
147
148 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_DEBUGGING, "RS_Hatch::clone(): OK");
149 return cloneHatch;
150}
151
152bool RS_Hatch::validate() {
153 bool success = true;
154
155 if (!m_needOptimization || count() == 0) {
156 return success;
157 }
158
159 try {
160 // Collect child loop containers and rebuild the container list.
161 std::vector<RS_Entity *> loops;
162 for (RS_Entity *en : std::as_const(*this)) {
163 if (en != nullptr && en->isContainer())
164 loops.push_back(en);
165 }
166 setOwner(false);
167 clear();
168 setOwner(true);
169 std::copy(loops.begin(), loops.end(), std::back_inserter(*this));
170
171 // Pre-pass: pre-rotation bbox for the pattern rotation centre, which
172 // must be the same for every container so that all loops rotate into
173 // the same axis-aligned frame.
174 RS_Vector preMin(RS_MAXDOUBLE1.0E+10, RS_MAXDOUBLE1.0E+10);
175 RS_Vector preMax(-RS_MAXDOUBLE1.0E+10, -RS_MAXDOUBLE1.0E+10);
176 for (RS_Entity *en : loops) {
177 auto *cont = static_cast<RS_EntityContainer *>(en);
178 cont->forcedCalculateBorders();
179 preMin.x = std::min(preMin.x, cont->getMin().x);
180 preMin.y = std::min(preMin.y, cont->getMin().y);
181 preMax.x = std::max(preMax.x, cont->getMax().x);
182 preMax.y = std::max(preMax.y, cont->getMax().y);
183 }
184 const RS_Vector rotCenter = (preMin + preMax) * 0.5;
185
186 // Pre-eps: a quick scale-relative tolerance computed from the raw
187 // bbox and endpoint extrema BEFORE rotation. Rotation is rigid, so
188 // the diagonal is identical post-rotation and M differs by at most
189 // sqrt(2); the resulting pre-eps shares the magnitude of the final
190 // tolerance and only drives avoidZeroLength (the length filter is
191 // rotation-invariant). The final tolerance is recomputed after
192 // rotation and is what the extractor and sorter see.
193 double preMaxAbs = 0.0;
194 for (RS_Entity *en : loops) {
195 lc::LC_ContainerTraverser t{
196 *static_cast<RS_EntityContainer *>(en), RS2::ResolveAll};
197 for (RS_Entity *entity : t.entities()) {
198 if (!entity || !entity->isAtomic()) continue;
199 if (isClosedAtomic(*entity)) {
200 const RS_Vector mn = entity->getMin();
201 const RS_Vector mx = entity->getMax();
202 if (mn.valid && mx.valid) {
203 preMaxAbs = std::max({preMaxAbs,
204 std::abs(mn.x), std::abs(mn.y),
205 std::abs(mx.x), std::abs(mx.y)});
206 }
207 } else {
208 const RS_Vector s = entity->getStartpoint();
209 const RS_Vector tp = entity->getEndpoint();
210 if (s.valid)
211 preMaxAbs = std::max({preMaxAbs, std::abs(s.x), std::abs(s.y)});
212 if (tp.valid)
213 preMaxAbs = std::max({preMaxAbs, std::abs(tp.x), std::abs(tp.y)});
214 }
215 }
216 }
217 const double preDiag = (preMax - preMin).magnitude();
218 const double preEps = LC_LoopUtils::hatchTolerance(preDiag, preMaxAbs);
219
220 // Pass 1: per-container clone+rotate, accumulate the post-rotation
221 // bbox D and the endpoint extremum M for the per-hatch tolerance.
222 // Closed atomic curves (circle, full ellipse, closed spline) have
223 // no valid vertex endpoints; include their finite bbox extrema in M.
224 std::vector<std::unique_ptr<RS_EntityContainer>> perContainerClones;
225 RS_Vector bbMin(RS_MAXDOUBLE1.0E+10, RS_MAXDOUBLE1.0E+10);
226 RS_Vector bbMax(-RS_MAXDOUBLE1.0E+10, -RS_MAXDOUBLE1.0E+10);
227 double maxAbsCoord = 0.0;
228 for (RS_Entity *en : loops) {
229 std::vector<RS_Entity *> edges;
230 std::unordered_set<const RS_Entity *> seen;
231 lc::LC_ContainerTraverser traverser{
232 *static_cast<RS_EntityContainer *>(en), RS2::ResolveAll};
233 for (RS_Entity *entity : traverser.entities()) {
234 if (entity != nullptr && entity->isAtomic() && seen.insert(entity).second) {
235 edges.push_back(entity);
236 }
237 }
238 avoidZeroLength(edges, preEps);
239 if (edges.empty())
240 continue;
241
242 // A clone keeps its source's parent and layer, and so does every clone
243 // made of it, down to the cached loops. Those outlive both: drop them.
244 auto perLoopCont = std::make_unique<RS_EntityContainer>(nullptr, true);
245 for (RS_Entity *e : edges) {
246 RS_Entity *edge = e->clone();
247 edge->setParent(nullptr);
248 edge->setLayer(nullptr);
249 perLoopCont->addEntity(edge);
250 }
251
252 // For pattern hatches apply the rotation before extraction so the
253 // extracted loop coordinates are in the axis-aligned tiling frame.
254 if (!isSolid())
255 perLoopCont->rotate(rotCenter, -m_data.angle);
256 perLoopCont->forcedCalculateBorders();
257
258 bbMin.x = std::min(bbMin.x, perLoopCont->getMin().x);
259 bbMin.y = std::min(bbMin.y, perLoopCont->getMin().y);
260 bbMax.x = std::max(bbMax.x, perLoopCont->getMax().x);
261 bbMax.y = std::max(bbMax.y, perLoopCont->getMax().y);
262
263 for (RS_Entity *e : *perLoopCont) {
264 if (e == nullptr) continue;
265 if (isClosedAtomic(*e)) {
266 const RS_Vector mn = e->getMin();
267 const RS_Vector mx = e->getMax();
268 maxAbsCoord = std::max({maxAbsCoord,
269 std::abs(mn.x), std::abs(mn.y),
270 std::abs(mx.x), std::abs(mx.y)});
271 } else {
272 const RS_Vector s = e->getStartpoint();
273 const RS_Vector t = e->getEndpoint();
274 maxAbsCoord = std::max({maxAbsCoord,
275 std::abs(s.x), std::abs(s.y),
276 std::abs(t.x), std::abs(t.y)});
277 }
278 }
279
280 perContainerClones.push_back(std::move(perLoopCont));
281 }
282
283 const double diagonal = (perContainerClones.empty())
284 ? 0.0
285 : (bbMax - bbMin).magnitude();
286 const double tolerance = LC_LoopUtils::hatchTolerance(diagonal, maxAbsCoord);
287
288 // Pass 2: extract each container's loops with their vertex tables,
289 // and run them through the geometry-aware LoopSorter.
290 std::vector<LC_LoopUtils::ExtractedLoop> allExtracted;
291 for (auto &cont : perContainerClones) {
292 LC_LoopUtils::LoopExtractor extractor{*cont, tolerance};
293 auto extracted = extractor.extractLoops();
294 for (auto &el : extracted)
295 allExtracted.push_back(std::move(el));
296 }
297
298 if (allExtracted.empty())
299 throw std::runtime_error(
300 "Loop optimization failed: no loops generated");
301
302 LC_LoopUtils::LoopSorter sorter{std::move(allExtracted), tolerance};
303 auto results = sorter.getResults();
304
305 if (!results || results->empty()) {
306 throw std::runtime_error("Loop optimization failed: no loops generated");
307 }
308
309 m_orderedLoops = results;
310 // Remember the frame of m_orderedLoops: validate() rotated by
311 // -m_data.angle about rotCenter. The lattice anchor is mapped
312 // through this stored pair; picking, the centroid and the moments use it too.
313 m_pivot = rotCenter;
314 m_loopAngle = isSolid() ? 0.0 : m_data.angle;
315 m_tolerance = tolerance;
316 m_duplicateLoops = LC_LoopUtils::countDuplicateLoops(*results);
317 if (m_duplicateLoops > 0) {
318 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_WARNING,
319 "RS_Hatch::validate(): %d duplicate boundary loop(s); duplicated areas draw empty",
320 m_duplicateLoops);
321 }
322 m_needOptimization = false;
323
324 // Collect all loops (outer + holes) into flat boundary container list
325 m_boundaryContainers = collectLoops(results);
326 } catch (const std::exception& e) {
327 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_ERROR, "Hatch validation error: %s", e.what());
328 m_updateError = HATCH_INVALID_CONTOUR;
329 m_duplicateLoops = 0;
330 success = false;
331 }
332
333 return success;
334}
335
336/**
337 * Counts the number of optimized boundary loops (subcontainers).
338 * For solid hatches, this represents the fill areas.
339 * For patterns, it represents regions to trim into.
340 *
341 * @return Number of loops.
342 */
343int RS_Hatch::countLoops() const {
344 return static_cast<int>(m_orderedLoops->size());
345}
346
347int RS_Hatch::countAllLoops() const {
348 return static_cast<int>(m_boundaryContainers.size());
349}
350
351/**
352 * @return Subcontainer for the boundary loop at index (0-based).
353 */
354RS_EntityContainer* RS_Hatch::getBoundaryContainer(const int loopIndex) const {
355 if (loopIndex >= 0 && loopIndex < static_cast<int>(m_boundaryContainers.size())) {
356 return m_boundaryContainers[loopIndex].get();
357 }
358 return nullptr;
359}
360
361RS_HatchPatternFrame RS_Hatch::patternFrame() const {
362 RS_HatchPatternFrame frame;
363 frame.origin = latticeOrigin();
364 frame.scale = m_data.scale;
365 frame.angle = m_data.angle;
366 frame.tileMin = m_tileMin;
367 frame.tileSize = m_tileSize;
368 const bool sizeFinite = m_tileSize.valid &&
369 std::isfinite(m_tileSize.x) &&
370 std::isfinite(m_tileSize.y) &&
371 m_tileSize.x > 1e-12 && m_tileSize.y > 1e-12;
372 const bool minFinite = m_tileMin.valid &&
373 std::isfinite(m_tileMin.x) &&
374 std::isfinite(m_tileMin.y);
375 frame.tileValid = !isSolid() && sizeFinite && minFinite;
376 return frame;
377}
378
379namespace {
380
381// Clones every atomic edge of `node.getOuterLoop()` into a fresh WCS
382// container and recurses depth-first.
383void appendExportLoops(const LC_LoopUtils::LC_Loops& node,
384 int depth,
385 const RS_Vector& pivot,
386 double loopAngle,
387 std::vector<RS_Hatch::ExportLoop>& out) {
388 auto edges = std::make_unique<RS_EntityContainer>(nullptr, true);
389 auto source = node.getOuterLoop();
390 if (source) {
391 for (RS_Entity* e : *source) {
392 if (e == nullptr) continue;
393 RS_Entity* copy = e->clone();
394 copy->setParent(nullptr);
395 copy->setLayer(nullptr);
396 edges->addEntity(copy);
397 }
398 }
399 // The loops live in the frame rotated by -loopAngle about pivot (see
400 // validate()). Rotating by +loopAngle maps them back into WCS. The
401 // identity case stays bit-identical, so a solid hatch (loopAngle = 0)
402 // reaches the writer with the same coordinates as before.
403 if (loopAngle != 0.0)
404 edges->rotate(pivot, RS_Vector{loopAngle});
405 out.push_back({std::move(edges), depth});
406 for (const auto& child : node.getChildren())
407 appendExportLoops(child, depth + 1, pivot, loopAngle, out);
408}
409
410// True when the given extracted container holds exactly one RS_Spline
411// anywhere in it. validate() flattens source entities with ResolveAll, so
412// an RS_Spline reaches the sorter as its chord lines; the writer wants to
413// write the SPLINE edge back for the single-spline draw-hatch input.
414bool holdsSingleRsSpline(const RS_EntityContainer& container) {
415 bool found = false;
416 lc::LC_ContainerTraverser traverser{container, RS2::ResolveNone};
417 for (const RS_Entity* entity : traverser.entities()) {
418 if (entity == nullptr) continue;
419 if (entity->rtti() == RS2::EntitySpline) {
420 if (found) return false;
421 found = true;
422 continue;
423 }
424 // A nested container is explored by the traverser already; any
425 // non-spline atomic entity rules the single-spline case out.
426 if (entity->isAtomic())
427 return false;
428 }
429 return found;
430}
431
432std::unique_ptr<RS_EntityContainer> cloneSourceWithChildren(
433 const RS_EntityContainer& source) {
434 auto owned = std::make_unique<RS_EntityContainer>(nullptr, true);
435 for (RS_Entity* e : source) {
436 if (e == nullptr) continue;
437 RS_Entity* copy = e->clone();
438 copy->setParent(owned.get());
439 copy->setLayer(nullptr);
440 owned->addEntity(copy);
441 }
442 return owned;
443}
444
445} // namespace
446
447std::vector<RS_Hatch::ExportLoop> RS_Hatch::exportLoops() const {
448 std::vector<ExportLoop> out;
449 if (!m_orderedLoops || m_orderedLoops->empty())
450 return out;
451 for (const auto& root : *m_orderedLoops)
452 appendExportLoops(root, 0, m_pivot, m_loopAngle, out);
453
454 // One narrow exception: validate() flattens source entities with
455 // ResolveAll, so an RS_Spline reaches the tree as its chord lines.
456 // When the hatch's own source has exactly one container and it holds
457 // one RS_Spline (any level), hand the writer the source container
458 // instead so the SPLINE edge survives the round trip.
459 if (out.size() == 1u) {
460 const RS_EntityContainer* onlySource = nullptr;
461 int containerCount = 0;
462 for (RS_Entity* e : *this) {
463 if (e != nullptr && e->isContainer() && !e->getFlag(RS2::FlagTemp)) {
464 ++containerCount;
465 onlySource = static_cast<const RS_EntityContainer*>(e);
466 }
467 }
468 if (containerCount == 1 && onlySource != nullptr &&
469 holdsSingleRsSpline(*onlySource)) {
470 out[0].edges = cloneSourceWithChildren(*onlySource);
471 }
472 }
473 return out;
474}
475
476/**
477 * Calculates the bounding box as update() does. The contour bounds the hatch
478 * even while the layer its edges were picked from is hidden.
479 */
480void RS_Hatch::calculateBorders() {
481 forcedCalculateBorders();
482}
483
484/**
485 * Updates the hatch by validating boundaries, generating fill paths or trimmed patterns directly in container,
486 * and caching results. Skips if already updating or disabled.
487 * Sets updateError on failure.
488 */
489void RS_Hatch::update() {
490 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_DEBUGGING, "RS_Hatch::update");
491
492 // Early exits for invalid states
493 if (m_updateRunning) {
494 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_NOTICE, "RS_Hatch::update: Already updating, skipping");
495 return;
496 }
497 if (!m_updateEnabled) {
498 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_NOTICE, "RS_Hatch::update: Updates disabled, skipping");
499 return;
500 }
501 if (isDeleted()) {
502 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_NOTICE, "RS_Hatch::update: Undone hatch, skipping");
503 return;
504 }
505 activateContour(true); // active for loop validation
506
507 m_updateRunning = true;
508 m_updateError = HATCH_OK;
509 m_densityReport = LC_HatchDensityReport{};
510
511 // Delete the pattern lines of an earlier update: validate() drops them only
512 // when the loops need optimizing.
513 std::vector<RS_Entity*> loops;
514 std::vector<RS_Entity*> patternLines;
515 for (RS_Entity* en : std::as_const(*this)) {
516 (en->isContainer() ? loops : patternLines).push_back(en);
517 }
518 setOwner(false);
519 clear();
520 setOwner(true);
521 std::copy(loops.begin(), loops.end(), std::back_inserter(*this));
522 for (RS_Entity* en : patternLines) {
523 delete en;
524 }
525
526 // Reset caches
527 m_area = RS_MAXDOUBLE1.0E+10;
528 m_secondMomentValid = false;
529 // The tile bbox is written by updatePatternHatch only; reset it so a
530 // solid hatch or a missing pattern leaves patternFrame().tileValid false.
531 m_tileMin = RS_Vector(false);
532 m_tileSize = RS_Vector(false);
533
534 // Validate and optimize loops (moves boundaries to subcontainers)
535 if (!validate()) {
536 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_ERROR, "RS_Hatch::update: Validation failed");
537 // Surviving contours must remain bounded when a copy cannot revalidate.
538 forcedCalculateBorders();
539 activateContour(false);
540 m_updateRunning = false;
541 return;
542 }
543
544 // Store original attributes for child entities
545 RS_Layer* layer = getLayer();
546 const RS_Pen pen = getPen();
547
548 if (isSolid()) {
549 updateSolidHatch(layer, pen);
550 } else {
551 updatePatternHatch(layer, pen);
552 }
553
554 // Compute area and the central tensor from the moment integrator in one
555 // traversal about the stored pivot. The loops live in the frame rotated by
556 // -m_loopAngle about m_pivot; the integrator returns the moments in that
557 // frame relative to the pivot, and we map the centroid and the central
558 // tensor back to WCS once the frame-local values are known.
559 LC_AreaMoments total;
560 for (const auto& loop : *m_orderedLoops) {
561 total += loop.getTotalMoments(m_pivot);
562 }
563 m_area = total.area;
564 m_centroid = RS_Vector(false);
565 m_centralMoment = {};
566 if (m_area >= RS_TOLERANCE1.0e-10) {
567 // In the loop frame, moments are about m_pivot; d is the centroid
568 // offset from the pivot in that frame.
569 const RS_Vector d{total.first.mx / m_area, total.first.my / m_area};
570 LC_SecondMoment central =
571 total.second.getCentral(m_area, d.x, d.y);
572 RS_Vector centroid = m_pivot + d.rotated(m_loopAngle);
573 if (m_loopAngle != 0.0) {
574 central = central.regionRotated(m_loopAngle);
575 }
576 m_centroid = centroid;
577 m_centralMoment = central;
578 }
579 m_secondMomentValid = true;
580
581 forcedCalculateBorders();
582 activateContour(false); // Hide boundaries by default
583 m_updateRunning = false;
584 m_updated = true;
585
586 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_DEBUGGING, "RS_Hatch::update: Completed successfully");
587}
588
589/**
590 * Helper: Generates and caches QPainterPaths for solid fill from optimized loops.
591 */
592void RS_Hatch::updateSolidHatch([[maybe_unused]] RS_Layer* layer, [[maybe_unused]] const RS_Pen& pen) const{
593 // Solid-fill paths are viewport-dependent (world→screen transform varies per
594 // draw call), so they are built lazily in drawSolidFill() rather than cached here.
595}
596
597/**
598 * Helper: Loads, scales, rotates, and trims pattern into direct children of RS_Hatch.
599 * Performs sanity checks on sizes to prevent overflows or invalid operations.
600 */
601void RS_Hatch::updatePatternHatch(RS_Layer* layer, const RS_Pen& pen) {
602 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_DEBUGGING, "RS_Hatch::updatePatternHatch");
603
604 // An insert expansion updates its clones again once they are placed. Only
605 // that update generates lines, so each clone is trimmed and charged once.
606 if (m_awaitingPlacement && LC_HatchDensity::sharedBudget() != nullptr) {
607 return;
608 }
609
610 // A NaN scale propagates through pattern->scale() without touching the
611 // existing size checks (NaN comparisons never trip 1e-6 / RS_MAXDOUBLE);
612 // treat it as a non-finite density outcome so the hatch stays empty of
613 // children but keeps its loops, area and picking.
614 if (!std::isfinite(m_data.scale)) {
615 m_densityReport.reason = LC_HatchDensityReport::Reason::NonFinite;
616 m_densityReport.finite = false;
617 m_updateError = HATCH_TOO_DENSE;
618 return;
619 }
620 // RS_EntityContainer::scale is a no-op when |factor| <= RS_TOLERANCE, so a
621 // sub-tolerance scale would silently leave the pattern at its loaded
622 // dimensions. Reject it up front as HATCH_TOO_SMALL.
623 if (std::abs(m_data.scale) <= RS_TOLERANCE1.0e-10) {
624 m_updateError = HATCH_TOO_SMALL;
625 return;
626 }
627
628 // Request and clone pattern
629 std::unique_ptr<RS_Pattern> pattern = RS_PATTERNLISTRS_PatternList::instance()->requestPattern(m_data.pattern);
630 if (!pattern) {
631 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_ERROR, "RS_Hatch::updatePatternHatch: Pattern '%s' not found",
632 m_data.pattern.toUtf8().constData());
633 m_updateError = HATCH_PATTERN_NOT_FOUND;
634 return;
635 }
636
637 // Clone to avoid modifying the shared pattern
638 pattern.reset(dynamic_cast<RS_Pattern*>(pattern->clone()));
639 if (!pattern) {
640 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_ERROR, "RS_Hatch::updatePatternHatch: Failed to clone pattern");
641 m_updateError = HATCH_PATTERN_NOT_FOUND;
642 return;
643 }
644 pattern->setOwner(true);
645
646 // Capture the native (unscaled, unrotated) tile bbox before applying the
647 // hatch scale. patternFrame() returns it to the exporter and to Quick
648 // Info; it is the quantity DXF/DWG writers need to size the placeholder
649 // definition line.
650 pattern->calculateBorders();
651 m_tileMin = pattern->getMin();
652 m_tileSize = pattern->getSize();
653
654 pattern->scale((pattern->getMin() + pattern->getMax()) * 0.5, RS_Vector{m_data.scale, m_data.scale});
655 pattern->calculateBorders();
656 forcedCalculateBorders();
657
658 // Sanity checks: Ensure sizes are reasonable to avoid computation issues
659 const RS_Vector patternSize = pattern->getSize();
660 const RS_Vector contourSize = getSize();
661 if (contourSize.x < 1e-6 || contourSize.y < 1e-6 ||
662 patternSize.x < 1e-6 || patternSize.y < 1e-6 ||
663 contourSize.x > RS_MAXDOUBLE1.0E+10 - 1 || contourSize.y > RS_MAXDOUBLE1.0E+10 - 1 ||
664 patternSize.x > RS_MAXDOUBLE1.0E+10 - 1 || patternSize.y > RS_MAXDOUBLE1.0E+10 - 1) {
665 LC_ERRRS_Debug::Log(RS_Debug::D_ERROR)<<"RS_Hatch::"<<__func__<<"(): contour or pattern size too small, "
666 "contour=["<<contourSize.x<<'x'<<contourSize.y
667 <<"], patternSize=["<<patternSize.x<<'x'<<patternSize.y<<']';
668 m_updateError = HATCH_TOO_SMALL;
669 return;
670 }
671 // Density guard: estimate, work bound, buffered piece budget. HATCH_TOO_DENSE
672 // keeps the loops, area, moments and picking intact; the trimmed pieces are
673 // buffered over all roots and only committed after every root finishes, so
674 // an abort leaves no FlagHatchChild children behind.
675 const LC_HatchDensityPolicy& dpolicy = LC_HatchDensity::policy();
676 LC_HatchDensityReport report = LC_HatchDensity::estimate(
677 *pattern, *m_orderedLoops, /*patternTurn*/ 0.0);
678 const double softCap = dpolicy.forced ? dpolicy.hardMax : dpolicy.maxPieces;
679 const double hardCap = dpolicy.hardMax;
680 double effectiveCap = std::min(softCap, hardCap);
681
682 // Insert expansion installs a shared budget that every hatch clone of
683 // the expansion draws from. Shrink our local cap by the remaining room
684 // so the first clones do not exhaust the shared pool; the "SharedBudget"
685 // trip reason flags clones the pool refused.
686 LC_HatchPieceBudget* shared = LC_HatchDensity::sharedBudget();
687 std::size_t sharedRemaining = std::numeric_limits<std::size_t>::max();
688 if (shared != nullptr) {
689 sharedRemaining = shared->limit > shared->taken
690 ? shared->limit - shared->taken
691 : 0U;
692 const double rem = static_cast<double>(sharedRemaining);
693 if (rem < effectiveCap) effectiveCap = rem;
694 }
695 report.limit = effectiveCap;
696
697 auto tripTooDense = [&](LC_HatchDensityReport::Reason why) {
698 report.reason = why;
699 m_densityReport = report;
700 m_updateError = HATCH_TOO_DENSE;
701 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_WARNING,
702 "RS_Hatch::updatePatternHatch: HATCH_TOO_DENSE "
703 "(estimate=%.3f, work=%.3f, limit=%.3f)",
704 report.estimatedPieces, report.work, report.limit);
705 };
706
707 if (!report.finite || !(report.estimatedPieces <= hardCap)) {
708 tripTooDense(report.finite ? LC_HatchDensityReport::Reason::Estimate
709 : LC_HatchDensityReport::Reason::NonFinite);
710 return;
711 }
712 if (!dpolicy.forced && report.estimatedPieces > softCap) {
713 tripTooDense(LC_HatchDensityReport::Reason::Estimate);
714 return;
715 }
716 // The shared pool is a hard cap even in forced mode: insert expansion must
717 // never produce more hatch pieces than the budget it was called with.
718 if (shared != nullptr && report.estimatedPieces > static_cast<double>(sharedRemaining)) {
719 tripTooDense(LC_HatchDensityReport::Reason::SharedBudget);
720 return;
721 }
722
723 // The contour was optimized in a frame rotated by -m_loopAngle about
724 // m_pivot (see validate()). The lattice anchor is one WCS point
725 // (latticeOrigin() = (0, 0)) mapped into that frame; every root uses it,
726 // so two roots of one hatch share the WCS phase and a non-lattice move of
727 // the boundary does not drag the pattern. The trimmed pieces are rotated
728 // back to WCS by +m_loopAngle about the same m_pivot.
729 RS_Vector anchor = latticeOrigin();
730 anchor.rotate(m_pivot, RS_Vector{-m_loopAngle});
731
732 // gridWork is the (grid cells) * (boundary edges) overlap-test cost of
733 // createTiles. Charge it before any tile is built so a thin diagonal
734 // strip with a small tile does not run its O(B) grid walk inside the
735 // density guard.
736 const double gridWorkTotal = LC_LoopUtils::gridWork(*m_orderedLoops, *pattern, anchor);
737 if (gridWorkTotal > 10.0 * dpolicy.workCap
738 || (!dpolicy.forced && gridWorkTotal > dpolicy.workCap)) {
739 report.work = gridWorkTotal;
740 tripTooDense(LC_HatchDensityReport::Reason::Work);
741 return;
742 }
743
744 // One tile set over the union of the roots' bounding boxes, de-duplicated
745 // by (i, k); measure per-tile pair work with an early stop.
746 const std::vector<LC_LoopUtils::LC_TileSlot> tiles =
747 LC_LoopUtils::createTiles(*m_orderedLoops, *pattern, anchor);
748 const double pairStop = 10.0 * dpolicy.workCap;
749 const double pairWorkTotal =
750 LC_LoopUtils::tileWork(*m_orderedLoops, *pattern, tiles, pairStop);
751 report.work = gridWorkTotal + pairWorkTotal;
752 if (report.work > 10.0 * dpolicy.workCap
753 || (!dpolicy.forced && report.work > dpolicy.workCap)) {
754 tripTooDense(LC_HatchDensityReport::Reason::Work);
755 return;
756 }
757
758 // Buffer every produced piece under one shared budget. A trip discards
759 // the whole buffer, keeping the hatch empty of FlagHatchChild children.
760 const std::size_t cap = static_cast<std::size_t>(effectiveCap);
761 LC_HatchPieceBudget budget{cap};
762 std::unique_ptr<RS_EntityContainer> trimmed =
763 LC_LoopUtils::trimPatternEntities(*m_orderedLoops, *pattern, tiles, budget);
764 // Charge the shared pool for every piece this clone tried, tripped or not.
765 // The next hatch clone in the same expansion sees a smaller remaining
766 // budget; its sharedBudget comparison and softCap adjustment above keep
767 // the pool bounded even when every clone would independently fit.
768 if (shared != nullptr) {
769 const std::size_t charge = budget.tripped ? budget.taken + 1 : budget.taken;
770 if (shared->limit - shared->taken >= charge) {
771 shared->taken += charge;
772 } else {
773 shared->taken = shared->limit;
774 shared->tripped = true;
775 }
776 }
777 if (budget.tripped) {
778 report.attempted = budget.taken + 1; // the piece that tripped
779 trimmed.reset();
780 tripTooDense(LC_HatchDensityReport::Reason::Budget);
781 return;
782 }
783 report.attempted = budget.taken;
784
785 // Collect every produced piece from the single trimmer run while they
786 // are still in the loop frame. The LINE pieces feed one tile-relative
787 // de-dup (tolerance 1e-6 x the scaled tile size), which merges touching
788 // tile borders so no ink is drawn twice. Non-LINE pieces (arcs, circles,
789 // ellipses, ...) pass through unchanged.
790 std::vector<RS_LineData> lineData;
791 std::vector<std::unique_ptr<RS_Entity>> otherPieces;
792 if (trimmed) {
793 for (auto& owned : trimmed->takeEntities()) {
794 if (owned && owned->rtti() == RS2::EntityLine) {
795 const auto* line = static_cast<const RS_Line*>(owned.get());
796 lineData.push_back(line->getData());
797 owned.reset();
798 } else if (owned) {
799 otherPieces.push_back(std::move(owned));
800 }
801 }
802 }
803 const double tolOffset = 1e-6 * std::max(patternSize.x, patternSize.y);
804 std::vector<RS_LineData> mergedLines =
805 LC_LoopUtils::deduplicatePatternLines(std::move(lineData), tolOffset);
806
807 const RS_Vector rotationVector{m_loopAngle};
808 int addedCount = 0;
809 auto emitEntity = [&](RS_Entity* entity) {
810 entity->setPen(pen);
811 entity->setLayer(layer);
812 entity->reparent(this);
813 entity->setFlag(RS2::FlagHatchChild);
814 entity->rotate(m_pivot, rotationVector);
815 addEntity(entity);
816 ++addedCount;
817 };
818 for (const RS_LineData& ld : mergedLines) {
819 auto* line = new RS_Line(nullptr, ld);
820 line->setVisible(true);
821 emitEntity(line);
822 }
823 for (auto& owned : otherPieces) {
824 RS_Entity* entity = owned.release();
825 emitEntity(entity);
826 }
827 m_densityReport = report;
828
829 RS_DEBUGRS_Debug::instance()->print(RS_Debug::D_DEBUGGING, "RS_Hatch::updatePatternHatch: Added %d direct entities",
830 addedCount);
831}
832
833/**
834 * Toggles visibility of boundary contour entities in subcontainers.
835 * Used during border calculation or for debugging.
836 *
837 * @param visible true to show boundaries, false to hide.
838 */
839void RS_Hatch::activateContour(const bool visible) const {
840 RS_DEBUGRS_Debug::instance()->print("RS_Hatch::activateContour: %d", static_cast<int>(visible));
841 for (const auto& sub : m_boundaryContainers) {
842 if (sub) {
843 for (RS_Entity* entity : *sub) {
844 if (!entity->isDeleted() && !entity->getFlag(RS2::FlagTemp)) {
845 RS_DEBUGRS_Debug::instance()->Log()
846 << "RS_Hatch::activateContour: Setting visibility for entity "
847 << entity->getId();
848 entity->setVisible(visible);
849 }
850 }
851 }
852 }
853 RS_DEBUGRS_Debug::instance()->print("RS_Hatch::activateContour: OK");
854}
855
856/**
857 * Draws the hatch: solid fill via cached paths or pattern lines via direct children.
858 * Boundaries (in subcontainers) are not drawn by default (use activateContour for that).
859 *
860 * @param painter The painter to draw with.
861 */
862void RS_Hatch::draw(RS_Painter* painter) {
863 if (isSolid()) {
1
Assuming the condition is false
2
Taking false branch
864 drawSolidFill(painter);
865 }
866 else if (m_updateError == HATCH_TOO_DENSE) {
3
Assuming field 'm_updateError' is equal to HATCH_TOO_DENSE
4
Taking true branch
867 drawDensityTint(painter);
5
Passing value via 1st parameter 'painter'
6
Calling 'RS_Hatch::drawDensityTint'
868 }
869 else {
870 drawPatternLines(painter);
871 }
872}
873
874/**
875 * Helper: Draws solid fill using cached QPainterPaths.
876 */
877void RS_Hatch::drawSolidFill(RS_Painter* painter) const {
878 drawFill(painter, 1.0);
879}
880
881/**
882 * Helper: Fills the hatch region with the resolved pen colour at the given
883 * alpha channel. One QPainterPath carries every loop in the hierarchy as a
884 * subpath and is drawn with Qt::OddEvenFill, so holes and islands fall out
885 * of the odd-even rule naturally and crossing boundaries draw as XOR. Shared
886 * by the solid fill (alpha = 1) and the HATCH_TOO_DENSE tint, so print,
887 * print preview, and image export paths produce the same ink as the screen
888 * for both outputs.
889 */
890void RS_Hatch::drawFill(RS_Painter* painter, double alpha) const {
891 if (m_boundaryContainers.empty()) {
15
Assuming the condition is false
892 return;
893 }
894 if (!std::isfinite(alpha) || alpha <= 0.0) {
16
Assuming the condition is false
17
Assuming the condition is false
18
Taking false branch
895 return;
896 }
897 if (alpha > 1.0) alpha = 1.0;
19
Assuming the condition is false
20
Taking false branch
898
899 painter->save();
21
Called C++ object pointer is null
900 QBrush fillBrush = painter->brush();
901 QColor fillColor = painter->getPen().getColor();
902 // Fold the requested alpha into the pen's existing alpha so a pre-tinted
903 // pen still contributes.
904 const int penA = fillColor.alpha();
905 const int a = static_cast<int>(std::lround(static_cast<double>(penA) * alpha));
906 fillColor.setAlpha(std::clamp(a, 0, 255));
907 fillBrush.setColor(fillColor);
908 fillBrush.setStyle(Qt::SolidPattern);
909 painter->setBrush(fillBrush);
910
911 QPainterPath path;
912 path.setFillRule(Qt::OddEvenFill);
913 for (const auto& cont : m_boundaryContainers) {
914 if (cont == nullptr) {
915 continue;
916 }
917 if (m_loopAngle == 0.0) {
918 path.addPath(LC_LoopUtils::buildLoopPath(painter, *cont, m_tolerance));
919 continue;
920 }
921 // validate() cached a pattern hatch's loops turned by -m_loopAngle
922 // about m_pivot: turn them back to draw the tint over the hatch.
923 RS_EntityContainer turned{nullptr, true};
924 for (RS_Entity* edge : *cont) {
925 if (edge != nullptr) {
926 RS_Entity* copy = edge->clone();
927 copy->rotate(m_pivot, RS_Vector{m_loopAngle});
928 turned.addEntity(copy);
929 }
930 }
931 path.addPath(LC_LoopUtils::buildLoopPath(painter, turned, m_tolerance));
932 }
933 painter->drawPath(path);
934
935 painter->restore();
936}
937
938/**
939 * Helper: Draws the HATCH_TOO_DENSE tint. The alpha comes from
940 * LC_HatchDensity::tintAlpha, which turns the saved line/piece densities
941 * into a coverage estimate at the current zoom/stroke width.
942 */
943void RS_Hatch::drawDensityTint(RS_Painter* painter) const {
944 // Physical pixels per UCS unit (one unit mapped through the view, then
945 // through the device pixel ratio).
946 double dpr = 1.0;
947 if (painter != nullptr && painter->device() != nullptr) {
7
Assuming pointer value is null
948 dpr = painter->device()->devicePixelRatioF();
949 }
950 if (!std::isfinite(dpr) || dpr <= 0.0) dpr = 1.0;
8
Assuming the condition is false
9
Assuming the condition is false
951 const double kPhys = painter != nullptr
10
Taking false branch
11
'?' condition is false
952 ? painter->toGuiDX(1.0) * dpr
953 : dpr;
954 // Pen width in physical pixels: a non-positive stored width means a
955 // cosmetic pen (1 logical pixel).
956 double wPhys = 1.0 * dpr;
957 if (painter != nullptr) {
12
Taking false branch
958 const double sw = painter->getPen().getScreenWidth();
959 if (std::isfinite(sw) && sw > 0.0) wPhys = sw * dpr;
960 }
961 const double alpha = LC_HatchDensity::tintAlpha(m_densityReport, kPhys, wPhys);
962 drawFill(painter, alpha);
13
Passing null pointer value via 1st parameter 'painter'
14
Calling 'RS_Hatch::drawFill'
963}
964
965/**
966 * Helper: Draws pattern lines from direct atomic children (trimmed entities).
967 * Skips subcontainers (boundaries).
968 */
969void RS_Hatch::drawPatternLines(RS_Painter* painter) const {
970 const bool selected = isSelected();
971 for (RS_Entity* subEntity : *this) {
972 // Draw only direct atomic children with FlagHatchChild (patterns); skip subcontainers
973 if ((subEntity != nullptr) && !subEntity->isContainer() && subEntity->getFlag(RS2::FlagHatchChild)) {
974 subEntity->setSelectionFlag(selected); // fixme - selection - just utility entity used on draw?
975 painter->drawEntity(subEntity);
976 }
977 }
978}
979
980/**
981 * @return Total enclosed area, or RS_MAXDOUBLE if update() has not run.
982 */
983double RS_Hatch::getTotalArea() const {
984 return m_area;
985}
986
987RS_Vector RS_Hatch::getCentroid() const {
988 if (!m_secondMomentValid || m_area < RS_TOLERANCE1.0e-10) {
989 return RS_Vector(false);
990 }
991 return m_centroid;
992}
993
994LC_SecondMoment RS_Hatch::getMomentOfInertia() const {
995 if (!m_secondMomentValid || m_area < RS_TOLERANCE1.0e-10) {
996 return {};
997 }
998 return m_centralMoment;
999}
1000
1001
1002double RS_Hatch::doGetDistanceToPoint(const RS_Vector& coord, RS_Entity** entity,
1003 [[maybe_unused]] RS2::ResolveLevel level, [[maybe_unused]] double solidDist) const{
1004 // validate() cached the loops turned by -m_loopAngle about m_pivot: test
1005 // the point in that frame so picking agrees with what is drawn at every
1006 // angle. For solid hatches m_loopAngle is 0 and this is the identity.
1007 RS_Vector query = coord;
1008 if (m_loopAngle != 0.0) {
1009 query.rotate(m_pivot, -m_loopAngle);
1010 }
1011 // Global even-odd parity: a point is inside the hatch region iff
1012 // classifyAll says so, matching one-path odd-even fill and the trimmer.
1013 if (LC_LoopUtils::classifyAll(query, *m_orderedLoops) != LC_LoopUtils::PointClass::Outside) {
1014 if (entity != nullptr) {
1015 *entity = const_cast<RS_Hatch*>(this);
1016 }
1017 return 0.0;
1018 }
1019 return RS_MAXDOUBLE1.0E+10;
1020}
1021
1022void RS_Hatch::setAngle(double angle) {
1023 m_data.angle = RS_Math::correctAngle(angle);
1024 m_needOptimization = true;
1025}
1026
1027void RS_Hatch::move(const RS_Vector& offset) {
1028 m_needOptimization = true;
1029 for(RS_Entity* en: std::as_const(*this)) {
1030 if(en->isContainer()) {
1031 en->move(offset);
1032 }
1033 }
1034 update();
1035}
1036
1037void RS_Hatch::rotate(const RS_Vector& center, const double angle) {
1038 rotate(center, RS_Vector{angle});
1039}
1040
1041void RS_Hatch::rotate(const RS_Vector& center, const RS_Vector& angleVector) {
1042 m_needOptimization = true;
1043 for (RS_Entity* en : std::as_const(*this)) {
1044 if (en->isContainer()) {
1045 en->rotate(center, angleVector);
1046 }
1047 }
1048 m_data.angle = RS_Math::correctAngle(m_data.angle + angleVector.angle());
1049 update();
1050}
1051
1052void RS_Hatch::scale(const RS_Vector& center, const RS_Vector& factor) {
1053 m_needOptimization = true;
1054 for(RS_Entity* en: std::as_const(*this)) {
1055 if(en->isContainer()) {
1056 en->scale(center, factor);
1057 }
1058 }
1059 m_data.scale *= factor.x;
1060 update();
1061}
1062
1063void RS_Hatch::mirror(const RS_Vector& axisPoint1, const RS_Vector& axisPoint2) {
1064 m_needOptimization = true;
1065 for(RS_Entity* en: std::as_const(*this)) {
1066 if(en->isContainer()) {
1067 en->mirror(axisPoint1, axisPoint2);
1068 }
1069 }
1070 // Reflecting a direction α across an axis at angle θ maps it to 2θ − α.
1071 double mirrorAngle = axisPoint1.angleTo(axisPoint2);
1072 m_data.angle = RS_Math::correctAngle(2.0 * mirrorAngle - m_data.angle);
1073 update();
1074}
1075
1076void RS_Hatch::stretch(const RS_Vector& firstCorner, const RS_Vector& secondCorner,
1077 const RS_Vector& offset) {
1078 m_needOptimization = true;
1079 for(RS_Entity* en: std::as_const(*this)) {
1080 if(en->isContainer()) {
1081 en->stretch(firstCorner, secondCorner, offset);
1082 }
1083 }
1084 update();
1085}
1086
1087std::ostream& operator<<(std::ostream& os, const RS_Hatch& hatchEntity) {
1088 os << "Hatch: " << hatchEntity.getData() << "\n";
1089 return os;
1090}