1 #include "canvas3d_base.hpp"
2 #include "import_step/import.hpp"
3 #include "board/board_layers.hpp"
4 #include "board/board.hpp"
5 #include "canvas/gl_util.hpp"
6 #include "util/util.hpp"
7 #include "util/min_max_accumulator.hpp"
8 #include <glm/glm.hpp>
9 #include <glm/gtc/matrix_transform.hpp>
10 #include <glm/gtc/type_ptr.hpp>
11 #include <glm/gtx/rotate_vector.hpp>
12 #include <glibmm/miscutils.h>
13 #include "pool/pool_manager.hpp"
14 #include "pool/ipool.hpp"
15 #include "logger/log_util.hpp"
16 
17 namespace horizon {
18 
Canvas3DBase()19 Canvas3DBase::Canvas3DBase()
20     : center(0), cover_renderer(*this), wall_renderer(*this), face_renderer(*this), background_renderer(*this),
21       point_renderer(*this)
22 {
23 }
24 
get_layer_color(int layer) const25 Color Canvas3DBase::get_layer_color(int layer) const
26 {
27     if (layer == 20000 || BoardLayers::is_copper(layer)) { // pth or cu
28         if (use_layer_colors && appearance.layer_colors.count(layer)) {
29             return appearance.layer_colors.at(layer);
30         }
31         return {1, .8, 0};
32     }
33 
34     if (layer == BoardLayers::TOP_MASK || layer == BoardLayers::BOTTOM_MASK)
35         return solder_mask_color;
36 
37     if (layer == BoardLayers::TOP_PASTE || layer == BoardLayers::BOTTOM_PASTE)
38         return {.7, .7, .7};
39 
40     if (layer == BoardLayers::TOP_SILKSCREEN || layer == BoardLayers::BOTTOM_SILKSCREEN)
41         return silkscreen_color;
42 
43     if (layer == BoardLayers::L_OUTLINE || layer >= 10000)
44         return substrate_color;
45     return {1, 0, 0};
46 }
47 
get_layer_offset(int layer) const48 float Canvas3DBase::get_layer_offset(int layer) const
49 {
50     if (layer == 20000)
51         return get_layer_offset(BoardLayers::TOP_COPPER);
52 
53     else
54         return ca.get_layer(layer).offset + ca.get_layer(layer).explode_mul * explode;
55 }
56 
get_layer_thickness(int layer) const57 float Canvas3DBase::get_layer_thickness(int layer) const
58 {
59     if (layer == BoardLayers::L_OUTLINE && explode == 0) {
60         return ca.get_layer(BoardLayers::BOTTOM_COPPER).offset + ca.get_layer(BoardLayers::BOTTOM_COPPER).thickness;
61     }
62     else if (layer == 20000) {
63         return -(get_layer_offset(BoardLayers::TOP_COPPER) - get_layer_offset(BoardLayers::BOTTOM_COPPER));
64     }
65     else {
66         return ca.get_layer(layer).thickness;
67     }
68 }
69 
layer_is_visible(int layer) const70 bool Canvas3DBase::layer_is_visible(int layer) const
71 {
72     if (layer == 20000) // pth holes
73         return show_copper;
74 
75     if (layer == BoardLayers::TOP_MASK || layer == BoardLayers::BOTTOM_MASK)
76         return show_solder_mask;
77 
78     if (layer == BoardLayers::TOP_PASTE || layer == BoardLayers::BOTTOM_PASTE)
79         return show_solder_paste;
80 
81     if (layer == BoardLayers::TOP_SILKSCREEN || layer == BoardLayers::BOTTOM_SILKSCREEN)
82         return show_silkscreen;
83 
84     if (layer == BoardLayers::L_OUTLINE || layer >= 10000) {
85         if (show_substrate) {
86             if (layer == BoardLayers::L_OUTLINE)
87                 return true;
88             else {
89                 return explode > 0;
90             }
91         }
92         else {
93             return false;
94         }
95     }
96 
97     if (layer < BoardLayers::TOP_COPPER && layer > BoardLayers::BOTTOM_COPPER)
98         return (show_substrate == false || explode > 0) && show_copper;
99 
100     if (BoardLayers::is_copper(layer))
101         return show_copper;
102 
103     return true;
104 }
105 
a_realize()106 void Canvas3DBase::a_realize()
107 {
108     cover_renderer.realize();
109     wall_renderer.realize();
110     face_renderer.realize();
111     background_renderer.realize();
112     point_renderer.realize();
113     glEnable(GL_DEPTH_TEST);
114     glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA);
115 
116     GLint fb;
117     glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fb); // save fb
118 
119     glGenRenderbuffers(1, &renderbuffer);
120     glGenRenderbuffers(1, &depthrenderbuffer);
121     glGenRenderbuffers(1, &pickrenderbuffer);
122     glGenRenderbuffers(1, &pickrenderbuffer_downsampled);
123 
124     resize_buffers();
125 
126     GL_CHECK_ERROR
127 
128     glGenFramebuffers(1, &fbo_downsampled);
129     glBindFramebuffer(GL_FRAMEBUFFER, fbo_downsampled);
130     glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, pickrenderbuffer_downsampled);
131 
132     if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
133         // Gtk::MessageDialog md("Error setting up framebuffer, will now exit", false /* use_markup */,
134         // Gtk::MESSAGE_ERROR,
135         //                      Gtk::BUTTONS_OK);
136         // md.run();
137         abort();
138     }
139 
140     GL_CHECK_ERROR
141 
142     glGenFramebuffers(1, &fbo);
143     glBindFramebuffer(GL_FRAMEBUFFER, fbo);
144 
145     glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
146     glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_RENDERBUFFER, pickrenderbuffer);
147     glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthrenderbuffer);
148 
149     GL_CHECK_ERROR
150 
151     if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
152         // Gtk::MessageDialog md("Error setting up framebuffer, will now exit", false /* use_markup */,
153         // Gtk::MESSAGE_ERROR,
154         //                      Gtk::BUTTONS_OK);
155         // md.run();
156         abort();
157     }
158     glBindFramebuffer(GL_FRAMEBUFFER, fb);
159 
160     GL_CHECK_ERROR
161 }
162 
163 
resize_buffers()164 void Canvas3DBase::resize_buffers()
165 {
166     GLint rb;
167     GLint samples = gl_clamp_samples(num_samples);
168     glGetIntegerv(GL_RENDERBUFFER_BINDING, &rb); // save rb
169     glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
170     glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, width * a_get_scale_factor(),
171                                      height * a_get_scale_factor());
172     glBindRenderbuffer(GL_RENDERBUFFER, depthrenderbuffer);
173     glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_DEPTH_COMPONENT, width * a_get_scale_factor(),
174                                      height * a_get_scale_factor());
175     glBindRenderbuffer(GL_RENDERBUFFER, pickrenderbuffer);
176     glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_R16UI, width * a_get_scale_factor(),
177                                      height * a_get_scale_factor());
178 
179     glBindRenderbuffer(GL_RENDERBUFFER, pickrenderbuffer_downsampled);
180     glRenderbufferStorage(GL_RENDERBUFFER, GL_R16UI, width * a_get_scale_factor(), height * a_get_scale_factor());
181 
182     glBindRenderbuffer(GL_RENDERBUFFER, rb);
183 }
184 
push()185 void Canvas3DBase::push()
186 {
187     cover_renderer.push();
188     wall_renderer.push();
189     face_renderer.push();
190     point_renderer.push();
191 }
192 
get_magic_number() const193 float Canvas3DBase::get_magic_number() const
194 {
195     return tan(0.5 * glm::radians(cam_fov));
196 }
197 
fix_cam_elevation(float cam_elevation)198 static float fix_cam_elevation(float cam_elevation)
199 {
200     while (cam_elevation >= 360)
201         cam_elevation -= 360;
202     while (cam_elevation < 0)
203         cam_elevation += 360;
204     if (cam_elevation > 180)
205         cam_elevation -= 360;
206     return cam_elevation;
207 }
208 
set_cam_elevation(const float & ele)209 void Canvas3DBase::set_cam_elevation(const float &ele)
210 {
211     cam_elevation = fix_cam_elevation(ele);
212     redraw();
213     invalidate_pick();
214     s_signal_view_changed.emit();
215 }
216 
set_cam_azimuth(const float & az)217 void Canvas3DBase::set_cam_azimuth(const float &az)
218 {
219     cam_azimuth = az;
220     while (cam_azimuth < 0)
221         cam_azimuth += 360;
222 
223     while (cam_azimuth > 360)
224         cam_azimuth -= 360;
225     redraw();
226     invalidate_pick();
227     s_signal_view_changed.emit();
228 }
229 
render(RenderBackground mode)230 void Canvas3DBase::render(RenderBackground mode)
231 {
232 
233     GLint fb;
234     glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fb); // save fb
235 
236     glBindFramebuffer(GL_FRAMEBUFFER, fbo);
237 
238     glClearColor(0, 0, 0, 0);
239     glClearDepth(10);
240     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
241     GL_CHECK_ERROR
242     {
243         const std::array<GLenum, 2> bufs = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
244         glDrawBuffers(bufs.size(), bufs.data());
245     }
246 
247     if (mode == RenderBackground::YES) {
248         glDisable(GL_DEPTH_TEST);
249         background_renderer.render();
250         glEnable(GL_DEPTH_TEST);
251     }
252 
253     float r = cam_distance;
254     float phi = glm::radians(cam_azimuth);
255     float theta = glm::radians(90 - cam_elevation);
256     auto cam_offset = glm::vec3(r * sin(theta) * cos(phi), r * sin(theta) * sin(phi), r * cos(theta));
257     auto cam_pos = cam_offset + glm::vec3(center, 0);
258 
259     glm::vec3 right(sin(phi - 3.14f / 2.0f), cos(phi - 3.14f / 2.0f), 0);
260 
261     viewmat = glm::lookAt(cam_pos, glm::vec3(center, 0), glm::vec3(0, 0, std::abs(cam_elevation) < 90 ? 1 : -1));
262 
263     float cam_dist_min = std::max(std::abs(cam_pos.z) - (10 + explode * (brd->get_n_inner_layers() * 2 + 3)), 1.0f);
264     float cam_dist_max = 0;
265 
266     float zmin = -10 - explode * (brd->get_n_inner_layers() * 2 + 3 + package_height_max);
267     float zmax = 10 + explode * 2 + package_height_max;
268     std::array<glm::vec3, 8> bbs = {
269             glm::vec3(bbox.first.x, bbox.first.y, zmin),  glm::vec3(bbox.first.x, bbox.second.y, zmin),
270             glm::vec3(bbox.second.x, bbox.first.y, zmin), glm::vec3(bbox.second.x, bbox.second.y, zmin),
271             glm::vec3(bbox.first.x, bbox.first.y, zmax),  glm::vec3(bbox.first.x, bbox.second.y, zmax),
272             glm::vec3(bbox.second.x, bbox.first.y, zmax), glm::vec3(bbox.second.x, bbox.second.y, zmax)};
273 
274     for (const auto &bb : bbs) {
275         float dist = glm::length(bb - cam_pos);
276         cam_dist_max = std::max(dist, cam_dist_max);
277         cam_dist_min = std::min(dist, cam_dist_min);
278     }
279     float m = get_magic_number() / height * cam_distance;
280     float d = cam_dist_max * 2;
281     if (projection == Projection::PERSP) {
282         projmat = glm::perspective(glm::radians(cam_fov), (float)width / height, cam_dist_min / 2, cam_dist_max * 2);
283     }
284     else {
285         projmat = glm::ortho(-width * m, width * m, -height * m, height * m, -d, d);
286     }
287 
288     cam_normal = glm::normalize(cam_offset);
289     wall_renderer.render();
290 
291     if (show_models)
292         face_renderer.render();
293 
294     if (show_points)
295         point_renderer.render();
296 
297     cover_renderer.render();
298 
299     GL_CHECK_ERROR
300 
301     if (pick_state == PickState::QUEUED) {
302         glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo_downsampled);
303         glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
304         glDrawBuffer(GL_COLOR_ATTACHMENT0);
305         glReadBuffer(GL_COLOR_ATTACHMENT1);
306         glBlitFramebuffer(0, 0, width * a_get_scale_factor(), height * a_get_scale_factor(), 0, 0,
307                           width * a_get_scale_factor(), height * a_get_scale_factor(), GL_COLOR_BUFFER_BIT, GL_NEAREST);
308 
309         glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo_downsampled);
310         glReadBuffer(GL_COLOR_ATTACHMENT0);
311         pick_buf.resize(width * a_get_scale_factor() * height * a_get_scale_factor());
312         GL_CHECK_ERROR
313 
314         glPixelStorei(GL_PACK_ALIGNMENT, 2);
315         glReadPixels(0, 0, width * a_get_scale_factor(), height * a_get_scale_factor(), GL_RED_INTEGER,
316                      GL_UNSIGNED_SHORT, pick_buf.data());
317 
318         GL_CHECK_ERROR
319         pick_state = PickState::CURRENT;
320         s_signal_pick_ready.emit();
321     }
322 
323     glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fb);
324     glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
325     glDrawBuffer(fb ? GL_COLOR_ATTACHMENT0 : GL_FRONT);
326     glReadBuffer(GL_COLOR_ATTACHMENT0);
327     glBlitFramebuffer(0, 0, width * a_get_scale_factor(), height * a_get_scale_factor(), 0, 0,
328                       width * a_get_scale_factor(), height * a_get_scale_factor(), GL_COLOR_BUFFER_BIT, GL_NEAREST);
329 
330     glBindFramebuffer(GL_FRAMEBUFFER, fb);
331     GL_CHECK_ERROR
332     glFlush();
333 }
334 
view_all()335 void Canvas3DBase::view_all()
336 {
337     if (!brd)
338         return;
339 
340     const auto &vertices = ca.get_layer(BoardLayers::L_OUTLINE).walls;
341     MinMaxAccumulator<float> acc_x, acc_y;
342 
343     for (const auto &it : vertices) {
344         acc_x.accumulate(it.x);
345         acc_y.accumulate(it.y);
346     }
347 
348     float xmin = acc_x.get_min();
349     float xmax = acc_x.get_max();
350     float ymin = acc_y.get_min();
351     float ymax = acc_y.get_max();
352 
353     float board_width = (xmax - xmin) / 1e6;
354     float board_height = (ymax - ymin) / 1e6;
355 
356     if (board_height < 1 || board_width < 1)
357         return;
358 
359     set_center({(xmin + xmax) / 2e6, (ymin + ymax) / 2e6});
360 
361 
362     set_cam_distance(std::max(board_width / width, board_height / height) / (2 * get_magic_number() / height) * 1.1);
363     set_cam_azimuth(270);
364     set_cam_elevation(89.99);
365 }
366 
prepare()367 void Canvas3DBase::prepare()
368 {
369     bbox.first = glm::vec3();
370     bbox.second = glm::vec3();
371     for (const auto &it : ca.get_patches()) {
372         for (const auto &path : it.second) {
373             for (const auto &p : path) {
374                 glm::vec3 q(p.X / 1e6, p.Y / 1e6, 0);
375                 bbox.first = glm::min(bbox.first, q);
376                 bbox.second = glm::max(bbox.second, q);
377             }
378         }
379     }
380 }
381 
a_get_scale_factor() const382 int Canvas3DBase::a_get_scale_factor() const
383 {
384     return 1;
385 }
386 
prepare_packages()387 void Canvas3DBase::prepare_packages()
388 {
389     if (!brd)
390         return;
391     package_infos.clear();
392     package_transforms.clear();
393     std::map<std::pair<std::string, bool>, std::set<const BoardPackage *>> pkg_map;
394     for (const auto &it : brd->packages) {
395         auto model = it.second.package.get_model(it.second.model);
396         if (model)
397             pkg_map[{model->filename, it.second.component ? it.second.component->nopopulate : false}].insert(
398                     &it.second);
399     }
400 
401     unsigned int pick_base = 2; // 0: background, 1: PCB, 2+: models
402     for (const auto &it_pkg : pkg_map) {
403         size_t size_before = package_transforms.size();
404         std::vector<UUID> pkgs;
405         for (const auto &it_brd_pkg : it_pkg.second) {
406             pkgs.push_back(it_brd_pkg->uuid);
407             const auto &pl = it_brd_pkg->placement;
408             const auto &pkg = it_brd_pkg->package;
409             package_transforms.emplace_back(pl.shift.x / 1e6, pl.shift.y / 1e6, pl.get_angle(), it_brd_pkg->flip,
410                                             packages_highlight.count(it_brd_pkg->uuid));
411             auto &tr = package_transforms.back();
412             const auto model = pkg.get_model(it_brd_pkg->model);
413             if (model) {
414                 tr.model_x = model->x / 1e6;
415                 tr.model_y = model->y / 1e6;
416                 tr.model_z = model->z / 1e6;
417                 tr.model_roll = model->roll;
418                 tr.model_pitch = model->pitch;
419                 tr.model_yaw = model->yaw;
420             }
421         }
422         size_t size_after = package_transforms.size();
423         package_infos[it_pkg.first] = {size_before, size_after - size_before, pick_base, pkgs};
424         pick_base += pkgs.size();
425     }
426     point_pick_base = pick_base;
427 }
428 
import_step(const std::string & filename_rel,const std::string & filename_abs)429 STEPImporter::Faces Canvas3DBase::import_step(const std::string &filename_rel, const std::string &filename_abs)
430 {
431     const auto result = STEPImporter::import(filename_abs);
432     return result.faces;
433 }
434 
load_3d_model(const std::string & filename,const std::string & filename_abs)435 void Canvas3DBase::load_3d_model(const std::string &filename, const std::string &filename_abs)
436 {
437     if (models.count(filename))
438         return;
439 
440     const auto faces = import_step(filename, filename_abs);
441     {
442         std::lock_guard<std::mutex> lock(models_loading_mutex);
443         // canvas->face_vertex_buffer.reserve(faces.size());
444         size_t vertex_offset = face_vertex_buffer.size();
445         size_t first_index = face_index_buffer.size();
446         for (const auto &face : faces) {
447             for (size_t i = 0; i < face.vertices.size(); i++) {
448                 const auto &v = face.vertices.at(i);
449                 const auto &n = face.normals.at(i);
450                 face_vertex_buffer.emplace_back(v.x, v.y, v.z, n.x, n.y, n.z, face.color.r * 255, face.color.g * 255,
451                                                 face.color.b * 255);
452             }
453 
454             for (const auto &tri : face.triangle_indices) {
455                 size_t a, b, c;
456                 std::tie(a, b, c) = tri;
457                 face_index_buffer.push_back(a + vertex_offset);
458                 face_index_buffer.push_back(b + vertex_offset);
459                 face_index_buffer.push_back(c + vertex_offset);
460             }
461             vertex_offset += face.vertices.size();
462         }
463         size_t last_index = face_index_buffer.size();
464         models.emplace(std::piecewise_construct, std::forward_as_tuple(filename),
465                        std::forward_as_tuple(first_index, last_index - first_index));
466     }
467 }
468 
model_is_loaded(const std::string & filename)469 bool Canvas3DBase::model_is_loaded(const std::string &filename)
470 {
471     std::lock_guard<std::mutex> lock(models_loading_mutex);
472     return models.count(filename);
473 }
474 
set_point_transform(const glm::dmat4 & mat)475 void Canvas3DBase::set_point_transform(const glm::dmat4 &mat)
476 {
477     point_mat = mat;
478     invalidate_pick();
479     redraw();
480 }
481 
set_points(const std::vector<Point3D> & pts)482 void Canvas3DBase::set_points(const std::vector<Point3D> &pts)
483 {
484     points = pts;
485 }
486 
get_model_filenames(IPool & pool)487 std::map<std::string, std::string> Canvas3DBase::get_model_filenames(IPool &pool)
488 {
489     std::map<std::string, std::string> model_filenames; // first: relative, second: absolute
490     for (const auto &it : brd->packages) {
491         auto model = it.second.package.get_model(it.second.model);
492         if (model) {
493             std::string model_filename;
494             const Package *pool_package = nullptr;
495 
496             UUID this_pool_uuid = pool.get_pool_info().uuid;
497             UUID pkg_pool_uuid;
498             try {
499                 pool_package = pool.get_package(it.second.package.uuid, &pkg_pool_uuid);
500             }
501             catch (const std::runtime_error &e) {
502                 // it's okay
503             }
504             if (it.second.pool_package == pool_package) {
505                 // package is from pool, ask pool for model filename (might come from cache)
506                 model_filename = pool.get_model_filename(it.second.package.uuid, model->uuid);
507             }
508             else {
509                 // package is not from pool (from package editor, use filename relative to current pool)
510                 if (pkg_pool_uuid && pkg_pool_uuid != this_pool_uuid) { // pkg is open in RO mode from included pool
511                     model_filename = pool.get_model_filename(it.second.package.uuid, model->uuid);
512                 }
513                 else { // really editing package
514                     model_filename = Glib::build_filename(pool.get_base_path(), model->filename);
515                 }
516             }
517             if (model_filename.size())
518                 model_filenames[model->filename] = model_filename;
519         }
520     }
521     return model_filenames;
522 }
523 
clear_3d_models()524 void Canvas3DBase::clear_3d_models()
525 {
526     face_vertex_buffer.clear();
527     face_index_buffer.clear();
528     models.clear();
529 }
530 
update_max_package_height()531 void Canvas3DBase::update_max_package_height()
532 {
533     package_height_max = 0;
534     for (const auto &it : face_vertex_buffer) {
535         package_height_max = std::max(it.z, package_height_max);
536     }
537 }
538 
invalidate_pick()539 void Canvas3DBase::invalidate_pick()
540 {
541     pick_state = PickState::INVALID;
542 }
543 
queue_pick()544 void Canvas3DBase::queue_pick()
545 {
546     if (pick_state == PickState::INVALID) {
547         pick_state = PickState::QUEUED;
548         redraw();
549     }
550     else if (pick_state == PickState::CURRENT) {
551         s_signal_pick_ready.emit();
552     }
553 }
554 
pick_package_or_point(unsigned int x,unsigned int y) const555 std::variant<UUID, glm::dvec3> Canvas3DBase::pick_package_or_point(unsigned int x, unsigned int y) const
556 {
557     if (pick_state != PickState::CURRENT) {
558         Logger::log_warning("can't with non-current pick state");
559         return {};
560     }
561     x *= a_get_scale_factor();
562     y *= a_get_scale_factor();
563     const int idx = ((height * a_get_scale_factor()) - y - 1) * width * a_get_scale_factor() + x;
564     const auto pick = pick_buf.at(idx);
565     if (pick >= point_pick_base) {
566         const auto &pt = points.at(pick - point_pick_base);
567         const auto p4 = point_mat * glm::dvec4(pt.x, pt.y, pt.z, 1);
568         return glm::dvec3(p4.x, p4.y, p4.z);
569     }
570     for (const auto &[k, v] : package_infos) {
571         if (pick >= v.pick_base && pick < (v.pick_base + v.n_packages)) {
572             return v.pkg.at(pick - v.pick_base);
573         }
574     }
575     return {};
576 }
577 
acc(float & l,float & h,float v)578 static void acc(float &l, float &h, float v)
579 {
580     l = std::min(l, v);
581     h = std::max(h, v);
582 }
583 
get_model_bbox(const std::string & filename) const584 Canvas3DBase::BBox Canvas3DBase::get_model_bbox(const std::string &filename) const
585 {
586     Canvas3DBase::BBox bb;
587     bool first = true;
588     if (models.count(filename)) {
589         const auto &m = models.at(filename);
590         for (size_t i = m.face_index_offset; i < m.face_index_offset + m.count; i++) {
591             const auto idx = face_index_buffer.at(i);
592             const auto &v = face_vertex_buffer.at(idx);
593             if (first) {
594                 bb.xh = v.x;
595                 bb.xl = v.x;
596                 bb.yh = v.y;
597                 bb.yl = v.y;
598                 bb.zh = v.z;
599                 bb.zl = v.z;
600             }
601             else {
602                 acc(bb.xl, bb.xh, v.x);
603                 acc(bb.yl, bb.yh, v.y);
604                 acc(bb.zl, bb.zh, v.z);
605             }
606             first = false;
607         }
608     }
609     else {
610         bb = {0};
611     }
612     return bb;
613 }
614 
615 } // namespace horizon
616