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