Lagrange
Loading...
Searching...
No Matches
generate_swept_surface.h
1/*
2 * Copyright 2021 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12#pragma once
13
14#include <lagrange/Logger.h>
15#include <lagrange/Mesh.h>
16#include <lagrange/common.h>
17#include <lagrange/compute_normal.h>
18#include <lagrange/compute_triangle_normal.h>
19#include <lagrange/create_mesh.h>
20#include <lagrange/legacy/inline.h>
21#include <lagrange/primitive/SweepPath.h>
22#include <lagrange/primitive/generation_utils.h>
23#include <lagrange/utils/assert.h>
24#include <lagrange/utils/safe_cast.h>
25
26#include <Eigen/Geometry>
27
28#include <lagrange/utils/warnoff.h>
29#include <lagrange/utils/warnon.h>
30#include <tbb/parallel_for.h>
31
32#include <lagrange/internal/constants.h>
33#include <limits>
34#include <vector>
35
36namespace lagrange {
37namespace primitive {
38LAGRANGE_LEGACY_INLINE
39namespace legacy {
40
41namespace internal {
42
43template <typename Derived>
44bool is_path_closed(const Eigen::MatrixBase<Derived>& path)
45{
46 if (path.rows() <= 2) return false;
47
48 using Scalar = typename Derived::Scalar;
49 constexpr Scalar TOL = std::numeric_limits<Scalar>::epsilon() * 10;
50 return (path.row(0) - path.row(path.rows() - 1)).squaredNorm() < TOL;
51}
52
66template <typename Derived>
67int compute_profile_breaks(
68 const Eigen::MatrixBase<Derived>& profile,
69 const std::vector<ScalarOf<Derived>>& arc_lengths,
70 const std::vector<ScalarOf<Derived>>& turning_angles,
71 typename Derived::Scalar max_len,
72 std::vector<bool>& breaks)
73{
74 using Scalar = typename Derived::Scalar;
75 using Index = int;
76 const Index N = static_cast<Index>(profile.rows());
77 la_runtime_assert(N > 1, "Invalid profile with less than 2 points.");
78
79 breaks.resize(N, false);
80
81 const Index num_pieces = std::max<Index>(
82 (max_len > 0 ? static_cast<Index>(std::ceil(arc_lengths[N - 1] / max_len)) : 1),
83 1);
84 const Scalar ave_len = arc_lengths[N - 1] / num_pieces;
85
86 constexpr Scalar EPSILON = std::numeric_limits<Scalar>::epsilon() * 100;
87 Index strip_index = 0;
88 Scalar prev_arc_length = 0;
89 for (Index i = 1; i < N - 1; i++) {
90 if (std::abs(turning_angles[i]) > lagrange::internal::pi / 4 ||
91 arc_lengths[i] - prev_arc_length > ave_len + EPSILON) {
92 breaks[i] = true;
93 prev_arc_length = arc_lengths[i];
94 strip_index++;
95 }
96 }
97
98 return strip_index + 1;
99}
100
104template <typename Derived>
105std::vector<typename Derived::Scalar> compute_turning_angles(
106 const Eigen::PlainObjectBase<Derived>& profile)
107{
108 using Scalar = typename Derived::Scalar;
109
110 const size_t N = safe_cast<size_t>(profile.rows());
111 const bool profile_closed = is_path_closed(profile);
112 std::vector<Scalar> profile_turning_angles(N, 0);
113
114 Eigen::Matrix<Scalar, 1, 3> v0, v1;
115 for (size_t i = 1; i < N - 1; i++) {
116 v0 = profile.row(i) - profile.row(i - 1);
117 v1 = profile.row(i + 1) - profile.row(i);
118 profile_turning_angles[i] = std::atan2(v1.cross(v0).norm(), v1.dot(v0));
119 }
120 if (profile_closed) {
121 v0 = profile.row(N - 1) - profile.row(N - 2);
122 v1 = profile.row(1) - profile.row(0);
123 Scalar angle = std::atan2(v1.cross(v0).norm(), v1.dot(v0));
124 profile_turning_angles[0] = angle;
125 profile_turning_angles[N - 1] = angle;
126 }
127
128 return profile_turning_angles;
129}
130
148template <typename MeshType>
149void generate_uv(
150 MeshType& mesh,
151 const VertexArrayOf<MeshType>& profile,
152 const std::vector<Eigen::Transform<ScalarOf<MeshType>, 3, Eigen::AffineCompact>>& transforms,
153 ScalarOf<MeshType> max_strip_len,
154 const std::vector<ScalarOf<MeshType>>& profile_turning_angles)
155{
156 using Scalar = ScalarOf<MeshType>;
157 using Index = IndexOf<MeshType>;
158 using UVArray = typename MeshType::UVArray;
159 using UVIndices = typename MeshType::UVIndices;
160
161 const Index N = safe_cast<Index>(profile.rows());
162 const Index M = safe_cast<Index>(transforms.size());
163
164 // Cumulative sum of arc lengths for profile and sweep path.
165 std::vector<Scalar> profile_arc_lengths(N);
166 profile_arc_lengths[0] = 0;
167 for (Index i = 1; i < N; i++) {
168 profile_arc_lengths[i] =
169 profile_arc_lengths[i - 1] + (profile.row(i) - profile.row(i - 1)).norm();
170 }
171
172 std::vector<Scalar> sweep_path_arc_lengths(M);
173 sweep_path_arc_lengths[0] = 0;
174 {
175 Eigen::Matrix<Scalar, 1, 3> c = profile.colwise().mean();
176 for (Index i = 1; i < M; i++) {
177 sweep_path_arc_lengths[i] =
178 sweep_path_arc_lengths[i - 1] +
179 (transforms[i] * c.transpose() - transforms[i - 1] * c.transpose()).norm();
180 }
181 }
182
183 const Index num_quads = (N - 1) * (M - 1);
184 std::vector<bool> breaks;
185 const Index num_strips = compute_profile_breaks(
186 profile,
187 profile_arc_lengths,
188 profile_turning_angles,
189 max_strip_len,
190 breaks);
191 const Index L = N + num_strips - 1;
192 const Index num_uvs = L * M + num_quads;
193 UVArray uvs(num_uvs, 2);
194 UVIndices uv_indices(mesh.get_num_facets(), 3);
195
196 for (Index i = 0; i < M; i++) {
197 Index strip_index = 0;
198 for (Index j = 0; j < N; j++) {
199 uvs.row(i * L + j + strip_index) << profile_arc_lengths[j], sweep_path_arc_lengths[i];
200
201 if (i != 0 && j != 0) {
202 Index id = (i - 1) * (N - 1) + j - 1;
203 Index v0 = (i - 1) * L + (j - 1) + strip_index;
204 Index v1 = (i - 1) * L + (j - 1) + strip_index + 1;
205 Index v2 = i * L + (j - 1) + strip_index;
206 Index v3 = i * L + (j - 1) + strip_index + 1;
207 Index c = L * M + id;
208
209 uvs.row(c) = (uvs.row(v0) + uvs.row(v1) + uvs.row(v2) + uvs.row(v3)) / 4;
210 uv_indices.row(id * 4) << c, v0, v1;
211 uv_indices.row(id * 4 + 1) << c, v1, v3;
212 uv_indices.row(id * 4 + 2) << c, v3, v2;
213 uv_indices.row(id * 4 + 3) << c, v2, v0;
214 }
215
216 if (breaks[j]) {
217 // End a strip
218 strip_index++;
219 uvs.row(i * L + j + strip_index) << profile_arc_lengths[j],
220 sweep_path_arc_lengths[i];
221 }
222 }
223 }
224 assert(uv_indices.maxCoeff() < safe_cast<Index>(uvs.rows()));
225 mesh.initialize_uv(uvs, uv_indices);
226}
227
245template <typename MeshType>
246void generate_normal(
247 MeshType& mesh,
248 IndexOf<MeshType> N,
249 ScalarOf<MeshType> angle_threshold,
250 const std::vector<ScalarOf<MeshType>>& profile_turning_angles)
251{
252 using Scalar = ScalarOf<MeshType>;
253 using Index = IndexOf<MeshType>;
254
255 // Profile corners (with turning angle > pi/4) will introduce a normal
256 // discontinuity in the swept surface.
257 if (!mesh.has_facet_attribute("normal")) {
258 compute_triangle_normal(mesh);
259 }
260 const auto& facet_normals = mesh.get_facet_attribute("normal");
261 const Scalar cos_threshold = std::cos(angle_threshold);
262
263 compute_normal(mesh, [&](Index f0, Index f1) -> bool {
264 Index quad0 = f0 / 4;
265 Index quad1 = f1 / 4;
266
267 Index row0 = quad0 / (N - 1);
268 Index row1 = quad1 / (N - 1);
269 Index col0 = quad0 % (N - 1);
270 Index col1 = quad1 % (N - 1);
271 if (row0 != row1 || quad0 == quad1) {
272 // Angle between n0 and n1 must be less than lagrange::internal::pi/4.
273 return facet_normals.row(f0).dot(facet_normals.row(f1)) > cos_threshold;
274 } else {
275 if (col0 + 1 == col1 || (col0 == N - 2 && col1 == 0)) {
276 return profile_turning_angles[col1] <= angle_threshold;
277 } else if (col1 + 1 == col0 || (col1 == N - 2 && col0 == 0)) {
278 return profile_turning_angles[col0] <= angle_threshold;
279 } else {
280 logger().debug("f0: {} quad0: {} row0: {} col0: {}", f0, quad0, row0, col0);
281 logger().debug("f1: {} quad1: {} row1: {} col1: {}", f1, quad1, row1, col1);
282 throw Error(fmt::format("Facet {} and {} are not adjacent!", f0, f1));
283 }
284 }
285 });
286}
287
288template <typename VertexArray>
289VertexArray compute_offset_directions(const VertexArray& profile)
290{
291 using Scalar = typename VertexArray::Scalar;
292 using Index = Eigen::Index;
293
294 bool closed = is_path_closed(profile);
295 const auto N = profile.rows();
296 assert(N >= 2);
297 VertexArray dirs(N, 3);
298
299 for (auto i : range(N)) {
300 Index v_curr = i;
301 Index v_next = closed ? (i + 1) % (N - 1) : std::min<Index>(i + 1, N - 1);
302 Index v_prev = closed ? (i + N - 2) % (N - 1) : std::max<Index>(1, i) - 1;
303 Eigen::Matrix<Scalar, 3, 1> n0;
304 Eigen::Matrix<Scalar, 3, 1> n1;
305 n0 = profile.row(v_curr) - profile.row(v_prev);
306 n1 = profile.row(v_next) - profile.row(v_curr);
307 std::swap(n0[0], n0[1]);
308 std::swap(n1[0], n1[1]);
309 n0[1] *= -1;
310 n1[1] *= -1;
311
312 if (i == 0 && !closed) {
313 dirs.row(i) = n1.normalized();
314 } else if (i == N - 1 && !closed) {
315 dirs.row(i) = n0.normalized();
316 } else {
317 n0.normalize();
318 n1.normalize();
319 Scalar l = 1 / std::sqrt(1 + n0.dot(n1) / 2 + (Scalar)1e-6);
320 // Sharp angle causes numerical instability.
321 // Thus, we limit the dihedral angle to be at minimum 10 degrees.
322 // This translates to l can be at most 11.4737132467.
323 l = std::max((Scalar)11.4737132467, l);
324 dirs.row(i) = (n0 + n1).normalized() * l;
325 }
326 }
327
328 if (!dirs.array().isFinite().all()) {
329 // Profile contains degenerate edges.
330 lagrange::logger().warn("Sweep profile contains degenerate edges.");
331 }
332 return dirs;
333}
334
335} // namespace internal
336
364template <typename MeshType>
365std::unique_ptr<MeshType> generate_swept_surface(
366 const VertexArrayOf<MeshType>& profile,
367 const std::vector<Eigen::Transform<ScalarOf<MeshType>, 3, Eigen::AffineCompact>>& transforms,
368 const std::vector<ScalarOf<MeshType>>& offsets,
369 ScalarOf<MeshType> max_strip_len = 0,
370 bool path_closed = false)
371{
372 using Scalar = typename MeshType::Scalar;
373 using Index = typename MeshType::Index;
374 using VertexArray = typename MeshType::VertexArray;
375 using FacetArray = typename MeshType::FacetArray;
376 using AttributeArray = typename MeshType::AttributeArray;
377 using IndexArray = typename MeshType::IndexArray;
378
379 const Index N = safe_cast<Index>(profile.rows());
380 const Index M = safe_cast<Index>(transforms.size());
381 logger().debug("N: {} M: {}", N, M);
382 la_runtime_assert(N > 1, "Invalid sweep profile!");
383 la_runtime_assert(M > 1, "Invalid sweep path transforms!");
384 const bool profile_closed = internal::is_path_closed(profile);
385 const Index n = profile_closed ? (N - 1) : N;
386 const Index m = path_closed ? (M - 1) : M;
387
388 const Index num_quads = (N - 1) * (M - 1);
389 const Index num_vertices = n * m + num_quads;
390 const Index num_facets = 4 * num_quads;
391 VertexArray vertices(num_vertices, 3);
392 FacetArray facets(num_facets, 3);
393
394 // Process offsets.
395 std::function<VertexArray(Index)> offset_profile;
396 VertexArray offset_dirs;
397 if (!offsets.empty()) {
399 static_cast<Index>(offsets.size()) == M,
400 "Transforms and offsets must be sampled consistently");
401 offset_dirs = internal::compute_offset_directions(profile);
402 offset_profile = [&](Index i) -> VertexArray {
403 const Scalar offset = offsets[i];
404 return profile.topRows(n) + offset_dirs.topRows(n) * offset;
405 };
406 } else {
407 offset_profile = [&](Index) -> VertexArray { return profile.topRows(n); };
408 }
409
410 // Compute vertex positions on transformed profiles.
411 // Vertices on facet centers will be initialized later when generating facets.
412 for (Index i = 0; i < m; i++) {
413 const auto& T = transforms[i];
414 vertices.block(i * n, 0, n, 3).transpose() = T * offset_profile(i).transpose();
415 }
416
417 // Compute triangle connectivity.
418 for (Index i = 0; i < M - 1; i++) {
419 for (Index j = 0; j < N - 1; j++) {
420 const Index id = i * (N - 1) + j;
421 const Index v0 = i * n + j;
422 const Index v1 = i * n + (j + 1) % n;
423 const Index v2 = ((i + 1) % m) * n + j;
424 const Index v3 = ((i + 1) % m) * n + (j + 1) % n;
425 const Index c = n * m + id;
426
427 // Initialize vertex representing quad center.
428 vertices.row(c) =
429 (vertices.row(v0) + vertices.row(v1) + vertices.row(v2) + vertices.row(v3)) / 4;
430
431 facets.row(id * 4) << c, v0, v1;
432 facets.row(id * 4 + 1) << c, v1, v3;
433 facets.row(id * 4 + 2) << c, v3, v2;
434 facets.row(id * 4 + 3) << c, v2, v0;
435 }
436 }
437
438 // Initialize arc lengths for profile and extrusion path.
439 std::vector<Scalar> profile_arc_lengths(N, 0), path_arc_lengths(M, 0);
440 for (auto i : range(N - 1)) {
441 profile_arc_lengths[i + 1] = (profile.row(i + 1) - profile.row(i)).norm();
442 }
443 std::partial_sum(
444 profile_arc_lengths.begin(),
445 profile_arc_lengths.end(),
446 profile_arc_lengths.begin());
447 if (profile_arc_lengths.back() > 0) {
448 for (auto& l : profile_arc_lengths) {
449 l /= profile_arc_lengths.back();
450 }
451 }
452 path_arc_lengths[0] = 0;
453 {
454 Eigen::Matrix<Scalar, 1, 3> c = profile.colwise().mean();
455 for (Index i = 1; i < M; i++) {
456 path_arc_lengths[i] =
457 path_arc_lengths[i - 1] +
458 (transforms[i] * c.transpose() - transforms[i - 1] * c.transpose()).norm();
459 }
460 }
461 if (path_arc_lengths.back() > 0) {
462 for (auto& l : path_arc_lengths) {
463 l /= path_arc_lengths.back();
464 }
465 }
466
467 // Compute latitudes and longitudes
468 AttributeArray latitude(N * M + num_quads, 1); // along transformed profiles.
469 AttributeArray longitude(N * M + num_quads, 1); // along sweep path.
470 IndexArray feature_indices(num_facets, 3);
471
472 for (Index i = 0; i < M; i++) {
473 for (Index j = 0; j < N; j++) {
474 latitude(i * N + j) = path_arc_lengths[i];
475 longitude(i * N + j) = profile_arc_lengths[j];
476
477 if (i < M - 1 && j < N - 1) {
478 const Index offset = i * (N - 1) + j;
479 latitude(M * N + offset) = (path_arc_lengths[i + 1] + path_arc_lengths[i]) / 2;
480 longitude(M * N + offset) =
481 (profile_arc_lengths[j + 1] + profile_arc_lengths[j]) / 2;
482
483 const Index v0 = i * N + j;
484 const Index v1 = i * N + j + 1;
485 const Index v2 = (i + 1) * N + j;
486 const Index v3 = (i + 1) * N + j + 1;
487 const Index c = M * N + offset;
488
489 feature_indices.row(offset * 4) << c, v0, v1;
490 feature_indices.row(offset * 4 + 1) << c, v1, v3;
491 feature_indices.row(offset * 4 + 2) << c, v3, v2;
492 feature_indices.row(offset * 4 + 3) << c, v2, v0;
493 }
494 }
495 }
496
497 auto mesh = create_mesh(std::move(vertices), std::move(facets));
498
499 std::vector<Scalar> profile_turning_angles = internal::compute_turning_angles(profile);
500 internal::generate_uv(*mesh, profile, transforms, max_strip_len, profile_turning_angles);
501 internal::generate_normal(*mesh, N, lagrange::internal::pi / 4, profile_turning_angles);
502
503 mesh->add_indexed_attribute("latitude");
504 mesh->set_indexed_attribute("latitude", latitude, feature_indices);
505 mesh->add_indexed_attribute("longitude");
506 mesh->set_indexed_attribute("longitude", longitude, feature_indices);
507
508 // Set uniform semantic label
509 lagrange::set_uniform_semantic_label(*mesh, PrimitiveSemanticLabel::SIDE);
510
511 return mesh;
512}
513
514
533template <typename MeshType>
534std::unique_ptr<MeshType> generate_swept_surface(
535 const VertexArrayOf<MeshType>& profile,
536 const VertexArrayOf<MeshType>& sweep_path,
537 ScalarOf<MeshType> max_strip_len = 0)
538{
539 typename MeshType::VertexType profile_center =
540 (profile.colwise().minCoeff() + profile.colwise().maxCoeff()) / 2;
541
543 path.set_pivot(profile_center);
544 path.initialize();
545 const auto& transforms = path.get_transforms();
546 return generate_swept_surface<MeshType>(
547 profile,
548 transforms,
549 {},
550 max_strip_len,
551 path.is_closed());
552}
553
569template <typename MeshType>
570std::unique_ptr<MeshType> generate_swept_surface(
571 const VertexArrayOf<MeshType>& profile,
572 const SweepPath<ScalarOf<MeshType>>& sweep_path,
573 ScalarOf<MeshType> max_strip_len = 0)
574{
576 sweep_path.get_num_samples() >= 2,
577 "Please make sure sweep_path obj is sufficiently sampled.");
578 return generate_swept_surface<MeshType>(
579 profile,
580 sweep_path.get_transforms(),
581 sweep_path.get_offsets(),
582 max_strip_len,
583 sweep_path.is_closed());
584}
585
586template <typename VertexArray>
587std::vector<VertexArray> generate_swept_surface_latitude(
588 const VertexArray& profile,
589 const std::vector<Eigen::Transform<ScalarOf<VertexArray>, 3, Eigen::AffineCompact>>& transforms,
590 const std::vector<ScalarOf<VertexArray>>& offsets = {})
591{
592 using Scalar = ScalarOf<VertexArray>;
593 std::function<VertexArray(size_t)> offset_profile;
594 VertexArray offset_dirs;
595 if (!offsets.empty()) {
597 offsets.size() == transforms.size(),
598 "Transforms and offsets must be sampled consistently");
599 offset_dirs = internal::compute_offset_directions(profile);
600 offset_profile = [&](size_t i) -> VertexArray {
601 const Scalar offset = offsets[i];
602 return profile + offset_dirs * offset;
603 };
604 } else {
605 offset_profile = [&](size_t) -> VertexArray { return profile; };
606 }
607
608 const size_t num_transforms = transforms.size();
609 std::vector<VertexArray> latitudes(num_transforms);
610
611 tbb::parallel_for(size_t(0), num_transforms, [&](size_t i) {
612 const auto& T = transforms[i];
613 latitudes[i] = (T * offset_profile(i).transpose()).transpose();
614 });
615 return latitudes;
616}
617
618template <typename VertexArray>
619std::vector<VertexArray> generate_swept_surface_latitude(
620 const VertexArray& profile,
621 SweepPath<ScalarOf<VertexArray>>& sweep_path)
622{
623 return generate_swept_surface_latitude(
624 profile,
625 sweep_path.get_transforms(),
626 sweep_path.get_offsets());
627}
628
629template <typename VertexArray>
630std::vector<VertexArray> generate_swept_surface_longitude(
631 const VertexArray& profile,
632 const std::vector<Eigen::Transform<ScalarOf<VertexArray>, 3, Eigen::AffineCompact>>& transforms,
633 const std::vector<ScalarOf<VertexArray>>& offsets = {})
634{
635 using Scalar = ScalarOf<VertexArray>;
636 using Index = size_t;
637 using VertexType = Eigen::Matrix<Scalar, 1, 3>;
638
639 std::function<VertexType(Index, Index)> offset_profile;
640 VertexArray offset_dirs;
641 if (!offsets.empty()) {
643 offsets.size() == transforms.size(),
644 "Transforms and offsets must be sampled consistently");
645 offset_dirs = internal::compute_offset_directions(profile);
646 offset_profile = [&](Index i, Index j) -> VertexType {
647 const Scalar offset = offsets[i];
648 return profile.row(j) + offset_dirs.row(j) * offset;
649 };
650 } else {
651 offset_profile = [&](Index, Index j) -> VertexType { return profile.row(j); };
652 }
653
654 Index num_transforms = transforms.size();
655 Index profile_size = static_cast<Index>(profile.rows());
656 la_runtime_assert(profile_size >= 2, "Invalid profile!");
657
658 if ((profile.row(0) - profile.row(profile_size - 1)).norm() == 0) {
659 // Profile is closed.
660 profile_size -= 1;
661 }
662
663 std::vector<VertexArray> longitudes(profile_size);
664 for (auto i : range(profile_size)) {
665 longitudes[i].resize(num_transforms, 3);
666 for (auto j : range(num_transforms)) {
667 longitudes[i].row(j) = (transforms[j] * offset_profile(j, i).transpose()).transpose();
668 }
669 }
670
671 return longitudes;
672}
673
674template <typename VertexArray>
675std::vector<VertexArray> generate_swept_surface_longitude(
676 const VertexArray& profile,
677 SweepPath<ScalarOf<VertexArray>>& sweep_path)
678{
679 return generate_swept_surface_longitude(
680 profile,
681 sweep_path.get_transforms(),
682 sweep_path.get_offsets());
683}
684
685
686} // namespace legacy
687} // namespace primitive
688} // namespace lagrange
Index get_num_facets() const
Retrieves the number of facets.
Definition SurfaceMesh.h:687
Create sweep path based on a polyline.
Definition SweepPath.h:531
Abstract base class for sweep path.
Definition SweepPath.h:70
LA_CORE_API spdlog::logger & logger()
Retrieves the current logger.
Definition Logger.cpp:40
@ Scalar
Mesh attribute must have exactly 1 channel.
Definition AttributeFwd.h:56
AttributeId compute_normal(SurfaceMesh< Scalar, Index > &mesh, function_ref< bool(Index)> is_edge_smooth, span< const Index > cone_vertices={}, NormalOptions options={})
Compute smooth normals based on specified sharp edges and cone vertices.
Definition compute_normal.cpp:198
#define la_runtime_assert(...)
Runtime assertion check.
Definition assert.h:174
internal::Range< Index > range(Index end)
Returns an iterable object representing the range [0, end).
Definition range.h:176
constexpr auto safe_cast(SourceType value) -> std::enable_if_t<!std::is_same< SourceType, TargetType >::value, TargetType >
Perform safe cast from SourceType to TargetType, where "safe" means:
Definition safe_cast.h:50
Main namespace for Lagrange.
auto create_mesh(const Eigen::MatrixBase< DerivedV > &vertices, const Eigen::MatrixBase< DerivedF > &facets)
This function create a new mesh given the vertex and facet arrays by copying data into the Mesh objec...
Definition create_mesh.h:39
@ Error
Throw an error if collision is detected.
Definition MappingPolicy.h:24