/src/assimp/code/AssetLib/IFC/IFCBoolean.cpp
Line | Count | Source |
1 | | /* |
2 | | Open Asset Import Library (assimp) |
3 | | ---------------------------------------------------------------------- |
4 | | |
5 | | Copyright (c) 2006-2025, assimp team |
6 | | All rights reserved. |
7 | | |
8 | | Redistribution and use of this software in source and binary forms, |
9 | | with or without modification, are permitted provided that the |
10 | | following conditions are met: |
11 | | |
12 | | * Redistributions of source code must retain the above |
13 | | copyright notice, this list of conditions and the |
14 | | following disclaimer. |
15 | | |
16 | | * Redistributions in binary form must reproduce the above |
17 | | copyright notice, this list of conditions and the |
18 | | following disclaimer in the documentation and/or other |
19 | | materials provided with the distribution. |
20 | | |
21 | | * Neither the name of the assimp team, nor the names of its |
22 | | contributors may be used to endorse or promote products |
23 | | derived from this software without specific prior |
24 | | written permission of the assimp team. |
25 | | |
26 | | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
27 | | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
28 | | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
29 | | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
30 | | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
31 | | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
32 | | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
33 | | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
34 | | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
35 | | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
36 | | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
37 | | |
38 | | ---------------------------------------------------------------------- |
39 | | */ |
40 | | |
41 | | /// @file IFCBoolean.cpp |
42 | | /// @brief Implements a subset of Ifc boolean operations |
43 | | |
44 | | #ifndef ASSIMP_BUILD_NO_IFC_IMPORTER |
45 | | |
46 | | #include "IFCUtil.h" |
47 | | #include "Common/PolyTools.h" |
48 | | #include "PostProcessing/ProcessHelper.h" |
49 | | |
50 | | #include <iterator> |
51 | | #include <tuple> |
52 | | #include <utility> |
53 | | |
54 | | namespace Assimp { |
55 | | namespace IFC { |
56 | | |
57 | | // ------------------------------------------------------------------------------------------------ |
58 | | // Calculates intersection between line segment and plane. To catch corner cases, specify which side you prefer. |
59 | | // The function then generates a hit only if the end is beyond a certain margin in that direction, filtering out |
60 | | // "very close to plane" ghost hits as long as start and end stay directly on or within the given plane side. |
61 | | bool IntersectSegmentPlane(const IfcVector3 &p, const IfcVector3 &n, const IfcVector3 &e0, |
62 | 0 | const IfcVector3 &e1, bool assumeStartOnWhiteSide, IfcVector3 &out) { |
63 | 0 | const IfcVector3 pdelta = e0 - p, seg = e1 - e0; |
64 | 0 | const IfcFloat dotOne = n * seg, dotTwo = -(n * pdelta); |
65 | | |
66 | | // if segment ends on plane, do not report a hit. We stay on that side until a following segment starting at this |
67 | | // point leaves the plane through the other side |
68 | 0 | if (std::abs(dotOne + dotTwo) < ai_epsilon) { |
69 | 0 | return false; |
70 | 0 | } |
71 | | |
72 | | // if segment starts on the plane, report a hit only if the end lies on the *other* side |
73 | 0 | if (std::abs(dotTwo) < ai_epsilon) { |
74 | 0 | if ((assumeStartOnWhiteSide && dotOne + dotTwo < ai_epsilon) || (!assumeStartOnWhiteSide && dotOne + dotTwo > -ai_epsilon)) { |
75 | 0 | out = e0; |
76 | 0 | return true; |
77 | 0 | } else { |
78 | 0 | return false; |
79 | 0 | } |
80 | 0 | } |
81 | | |
82 | | // ignore if segment is parallel to plane and far away from it on either side |
83 | | // Warning: if there's a few thousand of such segments which slowly accumulate beyond the epsilon, no hit would be registered |
84 | 0 | if (std::abs(dotOne) < ai_epsilon) { |
85 | 0 | return false; |
86 | 0 | } |
87 | | |
88 | | // t must be in [0..1] if the intersection point is within the given segment |
89 | 0 | const IfcFloat t = dotTwo / dotOne; |
90 | 0 | if (t > 1.0 || t < 0.0) { |
91 | 0 | return false; |
92 | 0 | } |
93 | | |
94 | 0 | out = e0 + t * seg; |
95 | 0 | return true; |
96 | 0 | } |
97 | | |
98 | | // ------------------------------------------------------------------------------------------------ |
99 | 0 | void FilterPolygon(std::vector<IfcVector3> &resultpoly) { |
100 | 0 | if (resultpoly.size() < 3) { |
101 | 0 | resultpoly.clear(); |
102 | 0 | return; |
103 | 0 | } |
104 | | |
105 | 0 | IfcVector3 vmin, vmax; |
106 | 0 | ArrayBounds(resultpoly.data(), static_cast<unsigned int>(resultpoly.size()), vmin, vmax); |
107 | | |
108 | | // filter our IfcFloat points - those may happen if a point lies |
109 | | // directly on the intersection line or directly on the clipping plane |
110 | 0 | const IfcFloat epsilon = (vmax - vmin).SquareLength() / 1e6f; |
111 | 0 | FuzzyVectorCompare fz(epsilon); |
112 | 0 | std::vector<IfcVector3>::iterator e = std::unique(resultpoly.begin(), resultpoly.end(), fz); |
113 | |
|
114 | 0 | if (e != resultpoly.end()) { |
115 | 0 | resultpoly.erase(e, resultpoly.end()); |
116 | 0 | } |
117 | |
|
118 | 0 | if (!resultpoly.empty() && fz(resultpoly.front(), resultpoly.back())) { |
119 | 0 | resultpoly.pop_back(); |
120 | 0 | } |
121 | 0 | } |
122 | | |
123 | | // ------------------------------------------------------------------------------------------------ |
124 | 0 | void WritePolygon(std::vector<IfcVector3> &resultpoly, TempMesh &result) { |
125 | 0 | FilterPolygon(resultpoly); |
126 | |
|
127 | 0 | if (resultpoly.size() > 2) { |
128 | 0 | result.mVerts.insert(result.mVerts.end(), resultpoly.begin(), resultpoly.end()); |
129 | 0 | result.mVertcnt.push_back(static_cast<unsigned int>(resultpoly.size())); |
130 | 0 | } |
131 | 0 | } |
132 | | |
133 | | // ------------------------------------------------------------------------------------------------ |
134 | | void ProcessBooleanHalfSpaceDifference(const Schema_2x3::IfcHalfSpaceSolid *hs, TempMesh &result, |
135 | | const TempMesh &first_operand, |
136 | 0 | ConversionData & /*conv*/) { |
137 | 0 | ai_assert(hs != nullptr); |
138 | |
|
139 | 0 | const Schema_2x3::IfcPlane *const plane = hs->BaseSurface->ToPtr<Schema_2x3::IfcPlane>(); |
140 | 0 | if (!plane) { |
141 | 0 | IFCImporter::LogError("expected IfcPlane as base surface for the IfcHalfSpaceSolid"); |
142 | 0 | return; |
143 | 0 | } |
144 | | |
145 | | // extract plane base position vector and normal vector |
146 | 0 | IfcVector3 p, n(0.f, 0.f, 1.f); |
147 | 0 | if (plane->Position->Axis) { |
148 | 0 | ConvertDirection(n, plane->Position->Axis.Get()); |
149 | 0 | } |
150 | 0 | ConvertCartesianPoint(p, plane->Position->Location); |
151 | |
|
152 | 0 | if (!IsTrue(hs->AgreementFlag)) { |
153 | 0 | n *= -1.f; |
154 | 0 | } |
155 | | |
156 | | // clip the current contents of `meshout` against the plane we obtained from the second operand |
157 | 0 | const std::vector<IfcVector3> &in = first_operand.mVerts; |
158 | 0 | std::vector<IfcVector3> &outvert = result.mVerts; |
159 | |
|
160 | 0 | std::vector<unsigned int>::const_iterator begin = first_operand.mVertcnt.begin(), |
161 | 0 | end = first_operand.mVertcnt.end(), iit; |
162 | |
|
163 | 0 | outvert.reserve(in.size()); |
164 | 0 | result.mVertcnt.reserve(first_operand.mVertcnt.size()); |
165 | |
|
166 | 0 | unsigned int vidx = 0; |
167 | 0 | for (iit = begin; iit != end; vidx += *iit++) { |
168 | |
|
169 | 0 | unsigned int newcount = 0; |
170 | 0 | bool isAtWhiteSide = (in[vidx] - p) * n > -ai_epsilon; |
171 | 0 | for (unsigned int i = 0; i < *iit; ++i) { |
172 | 0 | const IfcVector3 &e0 = in[vidx + i], e1 = in[vidx + (i + 1) % *iit]; |
173 | | |
174 | | // does the next segment intersect the plane? |
175 | 0 | IfcVector3 isectpos; |
176 | 0 | if (IntersectSegmentPlane(p, n, e0, e1, isAtWhiteSide, isectpos)) { |
177 | 0 | if (isAtWhiteSide) { |
178 | | // e0 is on the right side, so keep it |
179 | 0 | outvert.push_back(e0); |
180 | 0 | outvert.push_back(isectpos); |
181 | 0 | newcount += 2; |
182 | 0 | } else { |
183 | | // e0 is on the wrong side, so drop it and keep e1 instead |
184 | 0 | outvert.push_back(isectpos); |
185 | 0 | ++newcount; |
186 | 0 | } |
187 | 0 | isAtWhiteSide = !isAtWhiteSide; |
188 | 0 | } else { |
189 | 0 | if (isAtWhiteSide) { |
190 | 0 | outvert.push_back(e0); |
191 | 0 | ++newcount; |
192 | 0 | } |
193 | 0 | } |
194 | 0 | } |
195 | |
|
196 | 0 | if (!newcount) { |
197 | 0 | continue; |
198 | 0 | } |
199 | | |
200 | 0 | IfcVector3 vmin, vmax; |
201 | 0 | ArrayBounds(&*(outvert.end() - newcount), newcount, vmin, vmax); |
202 | | |
203 | | // filter our IfcFloat points - those may happen if a point lies |
204 | | // directly on the intersection line. However, due to IfcFloat |
205 | | // precision a bitwise comparison is not feasible to detect |
206 | | // this case. |
207 | 0 | const IfcFloat epsilon = (vmax - vmin).SquareLength() / 1e6f; |
208 | 0 | FuzzyVectorCompare fz(epsilon); |
209 | |
|
210 | 0 | std::vector<IfcVector3>::iterator e = std::unique(outvert.end() - newcount, outvert.end(), fz); |
211 | |
|
212 | 0 | if (e != outvert.end()) { |
213 | 0 | newcount -= static_cast<unsigned int>(std::distance(e, outvert.end())); |
214 | 0 | outvert.erase(e, outvert.end()); |
215 | 0 | } |
216 | 0 | if (fz(*(outvert.end() - newcount), outvert.back())) { |
217 | 0 | outvert.pop_back(); |
218 | 0 | --newcount; |
219 | 0 | } |
220 | 0 | if (newcount > 2) { |
221 | 0 | result.mVertcnt.push_back(newcount); |
222 | 0 | } else |
223 | 0 | while (newcount-- > 0) { |
224 | 0 | result.mVerts.pop_back(); |
225 | 0 | } |
226 | 0 | } |
227 | 0 | IFCImporter::LogVerboseDebug("generating CSG geometry by plane clipping (IfcBooleanClippingResult)"); |
228 | 0 | } |
229 | | |
230 | | // ------------------------------------------------------------------------------------------------ |
231 | | // Check if e0-e1 intersects a sub-segment of the given boundary line. |
232 | | // note: this functions works on 3D vectors, but performs its intersection checks solely in xy. |
233 | | // New version takes the supposed inside/outside state as a parameter and treats corner cases as if |
234 | | // the line stays on that side. This should make corner cases more stable. |
235 | | // Two million assumptions! Boundary should have all z at 0.0, will be treated as closed, should not have |
236 | | // segments with length <1e-6, self-intersecting might break the corner case handling... just don't go there, ok? |
237 | | bool IntersectsBoundaryProfile(const IfcVector3 &e0, const IfcVector3 &e1, const std::vector<IfcVector3> &boundary, |
238 | | const bool isStartAssumedInside, std::vector<std::pair<size_t, IfcVector3>> &intersect_results, |
239 | 0 | const bool halfOpen = false) { |
240 | 0 | ai_assert(intersect_results.empty()); |
241 | | |
242 | | // determine winding order - necessary to detect segments going "inwards" or "outwards" from a point directly on the border |
243 | | // positive sum of angles means clockwise order when looking down the -Z axis |
244 | 0 | IfcFloat windingOrder = 0.0; |
245 | 0 | for (size_t i = 0, bcount = boundary.size(); i < bcount; ++i) { |
246 | 0 | IfcVector3 b01 = boundary[(i + 1) % bcount] - boundary[i]; |
247 | 0 | IfcVector3 b12 = boundary[(i + 2) % bcount] - boundary[(i + 1) % bcount]; |
248 | 0 | IfcVector3 b1_side = IfcVector3(b01.y, -b01.x, 0.0); // rotated 90° clockwise in Z plane |
249 | | // Warning: rough estimate only. A concave poly with lots of small segments each featuring a small counter rotation |
250 | | // could fool the accumulation. Correct implementation would be sum( acos( b01 * b2) * sign( b12 * b1_side)) |
251 | 0 | windingOrder += (b1_side.x * b12.x + b1_side.y * b12.y); |
252 | 0 | } |
253 | 0 | windingOrder = windingOrder > 0.0 ? 1.0 : -1.0; |
254 | |
|
255 | 0 | const IfcVector3 e = e1 - e0; |
256 | |
|
257 | 0 | for (size_t i = 0, bcount = boundary.size(); i < bcount; ++i) { |
258 | | // boundary segment i: b0-b1 |
259 | 0 | const IfcVector3 &b0 = boundary[i]; |
260 | 0 | const IfcVector3 &b1 = boundary[(i + 1) % bcount]; |
261 | 0 | IfcVector3 b = b1 - b0; |
262 | | |
263 | | // segment-segment intersection |
264 | | // solve b0 + b*s = e0 + e*t for (s,t) |
265 | 0 | const IfcFloat det = (-b.x * e.y + e.x * b.y); |
266 | 0 | if (std::abs(det) < ai_epsilon) { |
267 | | // no solutions (parallel lines) |
268 | 0 | continue; |
269 | 0 | } |
270 | 0 | IfcFloat b_sqlen_inv = 1.0 / b.SquareLength(); |
271 | |
|
272 | 0 | const IfcFloat x = b0.x - e0.x; |
273 | 0 | const IfcFloat y = b0.y - e0.y; |
274 | 0 | const IfcFloat s = (x * e.y - e.x * y) / det; // scale along boundary edge |
275 | 0 | const IfcFloat t = (x * b.y - b.x * y) / det; // scale along given segment |
276 | 0 | const IfcVector3 p = e0 + e * t; |
277 | 0 | #ifdef ASSIMP_BUILD_DEBUG |
278 | 0 | const IfcVector3 check = b0 + b * s - p; |
279 | 0 | ai_assert((IfcVector2(check.x, check.y)).SquareLength() < 1e-5); |
280 | 0 | #endif |
281 | | |
282 | | // also calculate the distance of e0 and e1 to the segment. We need to detect the "starts directly on segment" |
283 | | // and "ends directly at segment" cases |
284 | 0 | bool startsAtSegment, endsAtSegment; |
285 | 0 | { |
286 | | // calculate closest point to each end on the segment, clamp that point to the segment's length, then check |
287 | | // distance to that point. This approach is like testing if e0 is inside a capped cylinder. |
288 | 0 | IfcFloat et0 = (b.x * (e0.x - b0.x) + b.y * (e0.y - b0.y)) * b_sqlen_inv; |
289 | 0 | IfcVector3 closestPosToE0OnBoundary = b0 + std::max(IfcFloat(0.0), std::min(IfcFloat(1.0), et0)) * b; |
290 | 0 | startsAtSegment = (closestPosToE0OnBoundary - IfcVector3(e0.x, e0.y, 0.0)).SquareLength() < 1e-12; |
291 | 0 | IfcFloat et1 = (b.x * (e1.x - b0.x) + b.y * (e1.y - b0.y)) * b_sqlen_inv; |
292 | 0 | IfcVector3 closestPosToE1OnBoundary = b0 + std::max(IfcFloat(0.0), std::min(IfcFloat(1.0), et1)) * b; |
293 | 0 | endsAtSegment = (closestPosToE1OnBoundary - IfcVector3(e1.x, e1.y, 0.0)).SquareLength() < 1e-12; |
294 | 0 | } |
295 | | |
296 | | // Line segment ends at boundary -> ignore any hit, it will be handled by possibly following segments |
297 | 0 | if (endsAtSegment && !halfOpen) { |
298 | 0 | continue; |
299 | 0 | } |
300 | | |
301 | | // Line segment starts at boundary -> generate a hit only if following that line would change the INSIDE/OUTSIDE |
302 | | // state. This should catch the case where a connected set of segments has a point directly on the boundary, |
303 | | // one segment not hitting it because it ends there and the next segment not hitting it because it starts there |
304 | | // Should NOT generate a hit if the segment only touches the boundary but turns around and stays inside. |
305 | 0 | if (startsAtSegment) { |
306 | 0 | IfcVector3 inside_dir = IfcVector3(b.y, -b.x, 0.0) * windingOrder; |
307 | 0 | bool isGoingInside = (inside_dir * e) > 0.0; |
308 | 0 | if (isGoingInside == isStartAssumedInside) { |
309 | 0 | continue; |
310 | 0 | } |
311 | | |
312 | | // only insert the point into the list if it is sufficiently far away from the previous intersection point. |
313 | | // This way, we avoid duplicate detection if the intersection is directly on the vertex between two segments. |
314 | 0 | if (!intersect_results.empty() && intersect_results.back().first == i - 1) { |
315 | 0 | const IfcVector3 diff = intersect_results.back().second - e0; |
316 | 0 | if (IfcVector2(diff.x, diff.y).SquareLength() < 1e-10) { |
317 | 0 | continue; |
318 | 0 | } |
319 | 0 | } |
320 | 0 | intersect_results.emplace_back(i, e0); |
321 | 0 | continue; |
322 | 0 | } |
323 | | |
324 | | // for a valid intersection, s and t should be in range [0,1]. Including a bit of epsilon on s, potential double |
325 | | // hits on two consecutive boundary segments are filtered |
326 | 0 | if (s >= -ai_epsilon * b_sqlen_inv && s <= 1.0 + ai_epsilon * b_sqlen_inv && t >= 0.0 && (t <= 1.0 || halfOpen)) { |
327 | | // only insert the point into the list if it is sufficiently far away from the previous intersection point. |
328 | | // This way, we avoid duplicate detection if the intersection is directly on the vertex between two segments. |
329 | 0 | if (!intersect_results.empty() && intersect_results.back().first == i - 1) { |
330 | 0 | const IfcVector3 diff = intersect_results.back().second - p; |
331 | 0 | if (IfcVector2(diff.x, diff.y).SquareLength() < 1e-10) { |
332 | 0 | continue; |
333 | 0 | } |
334 | 0 | } |
335 | 0 | intersect_results.emplace_back(i, p); |
336 | 0 | } |
337 | 0 | } |
338 | |
|
339 | 0 | return !intersect_results.empty(); |
340 | 0 | } |
341 | | |
342 | | // ------------------------------------------------------------------------------------------------ |
343 | | // note: this functions works on 3D vectors, but performs its intersection checks solely in xy. |
344 | 0 | bool PointInPoly(const IfcVector3 &p, const std::vector<IfcVector3> &boundary) { |
345 | | // even-odd algorithm: take a random vector that extends from p to infinite |
346 | | // and counts how many times it intersects edges of the boundary. |
347 | | // because checking for segment intersections is prone to numeric inaccuracies |
348 | | // or double detections (i.e. when hitting multiple adjacent segments at their |
349 | | // shared vertices) we do it thrice with different rays and vote on it. |
350 | | |
351 | | // the even-odd algorithm doesn't work for points which lie directly on |
352 | | // the border of the polygon. If any of our attempts produces this result, |
353 | | // we return false immediately. |
354 | |
|
355 | 0 | std::vector<std::pair<size_t, IfcVector3>> intersected_boundary; |
356 | 0 | size_t votes = 0; |
357 | |
|
358 | 0 | IntersectsBoundaryProfile(p, p + IfcVector3(1.0, 0, 0), boundary, true, intersected_boundary, true); |
359 | 0 | votes += intersected_boundary.size() % 2; |
360 | |
|
361 | 0 | intersected_boundary.clear(); |
362 | 0 | IntersectsBoundaryProfile(p, p + IfcVector3(0, 1.0, 0), boundary, true, intersected_boundary, true); |
363 | 0 | votes += intersected_boundary.size() % 2; |
364 | |
|
365 | 0 | intersected_boundary.clear(); |
366 | 0 | IntersectsBoundaryProfile(p, p + IfcVector3(0.6, -0.6, 0.0), boundary, true, intersected_boundary, true); |
367 | 0 | votes += intersected_boundary.size() % 2; |
368 | |
|
369 | 0 | return votes > 1; |
370 | 0 | } |
371 | | |
372 | | // ------------------------------------------------------------------------------------------------ |
373 | | void ProcessPolygonalBoundedBooleanHalfSpaceDifference(const Schema_2x3::IfcPolygonalBoundedHalfSpace *hs, TempMesh &result, |
374 | | const TempMesh &first_operand, |
375 | 0 | ConversionData &conv) { |
376 | 0 | ai_assert(hs != nullptr); |
377 | |
|
378 | 0 | const Schema_2x3::IfcPlane *const plane = hs->BaseSurface->ToPtr<Schema_2x3::IfcPlane>(); |
379 | 0 | if (!plane) { |
380 | 0 | IFCImporter::LogError("expected IfcPlane as base surface for the IfcHalfSpaceSolid"); |
381 | 0 | return; |
382 | 0 | } |
383 | | |
384 | | // extract plane base position vector and normal vector |
385 | 0 | IfcVector3 p, n(0.f, 0.f, 1.f); |
386 | 0 | if (plane->Position->Axis) { |
387 | 0 | ConvertDirection(n, plane->Position->Axis.Get()); |
388 | 0 | } |
389 | 0 | ConvertCartesianPoint(p, plane->Position->Location); |
390 | |
|
391 | 0 | if (!IsTrue(hs->AgreementFlag)) { |
392 | 0 | n *= -1.f; |
393 | 0 | } |
394 | |
|
395 | 0 | n.Normalize(); |
396 | | |
397 | | // obtain the polygonal bounding volume |
398 | 0 | std::shared_ptr<TempMesh> profile = std::make_shared<TempMesh>(); |
399 | 0 | if (!ProcessCurve(hs->PolygonalBoundary, *profile, conv)) { |
400 | 0 | IFCImporter::LogError("expected valid polyline for boundary of boolean halfspace"); |
401 | 0 | return; |
402 | 0 | } |
403 | | |
404 | | // determine winding order by calculating the normal. |
405 | 0 | IfcVector3 profileNormal = TempMesh::ComputePolygonNormal(profile->mVerts.data(), profile->mVerts.size()); |
406 | |
|
407 | 0 | IfcMatrix4 proj_inv; |
408 | 0 | ConvertAxisPlacement(proj_inv, hs->Position); |
409 | | |
410 | | // and map everything into a plane coordinate space so all intersection |
411 | | // tests can be done in 2D space. |
412 | 0 | IfcMatrix4 proj = proj_inv; |
413 | 0 | proj.Inverse(); |
414 | | |
415 | | // clip the current contents of `meshout` against the plane we obtained from the second operand |
416 | 0 | const std::vector<IfcVector3> &in = first_operand.mVerts; |
417 | 0 | std::vector<IfcVector3> &outvert = result.mVerts; |
418 | 0 | std::vector<unsigned int> &outvertcnt = result.mVertcnt; |
419 | |
|
420 | 0 | outvert.reserve(in.size()); |
421 | 0 | outvertcnt.reserve(first_operand.mVertcnt.size()); |
422 | |
|
423 | 0 | unsigned int vidx = 0; |
424 | 0 | std::vector<unsigned int>::const_iterator begin = first_operand.mVertcnt.begin(); |
425 | 0 | std::vector<unsigned int>::const_iterator end = first_operand.mVertcnt.end(); |
426 | 0 | std::vector<unsigned int>::const_iterator iit; |
427 | 0 | for (iit = begin; iit != end; vidx += *iit++) { |
428 | | // Our new approach: we cut the poly along the plane, then we intersect the part on the black side of the plane |
429 | | // against the bounding polygon. All the white parts, and the black part outside the boundary polygon, are kept. |
430 | 0 | std::vector<IfcVector3> whiteside, blackside; |
431 | |
|
432 | 0 | { |
433 | 0 | const IfcVector3 *srcVertices = &in[vidx]; |
434 | 0 | const size_t srcVtxCount = *iit; |
435 | 0 | if (srcVtxCount == 0) |
436 | 0 | continue; |
437 | | |
438 | 0 | IfcVector3 polyNormal = TempMesh::ComputePolygonNormal(srcVertices, srcVtxCount, true); |
439 | | |
440 | | // if the poly is parallel to the plane, put it completely on the black or white side |
441 | 0 | if (std::abs(polyNormal * n) > 0.9999) { |
442 | 0 | bool isOnWhiteSide = (srcVertices[0] - p) * n > -ai_epsilon; |
443 | 0 | std::vector<IfcVector3> &targetSide = isOnWhiteSide ? whiteside : blackside; |
444 | 0 | targetSide.insert(targetSide.end(), srcVertices, srcVertices + srcVtxCount); |
445 | 0 | } else { |
446 | | // otherwise start building one polygon for each side. Whenever the current line segment intersects the plane |
447 | | // we put a point there as an end of the current segment. Then we switch to the other side, put a point there, too, |
448 | | // as a beginning of the current segment, and simply continue accumulating vertices. |
449 | 0 | bool isCurrentlyOnWhiteSide = ((srcVertices[0]) - p) * n > -ai_epsilon; |
450 | 0 | for (size_t a = 0; a < srcVtxCount; ++a) { |
451 | 0 | IfcVector3 e0 = srcVertices[a]; |
452 | 0 | IfcVector3 e1 = srcVertices[(a + 1) % srcVtxCount]; |
453 | 0 | IfcVector3 ei; |
454 | | |
455 | | // put starting point to the current mesh |
456 | 0 | std::vector<IfcVector3> &trgt = isCurrentlyOnWhiteSide ? whiteside : blackside; |
457 | 0 | trgt.push_back(srcVertices[a]); |
458 | | |
459 | | // if there's an intersection, put an end vertex there, switch to the other side's mesh, |
460 | | // and add a starting vertex there, too |
461 | 0 | bool isPlaneHit = IntersectSegmentPlane(p, n, e0, e1, isCurrentlyOnWhiteSide, ei); |
462 | 0 | if (isPlaneHit) { |
463 | 0 | if (trgt.empty() || (trgt.back() - ei).SquareLength() > 1e-12) |
464 | 0 | trgt.push_back(ei); |
465 | 0 | isCurrentlyOnWhiteSide = !isCurrentlyOnWhiteSide; |
466 | 0 | std::vector<IfcVector3> &newtrgt = isCurrentlyOnWhiteSide ? whiteside : blackside; |
467 | 0 | newtrgt.push_back(ei); |
468 | 0 | } |
469 | 0 | } |
470 | 0 | } |
471 | 0 | } |
472 | | |
473 | | // the part on the white side can be written into the target mesh right away |
474 | 0 | WritePolygon(whiteside, result); |
475 | | |
476 | | // The black part is the piece we need to get rid of, but only the part of it within the boundary polygon. |
477 | | // So we now need to construct all the polygons that result from BlackSidePoly minus BoundaryPoly. |
478 | 0 | FilterPolygon(blackside); |
479 | | |
480 | | // Complicated, II. We run along the polygon. a) When we're inside the boundary, we run on until we hit an |
481 | | // intersection, which means we're leaving it. We then start a new out poly there. b) When we're outside the |
482 | | // boundary, we start collecting vertices until we hit an intersection, then we run along the boundary until we hit |
483 | | // an intersection, then we switch back to the poly and run on on this one again, and so on until we got a closed |
484 | | // loop. Then we continue with the path we left to catch potential additional polys on the other side of the |
485 | | // boundary as described in a) |
486 | 0 | if (!blackside.empty()) { |
487 | | // poly edge index, intersection point, edge index in boundary poly |
488 | 0 | std::vector<std::tuple<size_t, IfcVector3, size_t>> intersections; |
489 | 0 | bool startedInside = PointInPoly(proj * blackside.front(), profile->mVerts); |
490 | 0 | bool isCurrentlyInside = startedInside; |
491 | |
|
492 | 0 | std::vector<std::pair<size_t, IfcVector3>> intersected_boundary; |
493 | |
|
494 | 0 | for (size_t a = 0; a < blackside.size(); ++a) { |
495 | 0 | const IfcVector3 e0 = proj * blackside[a]; |
496 | 0 | const IfcVector3 e1 = proj * blackside[(a + 1) % blackside.size()]; |
497 | |
|
498 | 0 | intersected_boundary.clear(); |
499 | 0 | IntersectsBoundaryProfile(e0, e1, profile->mVerts, isCurrentlyInside, intersected_boundary); |
500 | | // sort the hits by distance from e0 to get the correct in/out/in sequence. Manually :-( I miss you, C++11. |
501 | 0 | if (intersected_boundary.size() > 1) { |
502 | 0 | bool keepSorting = true; |
503 | 0 | while (keepSorting) { |
504 | 0 | keepSorting = false; |
505 | 0 | for (size_t b = 0; b < intersected_boundary.size() - 1; ++b) { |
506 | 0 | if ((intersected_boundary[b + 1].second - e0).SquareLength() < (intersected_boundary[b].second - e0).SquareLength()) { |
507 | 0 | keepSorting = true; |
508 | 0 | std::swap(intersected_boundary[b + 1], intersected_boundary[b]); |
509 | 0 | } |
510 | 0 | } |
511 | 0 | } |
512 | 0 | } |
513 | | // now add them to the list of intersections |
514 | 0 | for (size_t b = 0; b < intersected_boundary.size(); ++b) |
515 | 0 | intersections.emplace_back(a, proj_inv * intersected_boundary[b].second, intersected_boundary[b].first); |
516 | | |
517 | | // and calculate our new inside/outside state |
518 | 0 | if (intersected_boundary.size() & 1) |
519 | 0 | isCurrentlyInside = !isCurrentlyInside; |
520 | 0 | } |
521 | | |
522 | | // we got a list of in-out-combinations of intersections. That should be an even number of intersections, or |
523 | | // we are facing a non-recoverable error. |
524 | 0 | if ((intersections.size() & 1) != 0) { |
525 | 0 | IFCImporter::LogWarn("Odd number of intersections, can't work with that. Omitting half space boundary check."); |
526 | 0 | continue; |
527 | 0 | } |
528 | | |
529 | 0 | if (intersections.size() > 1) { |
530 | | // If we started outside, the first intersection is a out->in intersection. Cycle them so that it |
531 | | // starts with an intersection leaving the boundary |
532 | 0 | if (!startedInside) |
533 | 0 | for (size_t b = 0; b < intersections.size() - 1; ++b) |
534 | 0 | std::swap(intersections[b], intersections[(b + intersections.size() - 1) % intersections.size()]); |
535 | | |
536 | | // Filter pairs of out->in->out that lie too close to each other. |
537 | 0 | for (size_t a = 0; intersections.size() > 0 && a < intersections.size() - 1; /**/) { |
538 | 0 | if ((std::get<1>(intersections[a]) - std::get<1>(intersections[(a + 1) % intersections.size()])).SquareLength() < 1e-10) |
539 | 0 | intersections.erase(intersections.begin() + a, intersections.begin() + a + 2); |
540 | 0 | else |
541 | 0 | a++; |
542 | 0 | } |
543 | 0 | if (intersections.size() > 1 && (std::get<1>(intersections.back()) - std::get<1>(intersections.front())).SquareLength() < 1e-10) { |
544 | 0 | intersections.pop_back(); |
545 | 0 | intersections.erase(intersections.begin()); |
546 | 0 | } |
547 | 0 | } |
548 | | |
549 | | // no intersections at all: either completely inside the boundary, so everything gets discarded, or completely outside. |
550 | | // in the latter case we're implementional lost. I'm simply going to ignore this, so a large poly will not get any |
551 | | // holes if the boundary is smaller and does not touch it anywhere. |
552 | 0 | if (intersections.empty()) { |
553 | | // starting point was outside -> everything is outside the boundary -> nothing is clipped -> add black side |
554 | | // to result mesh unchanged |
555 | 0 | if (!startedInside) { |
556 | 0 | outvertcnt.push_back(static_cast<unsigned int>(blackside.size())); |
557 | 0 | outvert.insert(outvert.end(), blackside.begin(), blackside.end()); |
558 | 0 | continue; |
559 | 0 | } else { |
560 | | // starting point was inside the boundary -> everything is inside the boundary -> nothing is spared from the |
561 | | // clipping -> nothing left to add to the result mesh |
562 | 0 | continue; |
563 | 0 | } |
564 | 0 | } |
565 | | |
566 | | // determine the direction in which we're marching along the boundary polygon. If the src poly is faced upwards |
567 | | // and the boundary is also winded this way, we need to march *backwards* on the boundary. |
568 | 0 | const IfcVector3 polyNormal = IfcMatrix3(proj) * TempMesh::ComputePolygonNormal(blackside.data(), blackside.size()); |
569 | 0 | bool marchBackwardsOnBoundary = (profileNormal * polyNormal) >= 0.0; |
570 | | |
571 | | // Build closed loops from these intersections. Starting from an intersection leaving the boundary we |
572 | | // walk along the polygon to the next intersection (which should be an IS entering the boundary poly). |
573 | | // From there we walk along the boundary until we hit another intersection leaving the boundary, |
574 | | // walk along the poly to the next IS and so on until we're back at the starting point. |
575 | | // We remove every intersection we "used up", so any remaining intersection is the start of a new loop. |
576 | 0 | while (!intersections.empty()) { |
577 | 0 | std::vector<IfcVector3> resultpoly; |
578 | 0 | size_t currentIntersecIdx = 0; |
579 | |
|
580 | 0 | while (true) { |
581 | 0 | ai_assert(intersections.size() > currentIntersecIdx + 1); |
582 | 0 | std::tuple<size_t, IfcVector3, size_t> currintsec = intersections[currentIntersecIdx + 0]; |
583 | 0 | std::tuple<size_t, IfcVector3, size_t> nextintsec = intersections[currentIntersecIdx + 1]; |
584 | 0 | intersections.erase(intersections.begin() + currentIntersecIdx, intersections.begin() + currentIntersecIdx + 2); |
585 | | |
586 | | // we start with an in->out intersection |
587 | 0 | resultpoly.push_back(std::get<1>(currintsec)); |
588 | | // climb along the polygon to the next intersection, which should be an out->in |
589 | 0 | size_t numPolyPoints = (std::get<0>(currintsec) > std::get<0>(nextintsec) ? blackside.size() : 0) + std::get<0>(nextintsec) - std::get<0>(currintsec); |
590 | 0 | for (size_t a = 1; a <= numPolyPoints; ++a) |
591 | 0 | resultpoly.push_back(blackside[(std::get<0>(currintsec) + a) % blackside.size()]); |
592 | | // put the out->in intersection |
593 | 0 | resultpoly.push_back(std::get<1>(nextintsec)); |
594 | | |
595 | | // generate segments along the boundary polygon that lie in the poly's plane until we hit another intersection |
596 | 0 | IfcVector3 startingPoint = proj * std::get<1>(nextintsec); |
597 | 0 | size_t currentBoundaryEdgeIdx = (std::get<2>(nextintsec) + (marchBackwardsOnBoundary ? 1 : 0)) % profile->mVerts.size(); |
598 | 0 | size_t nextIntsecIdx = SIZE_MAX; |
599 | 0 | while (nextIntsecIdx == SIZE_MAX) { |
600 | 0 | IfcFloat t = 1e10; |
601 | |
|
602 | 0 | size_t nextBoundaryEdgeIdx = marchBackwardsOnBoundary ? (currentBoundaryEdgeIdx + profile->mVerts.size() - 1) : currentBoundaryEdgeIdx + 1; |
603 | 0 | nextBoundaryEdgeIdx %= profile->mVerts.size(); |
604 | | // vertices of the current boundary segments |
605 | 0 | IfcVector3 currBoundaryPoint = profile->mVerts[currentBoundaryEdgeIdx]; |
606 | 0 | IfcVector3 nextBoundaryPoint = profile->mVerts[nextBoundaryEdgeIdx]; |
607 | | // project the two onto the polygon |
608 | 0 | if (std::abs(polyNormal.z) > 1e-5) { |
609 | 0 | currBoundaryPoint.z = startingPoint.z + (currBoundaryPoint.x - startingPoint.x) * polyNormal.x / polyNormal.z + (currBoundaryPoint.y - startingPoint.y) * polyNormal.y / polyNormal.z; |
610 | 0 | nextBoundaryPoint.z = startingPoint.z + (nextBoundaryPoint.x - startingPoint.x) * polyNormal.x / polyNormal.z + (nextBoundaryPoint.y - startingPoint.y) * polyNormal.y / polyNormal.z; |
611 | 0 | } |
612 | | |
613 | | // build a direction that goes along the boundary border but lies in the poly plane |
614 | 0 | IfcVector3 boundaryPlaneNormal = ((nextBoundaryPoint - currBoundaryPoint) ^ profileNormal).Normalize(); |
615 | 0 | IfcVector3 dirAtPolyPlane = (boundaryPlaneNormal ^ polyNormal).Normalize() * (marchBackwardsOnBoundary ? -1.0 : 1.0); |
616 | | // if we can project the direction to the plane, we can calculate a maximum marching distance along that dir |
617 | | // until we finish that boundary segment and continue on the next |
618 | 0 | if (std::abs(polyNormal.z) > 1e-5) { |
619 | 0 | t = std::min(t, (nextBoundaryPoint - startingPoint).Length()); |
620 | 0 | } |
621 | | |
622 | | // check if the direction hits the loop start - if yes, we got a poly to output |
623 | 0 | IfcVector3 dirToThatPoint = proj * resultpoly.front() - startingPoint; |
624 | 0 | IfcFloat tpt = dirToThatPoint * dirAtPolyPlane; |
625 | 0 | if (tpt > -1e-6 && tpt <= t && (dirToThatPoint - tpt * dirAtPolyPlane).SquareLength() < 1e-10) { |
626 | 0 | nextIntsecIdx = intersections.size(); // dirty hack to end marching along the boundary and signal the end of the loop |
627 | 0 | t = tpt; |
628 | 0 | } |
629 | | |
630 | | // also check if the direction hits any in->out intersections earlier. If we hit one, we can switch back |
631 | | // to marching along the poly border from that intersection point |
632 | 0 | for (size_t a = 0; a < intersections.size(); a += 2) { |
633 | 0 | dirToThatPoint = proj * std::get<1>(intersections[a]) - startingPoint; |
634 | 0 | tpt = dirToThatPoint * dirAtPolyPlane; |
635 | 0 | if (tpt > -1e-6 && tpt <= t && (dirToThatPoint - tpt * dirAtPolyPlane).SquareLength() < 1e-10) { |
636 | 0 | nextIntsecIdx = a; // switch back to poly and march on from this in->out intersection |
637 | 0 | t = tpt; |
638 | 0 | } |
639 | 0 | } |
640 | | |
641 | | // if we keep marching on the boundary, put the segment end point to the result poly and well... keep marching |
642 | 0 | if (nextIntsecIdx == SIZE_MAX) { |
643 | 0 | resultpoly.push_back(proj_inv * nextBoundaryPoint); |
644 | 0 | currentBoundaryEdgeIdx = nextBoundaryEdgeIdx; |
645 | 0 | startingPoint = nextBoundaryPoint; |
646 | 0 | } |
647 | | |
648 | | // quick endless loop check |
649 | 0 | if (resultpoly.size() > blackside.size() + profile->mVerts.size()) { |
650 | 0 | IFCImporter::LogError("Encountered endless loop while clipping polygon against poly-bounded half space."); |
651 | 0 | break; |
652 | 0 | } |
653 | 0 | } |
654 | | |
655 | | // we're back on the poly - if this is the intersection we started from, we got a closed loop. |
656 | 0 | if (nextIntsecIdx >= intersections.size()) { |
657 | 0 | break; |
658 | 0 | } |
659 | | |
660 | | // otherwise it's another intersection. Continue marching from there. |
661 | 0 | currentIntersecIdx = nextIntsecIdx; |
662 | 0 | } |
663 | |
|
664 | 0 | WritePolygon(resultpoly, result); |
665 | 0 | } |
666 | 0 | } |
667 | 0 | } |
668 | 0 | IFCImporter::LogVerboseDebug("generating CSG geometry by plane clipping with polygonal bounding (IfcBooleanClippingResult)"); |
669 | 0 | } |
670 | | |
671 | | // ------------------------------------------------------------------------------------------------ |
672 | | void ProcessBooleanExtrudedAreaSolidDifference(const Schema_2x3::IfcExtrudedAreaSolid *as, |
673 | | TempMesh &result, |
674 | | const TempMesh &first_operand, |
675 | 0 | ConversionData &conv) { |
676 | 0 | ai_assert(as != nullptr); |
677 | | |
678 | | // This case is handled by reduction to an instance of the quadrify() algorithm. |
679 | | // Obviously, this won't work for arbitrarily complex cases. In fact, the first |
680 | | // operand should be near-planar. Luckily, this is usually the case in Ifc |
681 | | // buildings. |
682 | |
|
683 | 0 | std::shared_ptr<TempMesh> meshtmp = std::make_shared<TempMesh>(); |
684 | 0 | ProcessExtrudedAreaSolid(*as, *meshtmp, conv, false); |
685 | |
|
686 | 0 | std::vector<TempOpening> openings(1, TempOpening(as, IfcVector3(0, 0, 0), std::move(meshtmp), std::shared_ptr<TempMesh>())); |
687 | |
|
688 | 0 | result = first_operand; |
689 | |
|
690 | 0 | TempMesh temp; |
691 | |
|
692 | 0 | std::vector<IfcVector3>::const_iterator vit = first_operand.mVerts.begin(); |
693 | 0 | for (unsigned int pcount : first_operand.mVertcnt) { |
694 | 0 | temp.Clear(); |
695 | |
|
696 | 0 | temp.mVerts.insert(temp.mVerts.end(), vit, vit + pcount); |
697 | 0 | temp.mVertcnt.push_back(pcount); |
698 | | |
699 | | // The algorithms used to generate mesh geometry sometimes |
700 | | // spit out lines or other degenerates which must be |
701 | | // filtered to avoid running into assertions later on. |
702 | | |
703 | | // ComputePolygonNormal returns the Newell normal, so the |
704 | | // length of the normal is the area of the polygon. |
705 | 0 | const IfcVector3 &normal = temp.ComputeLastPolygonNormal(false); |
706 | 0 | if (normal.SquareLength() < static_cast<IfcFloat>(1e-5)) { |
707 | 0 | IFCImporter::LogWarn("skipping degenerate polygon (ProcessBooleanExtrudedAreaSolidDifference)"); |
708 | 0 | continue; |
709 | 0 | } |
710 | | |
711 | 0 | GenerateOpenings(openings, temp, false, true); |
712 | 0 | result.Append(temp); |
713 | |
|
714 | 0 | vit += pcount; |
715 | 0 | } |
716 | |
|
717 | 0 | IFCImporter::LogVerboseDebug("generating CSG geometry by geometric difference to a solid (IfcExtrudedAreaSolid)"); |
718 | 0 | } |
719 | | |
720 | | // ------------------------------------------------------------------------------------------------ |
721 | 0 | void ProcessBoolean(const Schema_2x3::IfcBooleanResult &boolean, TempMesh &result, ConversionData &conv) { |
722 | | // supported CSG operations: |
723 | | // DIFFERENCE |
724 | 0 | if (const Schema_2x3::IfcBooleanResult *const clip = boolean.ToPtr<Schema_2x3::IfcBooleanResult>()) { |
725 | 0 | if (clip->Operator != "DIFFERENCE") { |
726 | 0 | IFCImporter::LogWarn("encountered unsupported boolean operator: ", (std::string)clip->Operator); |
727 | 0 | return; |
728 | 0 | } |
729 | | |
730 | | // supported cases (1st operand): |
731 | | // IfcBooleanResult -- call ProcessBoolean recursively |
732 | | // IfcSweptAreaSolid -- obtain polygonal geometry first |
733 | | |
734 | | // supported cases (2nd operand): |
735 | | // IfcHalfSpaceSolid -- easy, clip against plane |
736 | | // IfcExtrudedAreaSolid -- reduce to an instance of the quadrify() algorithm |
737 | | |
738 | 0 | const Schema_2x3::IfcHalfSpaceSolid *const hs = clip->SecondOperand->ResolveSelectPtr<Schema_2x3::IfcHalfSpaceSolid>(conv.db); |
739 | 0 | const Schema_2x3::IfcExtrudedAreaSolid *const as = clip->SecondOperand->ResolveSelectPtr<Schema_2x3::IfcExtrudedAreaSolid>(conv.db); |
740 | 0 | if (!hs && !as) { |
741 | 0 | IFCImporter::LogError("expected IfcHalfSpaceSolid or IfcExtrudedAreaSolid as second clipping operand"); |
742 | 0 | return; |
743 | 0 | } |
744 | | |
745 | 0 | TempMesh first_operand; |
746 | 0 | if (const Schema_2x3::IfcBooleanResult *const op0 = clip->FirstOperand->ResolveSelectPtr<Schema_2x3::IfcBooleanResult>(conv.db)) { |
747 | 0 | ProcessBoolean(*op0, first_operand, conv); |
748 | 0 | } else if (const Schema_2x3::IfcSweptAreaSolid *const swept = clip->FirstOperand->ResolveSelectPtr<Schema_2x3::IfcSweptAreaSolid>(conv.db)) { |
749 | 0 | ProcessSweptAreaSolid(*swept, first_operand, conv); |
750 | 0 | } else { |
751 | 0 | IFCImporter::LogError("expected IfcSweptAreaSolid or IfcBooleanResult as first clipping operand"); |
752 | 0 | return; |
753 | 0 | } |
754 | | |
755 | 0 | if (hs) { |
756 | |
|
757 | 0 | const Schema_2x3::IfcPolygonalBoundedHalfSpace *const hs_bounded = clip->SecondOperand->ResolveSelectPtr<Schema_2x3::IfcPolygonalBoundedHalfSpace>(conv.db); |
758 | 0 | if (hs_bounded) { |
759 | 0 | ProcessPolygonalBoundedBooleanHalfSpaceDifference(hs_bounded, result, first_operand, conv); |
760 | 0 | } else { |
761 | 0 | ProcessBooleanHalfSpaceDifference(hs, result, first_operand, conv); |
762 | 0 | } |
763 | 0 | } else { |
764 | 0 | ProcessBooleanExtrudedAreaSolidDifference(as, result, first_operand, conv); |
765 | 0 | } |
766 | 0 | } else { |
767 | 0 | IFCImporter::LogWarn("skipping unknown IfcBooleanResult entity, type is ", boolean.GetClassName()); |
768 | 0 | } |
769 | 0 | } |
770 | | |
771 | | } // namespace IFC |
772 | | } // namespace Assimp |
773 | | |
774 | | #endif // ASSIMP_BUILD_NO_IFC_IMPORTER |