10#include <CGAL/draw_triangulation_3.h>
11#include <CGAL/Graphics_scene.h>
12#include <CGAL/Graphics_scene_options.h>
13#include <CGAL/IO/Color.h>
14#include <CGAL/Qt/Basic_viewer.h>
15#include <CGAL/Qt/camera.h>
16#include <CGAL/Qt/init_ogl_context.h>
17#include <CGAL/Qt/qglviewer.h>
18#include <CGAL/version.h>
19#include <fmt/ostream.h>
20#include <fmt/printf.h>
25#include <boost/program_options.hpp>
36#include <QApplication>
38#include <QCryptographicHash>
41#include <QImageReader>
43#include <QJsonDocument>
45#include <QJsonParseError>
53#include <system_error>
62namespace po = boost::program_options;
66 using Delaunay = cdt::detail::TriangulationTraits<3>::Delaunay;
67 using Triangulation = Delaunay::Tr_Base;
69 CGAL::Graphics_scene_options<Triangulation, Triangulation::Vertex_handle,
70 Triangulation::Finite_edges_iterator,
71 Triangulation::Finite_facets_iterator>;
73 constexpr std::string_view USAGE =
74 R
"(Causal Dynamical Triangulations in C++ using CGAL.
76Copyright (c) 2022 Adam Getchell
78Load a versioned triangulation fixture and render it with the CGAL Qt viewer.
79Without --output, the viewer remains interactive. With --output it saves the
80manifest-declared frame and exits without waiting for user input.
83 cdt-viewer --manifest MANIFEST [--fixture FIXTURE] [--output IMAGE]
87 struct Topology_counts
89 std::size_t vertices{};
92 std::size_t simplices{};
99 std::string projection;
100 std::array<double, 3> position{};
101 std::array<double, 3> target{};
102 std::array<double, 3> up{};
103 double vertical_field_of_view_radians{};
110 bool transparent_background{};
111 double oversampling{};
112 bool expand_frustum{};
114 bool draw_vertices{};
117 std::string vertex_scope;
118 std::string edge_scope;
121 int edge_color_difference_threshold{};
122 int point_color_match_tolerance{};
126 std::vector<CGAL::IO::Color> face_palette;
127 Camera_config camera;
128 std::size_t minimum_foreground_pixels{};
131 [[nodiscard]]
auto draws_scene_edges(Render_config
const& render)
noexcept
133 {
return render.draw_edges && render.edge_scope ==
"all"; }
135 [[nodiscard]]
auto draws_screen_space_edges(
136 Render_config
const& render)
noexcept ->
bool
138 return render.draw_edges &&
139 render.edge_scope ==
"screen_space_face_boundaries";
142 struct Viewer_manifest
144 std::filesystem::path fixture;
145 std::string fixture_sha256;
146 Topology_counts expected;
147 std::string cgal_version;
148 std::string qt_version;
149 Render_config render;
152 class Artifact_viewer final :
public CGAL::Qt::Basic_viewer
155 using CGAL::Qt::Basic_viewer::Basic_viewer;
157 void after_initialization(std::function<
void()> callback)
158 { callback_ = std::move(callback); }
163 CGAL::Qt::Basic_viewer::init();
164 glDisable(GL_DITHER);
165 glDisable(GL_LINE_SMOOTH);
166 glDisable(GL_MULTISAMPLE);
167 if (callback_ && !callback_scheduled_)
169 callback_scheduled_ =
true;
170 QTimer::singleShot(0,
this, [
this]() { callback_(); });
175 std::function<void()> callback_;
176 bool callback_scheduled_{};
179 [[nodiscard]]
auto require_object(QJsonObject
const& parent,
180 QString
const& key) -> QJsonObject
182 auto const value = parent.value(key);
183 if (!value.isObject())
185 throw std::invalid_argument(fmt::format(
186 "Render manifest field '{}' must be an object.", key.toStdString()));
188 return value.toObject();
191 [[nodiscard]]
auto require_array(QJsonObject
const& parent,
192 QString
const& key) -> QJsonArray
194 auto const value = parent.value(key);
195 if (!value.isArray())
197 throw std::invalid_argument(fmt::format(
198 "Render manifest field '{}' must be an array.", key.toStdString()));
200 return value.toArray();
203 [[nodiscard]]
auto require_string(QJsonObject
const& parent,
204 QString
const& key) -> std::string
206 auto const value = parent.value(key);
207 if (!value.isString() || value.toString().isEmpty())
209 throw std::invalid_argument(
210 fmt::format(
"Render manifest field '{}' must be a nonempty string.",
213 return value.toString().toStdString();
216 [[nodiscard]]
auto require_bool(QJsonObject
const& parent, QString
const& key)
219 auto const value = parent.value(key);
222 throw std::invalid_argument(fmt::format(
223 "Render manifest field '{}' must be Boolean.", key.toStdString()));
225 return value.toBool();
228 [[nodiscard]]
auto require_number(QJsonObject
const& parent,
229 QString
const& key) ->
double
231 auto const value = parent.value(key);
232 if (!value.isDouble() || !std::isfinite(value.toDouble()))
234 throw std::invalid_argument(
235 fmt::format(
"Render manifest field '{}' must be a finite number.",
238 return value.toDouble();
241 constexpr long double MAX_EXACT_JSON_INTEGER{9'007'199'254'740'991.0L};
243 template <std::
integral Integer>
244 [[nodiscard]]
auto require_integer(QJsonObject
const& parent,
245 QString
const& key) -> Integer
247 auto const number = require_number(parent, key);
248 auto const widened =
static_cast<long double>(number);
251 if (std::trunc(number) != number ||
253 static_cast<long double>(std::numeric_limits<Integer>::min()) ||
255 static_cast<long double>(std::numeric_limits<Integer>::max()) ||
256 widened < -MAX_EXACT_JSON_INTEGER || widened > MAX_EXACT_JSON_INTEGER)
258 throw std::invalid_argument(
259 fmt::format(
"Render manifest field '{}' must fit the requested "
263 return static_cast<Integer
>(number);
266 [[nodiscard]]
auto require_vector(QJsonObject
const& parent,
267 QString
const& key) -> std::array<double, 3>
269 auto const values = require_array(parent, key);
270 if (values.size() != 3)
272 throw std::invalid_argument(
273 fmt::format(
"Render manifest field '{}' must have three entries.",
276 std::array<double, 3> result{};
277 for (qsizetype index = 0; index < values.size(); ++index)
279 auto const value = values.at(index);
280 if (!value.isDouble() || !std::isfinite(value.toDouble()))
282 throw std::invalid_argument(
283 fmt::format(
"Render manifest field '{}' contains a non-number.",
286 result.at(
static_cast<std::size_t
>(index)) = value.toDouble();
291 [[nodiscard]]
auto require_color(QJsonObject
const& parent,
292 QString
const& key) -> QColor
294 auto const values = require_array(parent, key);
295 if (values.size() != 4)
297 throw std::invalid_argument(fmt::format(
298 "Render manifest field '{}' must be RGBA.", key.toStdString()));
300 std::array<int, 4> channels{};
301 for (qsizetype index = 0; index < values.size(); ++index)
303 auto const value = values.at(index);
304 if (!value.isDouble() ||
305 std::trunc(value.toDouble()) != value.toDouble() ||
306 value.toDouble() < 0.0 || value.toDouble() > 255.0)
308 throw std::invalid_argument(
309 fmt::format(
"Render manifest color '{}' must use integer channels "
310 "from 0 through 255.",
313 channels.at(
static_cast<std::size_t
>(index)) =
314 static_cast<int>(value.toDouble());
316 return {channels[0], channels[1], channels[2], channels[3]};
319 [[nodiscard]]
auto to_cgal_color(QColor
const& color) -> CGAL::IO::Color
321 return {
static_cast<unsigned char>(color.red()),
322 static_cast<unsigned char>(color.green()),
323 static_cast<unsigned char>(color.blue()),
324 static_cast<unsigned char>(color.alpha())};
327 [[nodiscard]]
auto parse_manifest(std::filesystem::path
const& path)
330 QFile file(QString::fromStdString(path.string()));
331 if (!file.open(QIODevice::ReadOnly))
333 throw std::filesystem::filesystem_error(
334 "Could not open render manifest", path,
335 std::make_error_code(std::errc::no_such_file_or_directory));
338 QJsonParseError error;
339 auto const document = QJsonDocument::fromJson(file.readAll(), &error);
340 if (error.error != QJsonParseError::NoError || !document.isObject())
342 throw std::invalid_argument(
343 fmt::format(
"Could not parse render manifest {}: {}", path.string(),
344 error.errorString().toStdString()));
346 auto const root = document.object();
347 if (require_integer<int>(root,
"schema_version") != 1)
349 throw std::invalid_argument(
"Unsupported render manifest schema.");
352 auto const fixture = require_object(root,
"fixture");
353 auto const topology = require_object(fixture,
"expected_topology");
354 auto const renderer = require_object(root,
"renderer");
355 auto const render = require_object(root,
"render");
356 auto const geometry = require_object(render,
"geometry");
357 auto const style = require_object(render,
"style");
358 auto const camera = require_object(render,
"camera");
359 auto const checks = require_object(root,
"comparison");
361 auto fixture_path = path.parent_path() / require_string(fixture,
"path");
362 fixture_path = std::filesystem::weakly_canonical(fixture_path);
364 std::vector<CGAL::IO::Color> palette;
365 for (
auto const entry : require_array(style,
"face_palette"))
367 if (!entry.isArray())
369 throw std::invalid_argument(
370 "Every face palette entry must be an RGB array.");
372 auto const channels = entry.toArray();
373 if (channels.size() != 3)
375 throw std::invalid_argument(
376 "Every face palette entry must have three channels.");
378 std::array<unsigned char, 3> color{};
379 for (qsizetype index = 0; index < channels.size(); ++index)
381 auto const channel = channels.at(index);
382 if (!channel.isDouble() ||
383 std::trunc(channel.toDouble()) != channel.toDouble() ||
384 channel.toDouble() < 0.0 || channel.toDouble() > 255.0)
386 throw std::invalid_argument(
387 "Face palette channels must be integers from 0 through 255.");
389 color.at(
static_cast<std::size_t
>(index)) =
390 static_cast<unsigned char>(channel.toDouble());
392 palette.emplace_back(color[0], color[1], color[2]);
396 throw std::invalid_argument(
"The face palette must not be empty.");
399 auto const width = require_integer<int>(render,
"width");
400 auto const height = require_integer<int>(render,
"height");
401 auto const point_size = require_number(style,
"point_size");
402 auto const line_width = require_number(style,
"line_width");
403 auto const oversampling = require_number(render,
"oversampling");
404 constexpr double MAX_OVERSAMPLING{8.0};
405 if (width <= 0 || height <= 0 || point_size <= 0.0 || line_width <= 0.0 ||
406 point_size >
static_cast<double>(std::numeric_limits<float>::max()) ||
407 line_width >
static_cast<double>(std::numeric_limits<float>::max()))
409 throw std::invalid_argument(
410 "Render dimensions and point/line sizes must be positive and "
413 if (oversampling < 1.0 || oversampling > MAX_OVERSAMPLING)
415 throw std::invalid_argument(
416 "Render oversampling must be between 1 and 8.");
419 constexpr auto MAX_SNAPSHOT_PIXELS = std::size_t{268'435'456};
420 auto const sampled_width =
421 std::ceil(
static_cast<long double>(width) * oversampling);
422 auto const sampled_height =
423 std::ceil(
static_cast<long double>(height) * oversampling);
425 static_cast<long double>(std::numeric_limits<int>::max()) ||
427 static_cast<long double>(std::numeric_limits<int>::max()) ||
428 sampled_width * sampled_height >
429 static_cast<long double>(MAX_SNAPSHOT_PIXELS))
431 throw std::invalid_argument(
432 "Render dimensions and oversampling request an impractical "
436 auto const minimum_foreground_pixels =
437 require_integer<std::size_t>(checks,
"minimum_foreground_pixels");
438 auto const width_size =
static_cast<std::size_t
>(width);
439 auto const height_size =
static_cast<std::size_t
>(height);
440 if (width_size > std::numeric_limits<std::size_t>::max() / height_size ||
441 minimum_foreground_pixels > width_size * height_size)
443 throw std::invalid_argument(
444 "Minimum foreground pixels cannot exceed render width times "
447 if (require_string(render,
"output_format") !=
"png")
449 throw std::invalid_argument(
"The v1 viewer output format must be PNG.");
452 Viewer_manifest result{
453 .fixture = std::move(fixture_path),
454 .fixture_sha256 = require_string(fixture,
"sha256"),
455 .expected = {.vertices =
456 require_integer<std::size_t>(topology,
"vertices"),
457 .edges = require_integer<std::size_t>(topology,
"edges"),
458 .faces = require_integer<std::size_t>(topology,
"faces"),
460 require_integer<std::size_t>(topology,
"simplices"),
461 .minimum_timeslice = require_integer<cdt::Int_precision>(
462 topology,
"minimum_timeslice"),
463 .maximum_timeslice = require_integer<cdt::Int_precision>(
464 topology,
"maximum_timeslice")},
465 .cgal_version = require_string(renderer,
"cgal_version"),
466 .qt_version = require_string(renderer,
"qt_version"),
470 .transparent_background =
471 require_bool(render,
"transparent_background"),
472 .oversampling = oversampling,
473 .expand_frustum = require_bool(render,
"expand_frustum"),
474 .background = require_color(style,
"background_rgba"),
475 .draw_vertices = require_bool(geometry,
"vertices"),
476 .draw_edges = require_bool(geometry,
"edges"),
477 .draw_faces = require_bool(geometry,
"faces"),
478 .vertex_scope = require_string(geometry,
"vertex_scope"),
479 .edge_scope = require_string(geometry,
"edge_scope"),
480 .point_size =
static_cast<float>(point_size),
481 .line_width =
static_cast<float>(line_width),
482 .edge_color_difference_threshold =
483 require_integer<int>(style,
"edge_color_difference_threshold"),
484 .point_color_match_tolerance =
485 require_integer<int>(style,
"point_color_match_tolerance"),
486 .flat_shading = require_bool(style,
"flat_shading"),
487 .point_color = require_color(style,
"point_rgba"),
488 .edge_color = require_color(style,
"edge_rgba"),
489 .face_palette = std::move(palette),
490 .camera = {.projection = require_string(camera,
"projection"),
491 .position = require_vector(camera,
"position"),
492 .target = require_vector(camera,
"target"),
493 .up = require_vector(camera,
"up"),
494 .vertical_field_of_view_radians = require_number(
495 camera,
"vertical_field_of_view_radians")},
496 .minimum_foreground_pixels = minimum_foreground_pixels}
499 if (result.render.camera.projection !=
"perspective" &&
500 result.render.camera.projection !=
"orthographic")
502 throw std::invalid_argument(
503 "Camera projection must be 'perspective' or 'orthographic'.");
505 if (result.render.edge_scope !=
"all" &&
506 result.render.edge_scope !=
"screen_space_face_boundaries")
508 throw std::invalid_argument(
509 "Geometry edge scope must be 'all' or "
510 "'screen_space_face_boundaries'.");
512 if (result.render.vertex_scope !=
"all" &&
513 result.render.vertex_scope !=
"convex_hull")
515 throw std::invalid_argument(
516 "Geometry vertex scope must be 'all' or 'convex_hull'.");
518 if (result.render.edge_color_difference_threshold < 0 ||
519 result.render.edge_color_difference_threshold > 765)
521 throw std::invalid_argument(
522 "Edge color-difference threshold must be from 0 through 765.");
524 if (result.render.point_color_match_tolerance < 0 ||
525 result.render.point_color_match_tolerance > 765)
527 throw std::invalid_argument(
528 "Point color-match tolerance must be from 0 through 765.");
530 if (result.render.camera.vertical_field_of_view_radians <= 0.0 ||
531 result.render.camera.vertical_field_of_view_radians >= std::numbers::pi)
533 throw std::invalid_argument(
534 "Camera field of view must be between zero and pi radians.");
539 [[nodiscard]]
auto sha256(std::filesystem::path
const& path) -> std::string
541 QFile file(QString::fromStdString(path.string()));
542 if (!file.open(QIODevice::ReadOnly))
544 throw std::filesystem::filesystem_error(
545 "Could not open fixture for hashing", path,
546 std::make_error_code(std::errc::no_such_file_or_directory));
548 QCryptographicHash digest(QCryptographicHash::Sha256);
549 if (!digest.addData(&file))
551 throw std::runtime_error(
"Could not hash the viewer fixture.");
553 return digest.result().toHex().toStdString();
556 [[nodiscard]]
auto topology_counts(Delaunay
const& triangulation)
559 if (!triangulation.is_valid() || triangulation.dimension() != 3)
561 throw std::invalid_argument(
562 "The viewer fixture is not a valid three-dimensional "
565 if (triangulation.number_of_vertices() == 0)
567 throw std::invalid_argument(
"The viewer fixture is empty.");
570 auto minimum = std::numeric_limits<cdt::Int_precision>::max();
571 auto maximum = std::numeric_limits<cdt::Int_precision>::min();
572 for (
auto const vertex : triangulation.finite_vertex_handles())
574 minimum = std::min(minimum, vertex->info());
575 maximum = std::max(maximum, vertex->info());
577 return {.vertices = triangulation.number_of_vertices(),
578 .edges = triangulation.number_of_finite_edges(),
579 .faces = triangulation.number_of_finite_facets(),
580 .simplices = triangulation.number_of_finite_cells(),
581 .minimum_timeslice = minimum,
582 .maximum_timeslice = maximum};
585 void validate_fixture(Delaunay
const& triangulation,
586 Viewer_manifest
const& manifest)
588 auto const actual = topology_counts(triangulation);
589 if (actual.vertices != manifest.expected.vertices ||
590 actual.edges != manifest.expected.edges ||
591 actual.faces != manifest.expected.faces ||
592 actual.simplices != manifest.expected.simplices ||
593 actual.minimum_timeslice != manifest.expected.minimum_timeslice ||
594 actual.maximum_timeslice != manifest.expected.maximum_timeslice)
596 throw std::invalid_argument(fmt::format(
597 "Viewer fixture topology does not match the manifest: expected "
598 "V/E/F/T={}/{}/{}/{} and timeslices {}..{}; got "
599 "V/E/F/T={}/{}/{}/{} and timeslices {}..{}.",
600 manifest.expected.vertices, manifest.expected.edges,
601 manifest.expected.faces, manifest.expected.simplices,
602 manifest.expected.minimum_timeslice,
603 manifest.expected.maximum_timeslice, actual.vertices, actual.edges,
604 actual.faces, actual.simplices, actual.minimum_timeslice,
605 actual.maximum_timeslice));
609 constexpr auto FNV_OFFSET = std::uint64_t{14695981039346656037ULL};
610 constexpr auto FNV_PRIME = std::uint64_t{1099511628211ULL};
612 [[nodiscard]]
constexpr auto mix_hash(std::uint64_t hash)
noexcept
616 hash *= 0xbf58476d1ce4e5b9ULL;
618 hash *= 0x94d049bb133111ebULL;
619 return hash ^ (hash >> 31U);
622 void append_hash(std::uint64_t& hash, std::uint64_t value)
noexcept
624 for (
unsigned int byte = 0;
byte < 8; ++byte)
626 hash ^= (value >> (
byte * 8U)) & 0xffU;
631 [[nodiscard]]
auto vertex_key(Triangulation::Vertex_handle
const vertex)
632 -> std::array<std::uint64_t, 4>
634 auto const point = vertex->point();
635 return {std::bit_cast<std::uint64_t>(CGAL::to_double(point.x())),
636 std::bit_cast<std::uint64_t>(CGAL::to_double(point.y())),
637 std::bit_cast<std::uint64_t>(CGAL::to_double(point.z())),
638 static_cast<std::uint64_t
>(vertex->info())};
641 [[nodiscard]]
auto facet_vertices(
642 Triangulation::Finite_facets_iterator
const facet)
643 -> std::array<Triangulation::Vertex_handle, 3>
645 auto const cell = facet->first;
646 auto const index = facet->second;
647 std::array<Triangulation::Vertex_handle, 3> vertices{
648 cell->vertex((index + 1) % 4), cell->vertex((index + 2) % 4),
649 cell->vertex((index + 3) % 4)};
650 std::ranges::sort(vertices, {}, vertex_key);
654 [[nodiscard]]
auto facet_hash(
655 std::array<Triangulation::Vertex_handle, 3>
const& vertices)
658 auto hash = FNV_OFFSET;
659 for (
auto const vertex : vertices)
661 for (
auto const value : vertex_key(vertex)) { append_hash(hash, value); }
663 return mix_hash(hash);
666 [[nodiscard]]
auto convex_hull_vertices(Triangulation
const& triangulation)
667 -> std::set<std::array<std::uint64_t, 4>>
669 std::set<std::array<std::uint64_t, 4>> result;
670 for (
auto facet = triangulation.finite_facets_begin();
671 facet != triangulation.finite_facets_end(); ++facet)
673 auto const cell = facet->first;
674 auto const opposite = facet->second;
675 if (!triangulation.is_infinite(cell) &&
676 !triangulation.is_infinite(cell->neighbor(opposite)))
680 for (
auto const vertex : facet_vertices(facet))
682 result.emplace(vertex_key(vertex));
689 std::array<Triangulation::Vertex_handle, 3>& vertices)
noexcept
693 auto const point = [](Triangulation::Vertex_handle
const vertex) {
694 auto const& value = vertex->point();
695 return std::array{CGAL::to_double(value.x()), CGAL::to_double(value.y()),
696 CGAL::to_double(value.z())};
698 auto const a = point(vertices[0]);
699 auto const b = point(vertices[1]);
700 auto const c = point(vertices[2]);
701 auto const ab = std::array{b[0] - a[0], b[1] - a[1], b[2] - a[2]};
702 auto const ac = std::array{c[0] - a[0], c[1] - a[1], c[2] - a[2]};
704 std::array{ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2],
705 ab[0] * ac[1] - ab[1] * ac[0]};
706 auto const centroid =
707 std::array{a[0] + b[0] + c[0], a[1] + b[1] + c[1], a[2] + b[2] + c[2]};
708 auto const radial_alignment = normal[0] * centroid[0] +
709 normal[1] * centroid[1] +
710 normal[2] * centroid[2];
711 if (radial_alignment < 0.0) { std::swap(vertices[1], vertices[2]); }
714 [[nodiscard]]
auto make_scene(Delaunay
const& delaunay,
715 Render_config
const& render)
716 -> CGAL::Graphics_scene
718 auto const& triangulation =
static_cast<Triangulation const&
>(delaunay);
719 auto const hull_vertices = render.vertex_scope ==
"convex_hull"
720 ? convex_hull_vertices(triangulation)
721 : std::set<std::array<std::uint64_t, 4>>{};
722 Scene_options options;
723 options.ignore_all_vertices(!render.draw_vertices);
724 options.ignore_all_edges(!draws_scene_edges(render));
725 options.ignore_all_faces(
true);
726 options.colored_vertex = [](Triangulation
const&,
727 Triangulation::Vertex_handle) {
return true; };
728 options.draw_vertex = [&hull_vertices, &render](
729 Triangulation
const&,
730 Triangulation::Vertex_handle vertex) {
731 return render.vertex_scope ==
"all" ||
732 hull_vertices.contains(vertex_key(vertex));
734 options.vertex_color = [&render](Triangulation
const&,
735 Triangulation::Vertex_handle) {
736 return to_cgal_color(render.point_color);
738 options.colored_edge = [](Triangulation
const&,
739 Triangulation::Finite_edges_iterator) {
742 options.edge_color = [&render](Triangulation
const&,
743 Triangulation::Finite_edges_iterator) {
744 return to_cgal_color(render.edge_color);
746 CGAL::Graphics_scene scene;
747 CGAL::add_to_graphics_scene(triangulation, scene, options);
748 if (render.draw_faces)
750 for (
auto facet = triangulation.finite_facets_begin();
751 facet != triangulation.finite_facets_end(); ++facet)
753 auto vertices = facet_vertices(facet);
754 auto const index =
static_cast<std::size_t
>(facet_hash(vertices) %
755 render.face_palette.size());
756 orient_outward(vertices);
757 scene.face_begin(render.face_palette.at(index));
758 for (
auto const vertex : vertices)
760 scene.add_point_in_face(vertex->point());
768 [[nodiscard]]
auto vec(std::array<double, 3>
const& values)
769 -> CGAL::qglviewer::Vec
770 {
return {values[0], values[1], values[2]}; }
772 void configure_viewer(CGAL::Qt::Basic_viewer& viewer,
773 Render_config
const& render)
775 viewer.resize(render.width, render.height);
776 viewer.draw_vertices(render.draw_vertices);
777 viewer.draw_edges(draws_scene_edges(render));
778 viewer.draw_faces(render.draw_faces);
779 viewer.size_vertices(render.point_size);
780 viewer.size_edges(render.line_width);
781 viewer.flat_shading(render.flat_shading);
782 viewer.setBackgroundColor(render.background);
784 auto*
const camera = viewer.camera();
785 camera->setType(render.camera.projection ==
"perspective"
786 ? CGAL::qglviewer::Camera::PERSPECTIVE
787 : CGAL::qglviewer::Camera::ORTHOGRAPHIC);
788 camera->setFieldOfView(render.camera.vertical_field_of_view_radians);
789 camera->setPosition(vec(render.camera.position));
790 camera->setUpVector(vec(render.camera.up));
791 camera->lookAt(vec(render.camera.target));
795 [[nodiscard]]
auto color_distance(QRgb first, QRgb second)
noexcept ->
int
797 return std::abs(qRed(first) - qRed(second)) +
798 std::abs(qGreen(first) - qGreen(second)) +
799 std::abs(qBlue(first) - qBlue(second));
802 constexpr int RGBA_CHANNELS{4};
804 [[nodiscard]]
auto rgba_pixel(uchar
const* row,
int x)
noexcept -> QRgb
806 auto const offset = x * RGBA_CHANNELS;
807 return qRgba(row[offset], row[offset + 1], row[offset + 2],
811 void set_rgba_pixel(uchar* row,
int x, QRgb color)
noexcept
813 auto const offset = x * RGBA_CHANNELS;
814 row[offset] =
static_cast<uchar
>(qRed(color));
815 row[offset + 1] =
static_cast<uchar
>(qGreen(color));
816 row[offset + 2] =
static_cast<uchar
>(qBlue(color));
817 row[offset + 3] =
static_cast<uchar
>(qAlpha(color));
820 void outline_face_boundaries(std::filesystem::path
const& path,
821 Render_config
const& render)
823 if (!draws_screen_space_edges(render)) {
return; }
825 QImage source(QString::fromStdString(path.string()));
828 throw std::runtime_error(
829 "Could not reopen the rendered image for edge outlining.");
831 source = source.convertToFormat(QImage::Format_RGBA8888);
832 auto outlined = source;
833 auto const edge_color = render.edge_color.rgba();
835 for (
int y = 0; y < source.height(); ++y)
837 auto const* source_row = source.constScanLine(y);
838 auto const* lower_source_row =
839 y + 1 < source.height() ? source.constScanLine(y + 1) :
nullptr;
840 auto* outlined_row = outlined.scanLine(y);
841 auto* lower_outlined_row =
842 y + 1 < outlined.height() ? outlined.scanLine(y + 1) :
nullptr;
843 for (
int x = 0; x < source.width(); ++x)
845 auto const color = rgba_pixel(source_row, x);
846 if (qAlpha(color) == 0) {
continue; }
847 auto const boundary_with = [&](QRgb neighbor) {
848 return qAlpha(neighbor) != 0 &&
849 color_distance(color, neighbor) >
850 render.edge_color_difference_threshold;
852 auto const right_boundary =
853 x + 1 < source.width() &&
854 boundary_with(rgba_pixel(source_row, x + 1));
855 auto const lower_boundary =
856 lower_source_row !=
nullptr &&
857 boundary_with(rgba_pixel(lower_source_row, x));
858 if (right_boundary || lower_boundary)
860 set_rgba_pixel(outlined_row, x, edge_color);
861 if (render.line_width > 1.0F)
865 set_rgba_pixel(outlined_row, x + 1, edge_color);
867 if (lower_boundary && lower_outlined_row !=
nullptr)
869 set_rgba_pixel(lower_outlined_row, x, edge_color);
876 auto const point_color = render.point_color.rgba();
877 auto const is_point = [&render, point_color](QRgb color) {
878 return qAlpha(color) != 0 && color_distance(color, point_color) <=
879 render.point_color_match_tolerance;
881 for (
int y = 0; y < source.height(); ++y)
883 auto const* source_row = source.constScanLine(y);
884 auto const* upper_source_row =
885 y > 0 ? source.constScanLine(y - 1) :
nullptr;
886 auto const* lower_source_row =
887 y + 1 < source.height() ? source.constScanLine(y + 1) :
nullptr;
888 auto* outlined_row = outlined.scanLine(y);
889 for (
int x = 0; x < source.width(); ++x)
891 if (!is_point(rgba_pixel(source_row, x))) {
continue; }
892 auto const has_point_interior =
893 x > 0 && x + 1 < source.width() && upper_source_row !=
nullptr &&
894 lower_source_row !=
nullptr &&
895 is_point(rgba_pixel(source_row, x - 1)) &&
896 is_point(rgba_pixel(source_row, x + 1)) &&
897 is_point(rgba_pixel(upper_source_row, x)) &&
898 is_point(rgba_pixel(lower_source_row, x));
899 if (!has_point_interior)
901 set_rgba_pixel(outlined_row, x, edge_color);
906 if (!outlined.save(QString::fromStdString(path.string()),
"PNG"))
908 throw std::runtime_error(
"Could not save the outlined render.");
912 [[nodiscard]]
auto foreground_pixels(QImage image,
913 Render_config
const& render)
916 image = image.convertToFormat(QImage::Format_RGBA8888);
917 auto const background = render.background.rgba();
919 for (
int y = 0; y < image.height(); ++y)
921 auto const* row = image.constScanLine(y);
922 for (
int x = 0; x < image.width(); ++x)
924 auto const color = rgba_pixel(row, x);
925 if (render.transparent_background ? qAlpha(color) != 0
926 : color != background)
935 void validate_image(std::filesystem::path
const& path,
936 Render_config
const& render)
938 QImageReader reader(QString::fromStdString(path.string()));
939 if (!reader.canRead())
941 throw std::runtime_error(fmt::format(
942 "Renderer did not produce a readable image at {}.", path.string()));
944 auto const dimensions = reader.size();
945 if (dimensions.width() != render.width ||
946 dimensions.height() != render.height)
948 throw std::runtime_error(
949 fmt::format(
"Rendered image has dimensions {}x{}, expected {}x{}.",
950 dimensions.width(), dimensions.height(), render.width,
953 auto image = reader.read();
956 throw std::runtime_error(
"Renderer produced an unreadable image.");
958 auto const foreground = foreground_pixels(std::move(image), render);
959 if (foreground < render.minimum_foreground_pixels)
961 throw std::runtime_error(fmt::format(
962 "Rendered image contains {} foreground pixels; expected at least "
964 foreground, render.minimum_foreground_pixels));
968 [[nodiscard]]
auto run_viewer(CGAL::Graphics_scene
const& scene,
969 Render_config
const& render,
970 std::filesystem::path
const& output) ->
int
972 CGAL::Qt::init_ogl_context(4, 3);
974 char application_name[] =
"cdt-viewer";
975 char* qt_argv[] = {application_name,
nullptr};
976 QApplication application(qt_argc, qt_argv);
977 std::string render_error;
978 QTimer headless_watchdog;
979 Artifact_viewer viewer(
nullptr, scene,
"CDT++ archival viewer",
980 render.draw_vertices, draws_scene_edges(render),
985 viewer.setAttribute(::Qt::WA_DontShowOnScreen,
true);
986 viewer.after_initialization(
987 [&application, &viewer, &render, &output, &render_error]() {
990 configure_viewer(viewer, render);
991 if (output.has_parent_path())
993 std::filesystem::create_directories(output.parent_path());
995 viewer.saveSnapshot(QString::fromStdString(output.string()),
996 render.width, render.height,
997 render.expand_frustum, render.oversampling,
998 render.transparent_background
999 ? CGAL::qglviewer::TRANSPARENT_BACKGROUND
1000 : CGAL::qglviewer::CURRENT_BACKGROUND);
1001 outline_face_boundaries(output, render);
1002 validate_image(output, render);
1003 application.exit(EXIT_SUCCESS);
1005 catch (std::exception
const& error)
1007 render_error = error.what();
1008 application.exit(EXIT_FAILURE);
1012 render_error =
"Unknown failure while rendering the artifact.";
1013 application.exit(EXIT_FAILURE);
1016 constexpr int HEADLESS_RENDER_TIMEOUT_MS{60'000};
1017 headless_watchdog.setSingleShot(
true);
1018 QObject::connect(&headless_watchdog, &QTimer::timeout, &application,
1019 [&application, &render_error]() {
1021 "Headless renderer timed out before completion.";
1022 application.exit(EXIT_FAILURE);
1024 headless_watchdog.start(HEADLESS_RENDER_TIMEOUT_MS);
1028 viewer.after_initialization(
1029 [&viewer, &render]() { configure_viewer(viewer, render); });
1033 auto const status = application.exec();
1034 headless_watchdog.stop();
1035 if (!render_error.empty()) {
throw std::runtime_error(render_error); }
1040auto main(
int const argc,
char*
const argv[]) ->
int
1043 std::string manifest_path;
1044 std::string fixture_override;
1045 std::string output_path;
1047 po::options_description description(std::string{USAGE});
1048 description.add_options()(
"help,h",
"Show this message")(
1049 "version,v",
"Show program version")(
1050 "manifest,m", po::value<std::string>(&manifest_path)->required(),
1051 "Versioned JSON render manifest")(
1052 "fixture,f", po::value<std::string>(&fixture_override),
1053 "Override the manifest fixture path (digest and topology still apply)")(
1054 "output,o", po::value<std::string>(&output_path),
1055 "Render noninteractively to this image and exit");
1057 po::variables_map arguments;
1058 po::store(po::parse_command_line(argc, argv, description), arguments);
1059 if (arguments.count(
"help") != 0U)
1061 fmt::print(
"{}\n", fmt::streamed(description));
1062 return EXIT_SUCCESS;
1064 if (arguments.count(
"version") != 0U)
1066 fmt::print(
"cdt-viewer version {} (CGAL {}, Qt {})\n", cdt::VERSION,
1067 CGAL_VERSION_STR, qVersion());
1068 return EXIT_SUCCESS;
1070 po::notify(arguments);
1072 auto manifest = parse_manifest(manifest_path);
1073 if (!fixture_override.empty())
1075 manifest.fixture = std::filesystem::weakly_canonical(fixture_override);
1077 if (sha256(manifest.fixture) != manifest.fixture_sha256)
1079 throw std::invalid_argument(
1080 "Viewer fixture SHA-256 does not match the render manifest.");
1082 if (manifest.cgal_version != CGAL_VERSION_STR ||
1083 manifest.qt_version != qVersion())
1085 throw std::runtime_error(fmt::format(
1086 "Renderer version mismatch: manifest requires CGAL {} and Qt {}; "
1087 "this binary uses CGAL {} and Qt {}.",
1088 manifest.cgal_version, manifest.qt_version, CGAL_VERSION_STR,
1092 auto const triangulation =
1094 validate_fixture(triangulation, manifest);
1095 auto const scene = make_scene(triangulation, manifest.render);
1097 fmt::print(
"Validated viewer fixture {} with V/E/F/T={}/{}/{}/{}.\n",
1098 manifest.fixture.string(), manifest.expected.vertices,
1099 manifest.expected.edges, manifest.expected.faces,
1100 manifest.expected.simplices);
1101 auto const output = output_path.empty() ? std::filesystem::path{}
1102 : std::filesystem::path{output_path};
1103 auto const status = run_viewer(scene, manifest.render, output);
1104 if (!output.empty())
1106 fmt::print(
"Rendered {}x{} artifact to {}.\n", manifest.render.width,
1107 manifest.render.height, output.string());
1111catch (std::exception
const& error)
1113 fmt::print(stderr,
"cdt-viewer: {}\n", error.what());
1114 return EXIT_FAILURE;
1118 fmt::print(stderr,
"cdt-viewer: unknown failure\n");
1119 return EXIT_FAILURE;
Delaunay_t< 3 > Delaunay
Three-dimensional Delaunay triangulation used by move implementations.
Global integer and precision settings.
Traits class for particular uses of CGAL.
auto read_file(std::filesystem::path const &filename) -> TriangulationType
Read triangulation from file.
std::int32_t Int_precision