Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-06-24 08:00:23

0001 // This file is part of the actsvg package.
0002 //
0003 // Copyright (C) 2022 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
0008 
0009 #pragma once
0010 
0011 #include <algorithm>
0012 #include <string>
0013 #include <tuple>
0014 #include <utility>
0015 #include <vector>
0016 
0017 #include "actsvg/core.hpp"
0018 #include "actsvg/display/tools.hpp"
0019 #include "actsvg/proto/surface.hpp"
0020 #include "actsvg/styles/defaults.hpp"
0021 
0022 namespace actsvg {
0023 
0024 using namespace defaults;
0025 
0026 namespace display {
0027 
0028 struct surface_options {
0029 
0030     /// @param _boolean_shape draw the boolean
0031     bool _boolean_shape = true;
0032     /// @param _focus draw at focus
0033     bool _focus = false;
0034     /// @param _draw_at_scale draw at scale
0035     bool _draw_at_scale = false;
0036     /// @param _draw_as_template draw as template
0037     bool _draw_as_template = false;
0038     /// @param _label_measures draw the labels
0039     bool _label_measures = false;
0040 };
0041 
0042 /** Draw the surface with a dedicated view
0043  *
0044  * @param id_ the identification of this surface
0045  * @param s_ the surface type
0046  * @param o_ the options struct
0047  *
0048  * @note template surfaces ignore the view_type::scene range restriction
0049  */
0050 template <typename surface_type, typename view_type>
0051 svg::object surface(const std::string& id_, const surface_type& s_,
0052                     const view_type& v_,
0053                     const surface_options& o_ = surface_options{}) {
0054 
0055     svg::object s;
0056 
0057     // If the surface has a template and is it defined
0058     if (s_._template_object.is_defined()) {
0059 
0060         style::transform draw_transform = s_._transform;
0061         // No rotation nor shift as template
0062         if (o_._draw_as_template) {
0063             draw_transform._tr = {0., 0.};
0064             draw_transform._rot = {0., 0., 0.};
0065         }
0066         // Apply scale or not
0067         if (!o_._draw_at_scale) {
0068             draw_transform._scale = {1., 1.};
0069         }
0070 
0071         // Create a surface object from the template
0072         s = draw::from_template(id_, s_._template_object, s_._fill, s_._stroke,
0073                                 draw_transform);
0074         return s;
0075     }
0076 
0077     style::transform draw_transform =
0078         o_._focus ? style::transform{} : s_._transform;
0079     draw_transform._scale = s_._transform._scale;
0080 
0081     // Surface directly
0082     if (s_._type == surface_type::type::e_annulus) {
0083         // Special annulus bounds code
0084         scalar min_r = s_._measures[0];
0085         scalar max_r = s_._measures[1];
0086         scalar min_phi_rel = s_._measures[2];
0087         scalar max_phi_rel = s_._measures[3];
0088         // scalar average_phi = s_._measures[4];
0089         scalar origin_x = s_._measures[5];
0090         scalar origin_y = s_._measures[6];
0091 
0092         auto in_left_s_xy =
0093             annulusCircleIx(origin_x, origin_y, min_r, max_phi_rel);
0094         auto in_right_s_xy =
0095             annulusCircleIx(origin_x, origin_y, min_r, min_phi_rel);
0096         auto out_right_s_xy =
0097             annulusCircleIx(origin_x, origin_y, max_r, min_phi_rel);
0098         auto out_left_s_xy =
0099             annulusCircleIx(origin_x, origin_y, max_r, max_phi_rel);
0100 
0101         point2 translation = {0., 0.};
0102         if (s_._surface_transform.has_value()) {
0103             const auto& srot = s_._surface_transform.value()._rotation;
0104             scalar alpha = std::acos(srot[0][0]);
0105             if (srot[1][0] < 0) {
0106                 alpha = -alpha;
0107             }
0108             in_left_s_xy = utils::rotate(in_left_s_xy, alpha);
0109             in_right_s_xy = utils::rotate(in_right_s_xy, alpha);
0110             out_right_s_xy = utils::rotate(out_right_s_xy, alpha);
0111             out_left_s_xy = utils::rotate(out_left_s_xy, alpha);
0112 
0113             const auto& str = s_._surface_transform.value()._translation;
0114             translation = {static_cast<scalar>(str[0]),
0115                            static_cast<scalar>(str[1])};
0116             in_left_s_xy = utils::add<point2, 2u>(in_left_s_xy, translation);
0117             in_right_s_xy = utils::add<point2, 2u>(in_right_s_xy, translation);
0118             out_right_s_xy =
0119                 utils::add<point2, 2u>(out_right_s_xy, translation);
0120             out_left_s_xy = utils::add<point2, 2u>(out_left_s_xy, translation);
0121         }
0122 
0123         // For drawing, this needs a shift
0124         scalar irx = in_right_s_xy[0];
0125         scalar iry = in_right_s_xy[1];
0126         scalar ilx = in_left_s_xy[0];
0127         scalar ily = in_left_s_xy[1];
0128         scalar orx = out_right_s_xy[0];
0129         scalar ory = out_right_s_xy[1];
0130         scalar olx = out_left_s_xy[0];
0131         scalar only = out_left_s_xy[1];
0132         // Dedicated path drawing of the annulus bounds
0133         s._tag = "path";
0134         s._id = id_;
0135         std::string path =
0136             "M " + std::to_string(irx) + " " + std::to_string(iry);
0137         path += " A " + std::to_string(min_r) + " " + std::to_string(min_r);
0138         path += " 0 0 1 ";
0139         path += std::to_string(ilx) + " " + std::to_string(ily);
0140         path += " L " + std::to_string(olx) + " " + std::to_string(only);
0141         path += " A " + std::to_string(max_r) + " " + std::to_string(max_r);
0142         path += " 0 0 0 ";
0143         path += std::to_string(orx) + " " + std::to_string(ory);
0144         path += " L " + std::to_string(irx) + " " + std::to_string(iry);
0145         s._attribute_map["d"] = path;
0146         s._fill = s_._fill;
0147         s._stroke = s_._stroke;
0148         s._transform = draw_transform;
0149         if (!o_._focus) {
0150             s._x_range = {-max_r, max_r};
0151             s._y_range = {-max_r, max_r};
0152         }
0153 
0154         if (o_._label_measures) {
0155             // make a copy & a group out of the object
0156             auto sc = s;
0157             s = svg::object::create_group(id_ + "_labeled_group");
0158             s.add_object(sc);
0159 
0160             // Draw min / max circles
0161             s.add_object(draw::circle(id_ + "_inner_circle", {0., 0}, min_r,
0162                                       style::fill{},
0163                                       defaults::__bl_dotted_stroke));
0164             s.add_object(draw::circle(id_ + "_outer_circle", {0., 0}, max_r,
0165                                       style::fill{},
0166                                       defaults::__bl_dotted_stroke));
0167 
0168             // Define the origin of the strip system & draw the lines
0169             point2 origin = {-origin_x, -origin_y};
0170             auto el0 = point2{olx, only};
0171             auto el1 = point2{orx, ory};
0172             std::array<point2, 2u> lines = {el0, el1};
0173             for (const auto [il, lline] : utils::enumerate(lines)) {
0174                 point2 dline = utils::unit_direction<>(origin, lline);
0175                 point2 dneg = utils::scale<point2, 2u>(dline, -0.2 * min_r);
0176                 point2 dpos = utils::scale<point2, 2u>(dneg, -1.);
0177 
0178                 auto line = draw::line(id_ + "_line" + std::to_string(il),
0179                                        utils::add<point2, 2u>(origin, dneg),
0180                                        utils::add<point2, 2u>(lline, dpos),
0181                                        defaults::__bl_dotted_stroke);
0182                 line._transform = draw_transform;
0183                 s.add_object(line);
0184             }
0185         }
0186     } else if (s_._type == surface_type::type::e_disc) {
0187 
0188         // x-y view for discs
0189         if constexpr (std::is_same_v<view_type, views::x_y>) {
0190 
0191             // view activation / deactivation
0192             if (not utils::inside_range(s_._zparameters[0u],
0193                                         v_._scene._range[1u])) {
0194                 s._active = false;
0195                 return s;
0196             }
0197             // Sector or circle
0198             if (std::abs((s_._opening[1u] - s_._opening[0u]) - 2 * M_PI) >
0199                 5 * std::numeric_limits<scalar>::epsilon()) {
0200                 auto view_vertices = generators::sector_contour(
0201                     s_._radii[0u], s_._radii[1u], s_._opening[0u],
0202                     s_._opening[1u]);
0203                 s = draw::polygon(id_, view_vertices, s_._fill, s_._stroke,
0204                                   draw_transform);
0205             } else {
0206 
0207                 s = draw::circle(id_, {0., 0.}, s_._radii[1u], s_._fill,
0208                                  s_._stroke, draw_transform);
0209 
0210                 // A ring is present
0211                 if (s_._radii[0u] != 0.) {
0212 
0213                     std::string mask_id = id_ + "_mask";
0214 
0215                     auto s_c_ = s_;
0216                     s_c_._radii = {0., s_._radii[1u]};
0217 
0218                     svg::object outer_mask =
0219                         surface(id_ + "_mask_surface_outer", s_c_, v_, {false});
0220                     outer_mask._fill = style::fill{true};
0221                     outer_mask._stroke = style::stroke{true};
0222                     outer_mask._attribute_map["fill"] = "white";
0223 
0224                     s_c_._radii = {0., s_._radii[0u]};
0225                     svg::object inner_mask =
0226                         surface(id_ + "_mask_surface_inner", s_c_, v_, {false});
0227                     inner_mask._fill = style::fill{true};
0228                     inner_mask._stroke = style::stroke{true};
0229                     inner_mask._attribute_map["fill"] = "black";
0230 
0231                     // Create the mask object
0232                     svg::object mask;
0233                     mask._fill = style::fill{true};
0234                     mask._stroke = style::stroke{true};
0235                     mask._id = mask_id;
0236                     mask._tag = "mask";
0237                     mask.add_object(outer_mask);
0238                     mask.add_object(inner_mask);
0239 
0240                     // Modify the surface
0241                     s._definitions.push_back(mask);
0242                     s._attribute_map["mask"] = utils::id_to_url(mask_id);
0243                 }
0244             }
0245         }
0246         // r_z view for discs
0247         if constexpr (std::is_same_v<view_type, views::z_r>) {
0248             scalar zpos = s_._zparameters[0u];
0249             point2 start = {zpos, s_._radii[0u]};
0250             point2 end = {zpos, s_._radii[1u]};
0251             s = draw::line(id_, start, end, s_._stroke, draw_transform);
0252         }
0253 
0254     } else if (s_._type == surface_type::type::e_cylinder) {
0255 
0256         // xy - view
0257         if constexpr (std::is_same_v<view_type, views::x_y>) {
0258             // view activation / deactivation
0259             if (not utils::inside_range(s_._zparameters[0u],
0260                                         v_._scene._range[1u])) {
0261                 s._active = false;
0262                 return s;
0263             }
0264 
0265             if (std::abs((s_._opening[1u] - s_._opening[0u]) - 2 * M_PI) >
0266                 5 * std::numeric_limits<scalar>::epsilon()) {
0267                 scalar r = s_._radii[1u];
0268                 point2 start = {r * std::cos(s_._opening[0u]),
0269                                 r * s_._opening[0u]};
0270                 point2 end = {r * std::cos(s_._opening[1u]),
0271                               r * s_._opening[1u]};
0272                 s = draw::arc(id_, r, start, end, style::fill({}), s_._stroke,
0273                               draw_transform);
0274             } else {
0275                 s = draw::circle(id_, {0., 0.}, s_._radii[1u], __w_fill,
0276                                  s_._stroke, draw_transform);
0277             }
0278         }
0279 
0280         // z-r view
0281         if constexpr (std::is_same_v<view_type, views::z_r>) {
0282             scalar zpos = s_._zparameters[0u];
0283             scalar zhalf = s_._zparameters[1u];
0284             point2 start = {zpos - zhalf, s_._radii[1u]};
0285             point2 end = {zpos + zhalf, s_._radii[1u]};
0286             s = draw::line(id_, start, end, s_._stroke, draw_transform);
0287         }
0288 
0289     } else if (s_._type == surface_type::type::e_straw) {
0290         // xy view
0291         if constexpr (std::is_same_v<view_type, views::x_y>) {
0292             // Skin of the straw
0293             auto ss = draw::circle(id_, {0., 0.}, s_._radii[1u], s_._fill,
0294                                    s_._stroke, draw_transform);
0295             // Wire of the straw
0296             auto ws = draw::circle(id_, {0., 0.}, s_._radii[0u], __s_fill,
0297                                    s_._stroke, draw_transform);
0298             s._tag = "g";
0299             s.add_object(ss);
0300             s.add_object(ws);
0301         }
0302 
0303     } else if (not s_._vertices.empty()) {
0304 
0305         // View activiation, deactivation
0306         // if only one vertex is within the view range, the surface is shown
0307         bool view_active = true;
0308         if constexpr (std::is_same_v<view_type, views::x_y>) {
0309             view_active = false;
0310             for (auto v : s_._vertices) {
0311                 if (utils::inside_range(v[2], v_._scene._range[1u])) {
0312                     view_active = true;
0313                     break;
0314                 }
0315             }
0316         }
0317         if (not view_active) {
0318             s._active = view_active;
0319             return s;
0320         }
0321 
0322         // Check if the vertices have to be transformed into global at first
0323         // place
0324         std::vector<typename surface_type::point3_type> vertices = s_._vertices;
0325         if (s_._surface_transform.has_value()) {
0326             const auto& sftr = s_._surface_transform.value();
0327             std::for_each(vertices.begin(), vertices.end(),
0328                           [&sftr](auto& v) { v = sftr.point_to_global(v); });
0329         }
0330 
0331         auto view_vertices = v_.path(vertices);
0332 
0333         if constexpr (std::is_same_v<view_type, views::z_rphi>) {
0334             // Check if we have to split the surface in phi and
0335             // draw wiggle
0336             // - currently supported for rectangular surfaces only
0337             if (vertices.size() == 4u) {
0338                 scalar min_phi = std::numeric_limits<scalar>::max();
0339                 scalar max_phi = std::numeric_limits<scalar>::min();
0340                 // Pre-emptively split the vertices
0341                 std::vector<typename surface_type::point3_type> n_vertices;
0342                 std::vector<typename surface_type::point3_type> p_vertices;
0343                 // Calculate phi
0344                 for (const auto& v : s_._vertices) {
0345                     scalar phi = std::atan2(v[1u], v[0u]);
0346                     min_phi = std::min(min_phi, phi);
0347                     max_phi = std::max(max_phi, phi);
0348                     if (phi < 0.) {
0349                         n_vertices.push_back(v);
0350                     } else {
0351                         p_vertices.push_back(v);
0352                     }
0353                 }
0354                 // Detect phi boundary jump
0355                 if (max_phi - min_phi > 1.5 * M_PI) {
0356                     // Check first if you have only collected one
0357                     // negative vertex. It may happen if you have
0358                     // tilted surfaces. Handle with care and
0359                     // re-evaluate the intersections with
0360                     // the grid to show split surfaces correctly
0361                     if (n_vertices.size() < p_vertices.size()) {
0362                         std::vector<typename surface_type::point3_type>
0363                             bottom_vertices = {p_vertices[2u], p_vertices[0u]};
0364                         std::vector<typename surface_type::point3_type>
0365                             top_vertices = {n_vertices[0u], n_vertices[0u]};
0366 
0367                         for (size_t index = 0u; index < 2u; index++) {
0368                             auto p_bottom = bottom_vertices[index];
0369                             auto p_top = top_vertices[index];
0370                             float m = (p_bottom[1u] - p_top[1u]) /
0371                                       (p_bottom[2u] - p_top[2u]);
0372                             float q = p_bottom[1u] - m * p_bottom[2u];
0373                             float z = -q / m;
0374 
0375                             typename surface_type::point3_type add = {
0376                                 p_bottom[0u],
0377                                 std::numeric_limits<scalar>::epsilon(), z};
0378                             p_vertices.push_back(add);
0379                             add[1u] = -std::numeric_limits<scalar>::epsilon();
0380                             n_vertices.push_back(add);
0381                         }
0382 
0383                         auto p_view_vertices = v_.path(p_vertices);
0384                         auto p_middle = p_view_vertices.back();
0385                         p_middle[1u] = static_cast<scalar>(
0386                             1.5 * p_view_vertices.back()[1u] -
0387                             0.5 * p_view_vertices.front()[1u]);
0388                         p_middle[0u] = static_cast<scalar>(
0389                             0.5 * (p_view_vertices.at(p_view_vertices.size() -
0390                                                       2)[0u] +
0391                                    p_view_vertices.back()[0u]));
0392                         p_view_vertices.insert(p_view_vertices.end() - 1,
0393                                                p_middle);
0394 
0395                         auto s_n = draw::polygon(id_ + std::string("_n_split"),
0396                                                  v_.path(n_vertices), s_._fill,
0397                                                  s_._stroke, draw_transform);
0398 
0399                         auto s_p = draw::polygon(id_ + std::string("_p_split"),
0400                                                  p_view_vertices, s_._fill,
0401                                                  s_._stroke, draw_transform);
0402 
0403                         // The surface turns into a group of two
0404                         s._tag = "g";
0405 
0406                         s.add_object(s_n);
0407                         s.add_object(s_p);
0408                         return s;
0409                     }
0410 
0411                     // The surface turns into a group of two
0412                     s._tag = "g";
0413 
0414                     // Split part on negative side
0415                     auto n4 = n_vertices[0u];
0416                     auto n2 = n_vertices[1u];
0417 
0418                     n2[1u] = -std::numeric_limits<scalar>::epsilon();
0419                     n4[1u] = -std::numeric_limits<scalar>::epsilon();
0420 
0421                     // Create the wiggle in
0422                     typename surface_type::point3_type n3 = {
0423                         n2[0u],
0424                         static_cast<scalar>(0.5 *
0425                                             (n_vertices[1u][1u] + n2[1u])),
0426                         static_cast<scalar>(0.5 * (n2[2u] + n4[2u]))};
0427 
0428                     n_vertices.push_back(n2);
0429                     n_vertices.push_back(n3);
0430                     n_vertices.push_back(n4);
0431 
0432                     auto s_n = draw::polygon(id_ + std::string("_n_split"),
0433                                              v_.path(n_vertices), s_._fill,
0434                                              s_._stroke, draw_transform);
0435                     // Split art on positive side
0436                     auto p4 = p_vertices[0u];
0437                     auto p2 = p_vertices[1u];
0438                     p2[1u] = std::numeric_limits<scalar>::epsilon();
0439                     p4[1u] = std::numeric_limits<scalar>::epsilon();
0440                     typename surface_type::point3_type p3 = {
0441                         p2[0u], p2[1u],
0442                         static_cast<scalar>(0.5 * (p2[2u] + p4[2u]))};
0443                     p_vertices.push_back(p2);
0444                     p_vertices.push_back(p3);
0445                     p_vertices.push_back(p4);
0446                     // Create the wiggle out (on view to not fall into phi trap)
0447                     auto p_view_vertices = v_.path(p_vertices);
0448                     const auto& p_v_1 = p_view_vertices[1u];
0449                     const auto& p_v_2 = p_view_vertices[2u];
0450                     auto& p_v_3 = p_view_vertices[3u];
0451                     p_v_3[1u] =
0452                         static_cast<scalar>(1.5 * p_v_2[1u] - 0.5 * p_v_1[1u]);
0453 
0454                     auto s_p = draw::polygon(id_ + std::string("_p_split"),
0455                                              p_view_vertices, s_._fill,
0456                                              s_._stroke, draw_transform);
0457                     s.add_object(s_n);
0458                     s.add_object(s_p);
0459                     return s;
0460                 }
0461             }
0462         }
0463 
0464         s = draw::polygon(id_, view_vertices, s_._fill, s_._stroke,
0465                           draw_transform);
0466     }
0467 
0468     if (o_._boolean_shape) {
0469         /// Boolean surfaces only supported for x-y view so far
0470         if constexpr (std::is_same_v<view_type, views::x_y>) {
0471             if (s_._boolean_surface.size() == 1u and
0472                 s_._boolean_operation == surface_type::boolean::e_subtraction) {
0473                 std::string mask_id = id_ + "_mask";
0474                 // make a new boolean surface
0475                 svg::object outer_mask =
0476                     surface(id_ + "_mask_surface_outer", s_, v_, {false});
0477                 outer_mask._fill = style::fill{true};
0478                 outer_mask._stroke = style::stroke{true};
0479                 outer_mask._attribute_map["fill"] = "white";
0480 
0481                 svg::object inner_mask = surface(id_ + "_mask_surface_inner",
0482                                                  s_._boolean_surface[0], v_);
0483                 inner_mask._fill = style::fill{true};
0484                 inner_mask._stroke = style::stroke{true};
0485                 inner_mask._attribute_map["fill"] = "black";
0486 
0487                 // Create the mask object
0488                 svg::object mask;
0489                 mask._fill = style::fill{true};
0490                 mask._stroke = s_._stroke;
0491                 mask._id = mask_id;
0492                 mask._tag = "mask";
0493                 mask.add_object(outer_mask);
0494                 mask.add_object(inner_mask);
0495 
0496                 // Modify the surface
0497                 s._definitions.push_back(mask);
0498                 s._attribute_map["mask"] = utils::id_to_url(mask_id);
0499             }
0500         }
0501     }
0502 
0503     return s;
0504 }
0505 
0506 /** Draw a surface as an oriented polygon
0507  *
0508  * @param id_ the identification of this surface
0509  * @param s_ the surface type
0510  * @param v_ the view type
0511  *
0512  */
0513 template <typename surface_type, typename view_type>
0514 svg::object oriented_polygon(const std::string& id_, const surface_type& s_,
0515                              const view_type& v_) {
0516     svg::object s = svg::object::create_group(id_);
0517     s._fill._sterile = true;
0518     s._stroke._sterile = true;
0519 
0520     if (s_._vertices.empty()) {
0521         throw std::runtime_error("Surface is no polygon: no vertices defined!");
0522     }
0523 
0524     // View activation / deactivation
0525     if constexpr (std::is_same_v<view_type, views::x_y>) {
0526         // Get the path
0527         auto path = v_.path(s_._vertices);
0528         point2 center = {0., 0.};
0529         for (const auto& p : path) {
0530             center[0] += p[0];
0531             center[1] += p[1];
0532         }
0533         center[0] /= path.size();
0534         center[1] /= path.size();
0535 
0536         // Get the angle
0537         scalar angle = std::atan2(center[1], center[0]);
0538 
0539         // Re-center the path
0540         for_each(path.begin(), path.end(), [&center, &angle](point2& p) {
0541             p[0] -= center[0];
0542             p[1] -= center[1];
0543             // rotate
0544             utils::rotate(p, -angle);
0545         });
0546 
0547         style::transform s_transform{};
0548         s_transform._tr[0] = center[0];
0549         s_transform._tr[1] = center[1];
0550         s_transform._rot[0] = angle;
0551         auto polygon =
0552             draw::polygon(id_, path, s_._fill, s_._stroke, s_transform, false);
0553         return polygon;
0554     }
0555 
0556     return s;
0557 }
0558 
0559 /** Draw a portal link
0560  *
0561  * @param id_ the identification of this portal link
0562  * @param p_ the portal for understanding the span
0563  * @param link_ the link itself
0564  * @param v_ the view type
0565  *
0566  * @return a single object containing the portal view
0567  **/
0568 template <typename portal_type, typename view_type>
0569 svg::object portal_link(const std::string& id_,
0570                         [[maybe_unused]] const portal_type& p_,
0571                         const typename portal_type::link& link_,
0572                         const view_type& v_) {
0573     svg::object l = svg::object::create_group(id_);
0574 
0575     scalar d_z = static_cast<scalar>(link_._end[2u] - link_._start[2u]);
0576     // View activation / deactivation
0577     if constexpr (std::is_same_v<view_type, views::x_y>) {
0578         if (v_._scene._strict and d_z * v_._scene._view[1] < 0) {
0579             l._active = false;
0580             return l;
0581         }
0582     }
0583 
0584     scalar d_x = static_cast<scalar>(link_._end[0u] - link_._start[0u]);
0585     scalar d_y = static_cast<scalar>(link_._end[1u] - link_._start[1u]);
0586     scalar d_r = std::sqrt(d_x * d_x + d_y * d_y);
0587 
0588     if (std::is_same_v<view_type, views::x_y> and
0589         d_r <= std::numeric_limits<scalar>::epsilon()) {
0590         svg::object arr_xy = svg::object::create_group(id_ + "_arrow");
0591         arr_xy.add_object(draw::circle(id_ + "_arrow_top",
0592                                        {static_cast<scalar>(link_._start[0u]),
0593                                         static_cast<scalar>(link_._start[1u])},
0594                                        link_._end_marker._size,
0595                                        link_._end_marker._fill));
0596         // Camera view onto the surface
0597         if (d_z * v_._scene._view[1] < 0) {
0598             arr_xy.add_object(
0599                 draw::circle(id_ + "_arrow_top_tip",
0600                              {static_cast<scalar>(link_._start[0u]),
0601                               static_cast<scalar>(link_._start[1u])},
0602                              link_._end_marker._size * 0.1_scalar, __w_fill));
0603         } else {
0604             scalar d_l_x = link_._end_marker._size * 0.9_scalar *
0605                            std::cos(0.25_scalar * pi);
0606             scalar d_l_y = link_._end_marker._size * 0.9_scalar *
0607                            std::sin(0.25_scalar * pi);
0608             arr_xy.add_object(
0609                 draw::line(id_ + "_arrow_top_cl0",
0610                            {static_cast<scalar>(link_._start[0u] - d_l_x),
0611                             static_cast<scalar>(link_._start[1u] - d_l_y)},
0612                            {static_cast<scalar>(link_._start[0u] + d_l_x),
0613                             static_cast<scalar>(link_._start[1u] + d_l_y)},
0614                            __w_stroke));
0615             arr_xy.add_object(
0616                 draw::line(id_ + "_arrow_top_cl1",
0617                            {static_cast<scalar>(link_._start[0u] + d_l_x),
0618                             static_cast<scalar>(link_._start[1u] - d_l_y)},
0619                            {static_cast<scalar>(link_._start[0u] - d_l_x),
0620                             static_cast<scalar>(link_._start[1u] + d_l_y)},
0621                            __w_stroke));
0622         }
0623         l.add_object(arr_xy);
0624         // draw plot
0625     } else {
0626         typename portal_type::container_type start_end_3d = {link_._start,
0627                                                              link_._end};
0628         auto start_end = v_.path(start_end_3d);
0629 
0630         l.add_object(draw::arrow(id_ + "_arrow", start_end[0u], start_end[1u],
0631                                  link_._stroke, link_._start_marker,
0632                                  link_._end_marker));
0633     }
0634     return l;
0635 }
0636 
0637 /** Draw a portal with a dedicated view
0638  *
0639  * @param id_ the identification of this surface
0640  * @param s_ the surface type
0641  * @param v_ the view type
0642  *
0643  * @return a single object containing the portal view;
0644  **/
0645 template <typename portal_type, typename view_type>
0646 svg::object portal(const std::string& id_, const portal_type& p_,
0647                    const view_type& v_) {
0648     svg::object p = svg::object::create_group(id_);
0649     p._fill._sterile = true;
0650     p._stroke._sterile = true;
0651 
0652     p.add_object(surface(id_ + "_surface", p_._surface, v_));
0653     for (auto [il, vl] : utils::enumerate(p_._volume_links)) {
0654         p.add_object(portal_link(id_ + "_volume_link_" + std::to_string(il), p_,
0655                                  vl, v_));
0656     }
0657 
0658     return p;
0659 }
0660 
0661 /** Draw a volume
0662  *
0663  * @param id_ the identification of this portal link
0664  * @param dv_ the detector volume
0665  * @param v_ the view type
0666  * @param p_ draw the portals
0667  * @param s_ draw the surfaces
0668  *
0669  * @return a single object containing the volume view
0670  **/
0671 template <typename volume_type, typename view_type>
0672 svg::object volume(const std::string& id_, const volume_type& dv_,
0673                    const view_type& v_, bool p_ = true, bool s_ = true) {
0674     svg::object v = svg::object::create_group(id_);
0675     v._fill._sterile = true;
0676     v._stroke._sterile = true;
0677 
0678     // The volume shape - only rz view currently supported
0679     if (not dv_._vertices.empty() and std::is_same_v<view_type, views::z_r>) {
0680         auto view_vertices = v_.path(dv_._vertices);
0681         auto pv = draw::polygon(id_ + "_volume", view_vertices, dv_._fill,
0682                                 dv_._stroke, dv_._transform);
0683         v.add_object(pv);
0684     } else {
0685         if (dv_._type == volume_type::type::e_cylinder and
0686             dv_._bound_values.size() >= 6u) {
0687             scalar ri = dv_._bound_values[0u];
0688             scalar ro = dv_._bound_values[1u];
0689             scalar zp = dv_._bound_values[2u];
0690             scalar zh = dv_._bound_values[3u];
0691             scalar ps = dv_._bound_values[4u];
0692             scalar ap = dv_._bound_values[5u];
0693             if constexpr (std::is_same_v<view_type, views::x_y>) {
0694                 // Make a dummy surface and draw it
0695                 typename volume_type::surface_type s;
0696                 s._name = id_ + "_volume";
0697                 s._radii = {ri, ro};
0698                 s._opening = {ap - ps, ap + ps};
0699                 s._zparameters = {zp, zh};
0700                 s._fill = dv_._fill;
0701                 s._stroke = dv_._stroke;
0702                 s._type = decltype(s)::type::e_disc;
0703                 v.add_object(surface(s._name, s, v_));
0704             }
0705             if constexpr (std::is_same_v<view_type, views::z_r>) {
0706                 std::vector<point2> view_vertices = {
0707                     {zp - zh, ri}, {zp + zh, ri}, {zp + zh, ro}, {zp - zh, ro}};
0708                 auto pv = draw::polygon(id_ + "_volume", view_vertices,
0709                                         dv_._fill, dv_._stroke, dv_._transform);
0710                 v.add_object(pv);
0711             }
0712         }
0713     }
0714 
0715     // Draw the portals
0716     if (p_) {
0717         for (auto [ip, p] : utils::enumerate(dv_._portals)) {
0718             v.add_object(portal(id_ + "_portal_" + std::to_string(ip), p, v_));
0719         }
0720     }
0721     // Draw the surfaces
0722     if (s_) {
0723         for (auto [is, s] : utils::enumerate(dv_._v_surfaces)) {
0724             v.add_object(display::surface(
0725                 id_ + "_surface_" + std::to_string(is), s, v_));
0726         }
0727     }
0728     return v;
0729 }
0730 
0731 /** Draw a detector
0732  *
0733  * @param id_ the identification of this portal link
0734  * @param d_ the detector
0735  * @param v_ the view type
0736  *
0737  * @return a single object containing the volume view
0738  **/
0739 template <typename detector_type, typename view_type>
0740 svg::object detector(const std::string& id_, const detector_type& d_,
0741                      const view_type& v_) {
0742     svg::object d = svg::object::create_group(id_);
0743     d._fill._sterile = true;
0744     d._stroke._sterile = true;
0745 
0746     // Sort the volumes after their depth level, local copy first
0747     auto volumes = d_._volumes;
0748     std::sort(volumes.begin(), volumes.end(),
0749               [](const auto& a_, const auto& b_) {
0750                   return (a_._depth_level < b_._depth_level);
0751               });
0752 
0753     // Draw the volume areas first
0754     for (const auto& v : volumes) {
0755         d.add_object(volume(v._name, v, v_, false));
0756     }
0757 
0758     // Collect all the portals (in a named map) - to avoid double drawing of
0759     // shared ones
0760     std::map<std::string, typename detector_type::volume_type::portal_type>
0761         portals;
0762 
0763     for (const auto& v : volumes) {
0764         for (const auto& p : v._portals) {
0765             portals[p._name] = p;
0766         }
0767     }
0768     // Now draw the portals
0769     for (const auto& [n, p] : portals) {
0770         d.add_object(portal(n, p, v_));
0771     }
0772 
0773     return d;
0774 }
0775 
0776 /** Draw eta lines in a zr view
0777  *
0778  * @param id_ the identiier
0779  * @param zr_ the z range of the detector
0780  * @param rr_ the r range of the detector
0781  * @param els_ the stroked eta lines + boolean whether to label
0782  * @param tr_ a potential transform
0783  *
0784  * @return a single object containing the frame
0785  */
0786 svg::object eta_lines(
0787     const std::string& id_, scalar zr_, scalar rr_,
0788     const std::vector<std::tuple<std::vector<scalar>, style::stroke, bool,
0789                                  style::font>>& els_,
0790     const style::transform& tr_ = style::transform());
0791 }  // namespace display
0792 
0793 }  // namespace actsvg