Design tab: studio lighting and body edges in the 3D view

Reported: once extruded, a part is hard to read; the lighting says
little about its shape. Both lights of the object shaders sit near the
camera, so the sides of a part come out in almost the same tone, and
nothing marks where one face ends and the next begins.

- The phong shader gains a studio lighting model, chosen by a new
  lighting_model uniform: a sky/ground hemisphere in world space (up
  faces cool and bright, down faces warm and dark), a key light from the
  upper left and a weak fill from the right, a plastic-like highlight,
  and a darker base with a faint sheen toward the silhouette so curved
  faces read as round. GLCanvas3D::set_studio_lighting() makes a canvas
  draw its objects with it whatever the realistic-view preferences; only
  the Design canvas turns it on. Every canvas sets the uniform on each
  use, 0 for the slicer's, so they render as before.
- Every B-rep edge of a body is drawn as a thin dark line over it, depth
  tested and pulled a few pixels toward the eye so it wins against the
  faces meeting at it and hides behind the faces in front. The seam of a
  closed surface and degenerate edges are left out
  (GeometryEngine::display_edges). Edges are sampled once per shape and
  kept across recomputes that leave a body unchanged; bodies faded by
  body focus get fainter edges, and a dress-up previewing its result
  alone hides them with the bodies.

Tests: display_edges gives a box its 12 edges at their lengths, a
cylinder its two round rims without the seam, a cone its base rim
without the seam or the apex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
This commit is contained in:
Claude
2026-09-30 08:34:46 +00:00
parent b060101707
commit b8d45c6bb1
11 changed files with 237 additions and 5 deletions
+7
View File
@@ -362,6 +362,13 @@ the plane, `F` flips which half is kept. **Place on Face** (`F`, when section is
picked face flat on the bed. Origin planes (`P`) and world axes (`A`) can be toggled on while
you orient yourself.
Bodies are lit as in a studio, whatever the realistic-view preferences of the slicer: faces
facing up read cooler and brighter than faces facing down, a key light from the upper left
separates the sides of a part, and curved faces darken toward their outline. Every edge of a
body is drawn as a thin dark line, except the seam OCCT puts down the side of a cylinder or
cone; the edges of bodies faded by body focus are fainter, and a dress-up previewing its
result alone hides them with the bodies.
---
## Known limitations
+29 -2
View File
@@ -64,6 +64,10 @@ uniform PrintVolumeDetection print_volume;
uniform float z_far;
uniform float z_near;
uniform bool enable_ssao;
// 1 = the Design tab's studio lighting (studio_shade below); 0 = the two-light model above, which
// every other canvas keeps. world_up_eye is world +Z in eye space, for the hemisphere ambient.
uniform int lighting_model;
uniform vec3 world_up_eye;
// Depth-based shadow map (object-on-object and self shadows). shadow_intensity == 0 disables it.
uniform sampler2D shadow_map;
@@ -206,6 +210,27 @@ float shadow_shade()
return 1.0 - shadow_intensity * (sum / 25.0);
}
// Studio lighting for the Design tab. The default model lights every face from near the camera,
// so the sides of a part come out in nearly the same tone and its form is hard to read. This one
// separates faces by their orientation in the WORLD (a sky/ground hemisphere: up-facing faces
// cool and bright, down-facing ones warm and dark), keeps a strong key light from the upper left
// and a weak fill from the right, gives a plastic-like highlight, and darkens the base colour
// toward the silhouette while adding a faint sheen there, so curved faces read as round.
vec3 studio_shade(vec3 base, vec3 n, vec3 v)
{
vec3 key = normalize(vec3(-0.45, 0.60, 0.66));
vec3 fill = normalize(vec3(0.70, -0.15, 0.70));
float hemi = 0.5 + 0.5 * dot(n, normalize(world_up_eye));
vec3 ambient = mix(vec3(0.16, 0.15, 0.14), vec3(0.40, 0.42, 0.46), hemi);
float kd = max(dot(n, key), 0.0);
float fd = max(dot(n, fill), 0.0);
vec3 diffuse = ambient + vec3(0.60) * kd + vec3(0.20) * fd;
float spec = 0.28 * pow(max(dot(n, normalize(key + v)), 0.0), 48.0)
+ 0.06 * pow(max(dot(n, normalize(fill + v)), 0.0), 24.0);
float rim = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 3.0);
return base * diffuse * (1.0 - 0.30 * rim) + vec3(spec + 0.08 * rim);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -262,9 +287,11 @@ void main()
// SSAO is applied in post-process pass. Keep base lighting unchanged here.
float shade = shadow_shade();
vec3 lit = (lighting_model == 1) ? studio_shade(color.rgb, normal, view_dir)
: (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS;
if (is_outline) {
vec3 shaded_rgb = (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade;
vec3 shaded_rgb = lit * shade;
vec4 shaded_color = vec4(clamp(shaded_rgb, vec3(0.0), vec3(1.0)), color.a);
vec2 fragCoord = gl_FragCoord.xy;
float s = DetectSilho(fragCoord);
@@ -282,5 +309,5 @@ void main()
gl_FragColor = vec4(clamp((0.45 * texture2D(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + window_reflection + 0.8 * color.rgb * diffuse) * PHONG_BRIGHTNESS * shade, vec3(0.0), vec3(1.0)), color.a);
#endif
else
gl_FragColor = vec4(clamp((vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade, vec3(0.0), vec3(1.0)), color.a);
gl_FragColor = vec4(clamp(lit * shade, vec3(0.0), vec3(1.0)), color.a);
}
+29 -2
View File
@@ -74,6 +74,10 @@ uniform PrintVolumeDetection print_volume;
uniform float z_far;
uniform float z_near;
uniform bool enable_ssao;
// 1 = the Design tab's studio lighting (studio_shade below); 0 = the two-light model above, which
// every other canvas keeps. world_up_eye is world +Z in eye space, for the hemisphere ambient.
uniform int lighting_model;
uniform vec3 world_up_eye;
// Depth-based shadow map (object-on-object and self shadows). shadow_intensity == 0 disables it.
uniform sampler2D shadow_map;
@@ -263,6 +267,27 @@ float shadow_shade()
return 1.0 - shadow_intensity * (sum / 25.0);
}
// Studio lighting for the Design tab. The default model lights every face from near the camera,
// so the sides of a part come out in nearly the same tone and its form is hard to read. This one
// separates faces by their orientation in the WORLD (a sky/ground hemisphere: up-facing faces
// cool and bright, down-facing ones warm and dark), keeps a strong key light from the upper left
// and a weak fill from the right, gives a plastic-like highlight, and darkens the base colour
// toward the silhouette while adding a faint sheen there, so curved faces read as round.
vec3 studio_shade(vec3 base, vec3 n, vec3 v)
{
vec3 key = normalize(vec3(-0.45, 0.60, 0.66));
vec3 fill = normalize(vec3(0.70, -0.15, 0.70));
float hemi = 0.5 + 0.5 * dot(n, normalize(world_up_eye));
vec3 ambient = mix(vec3(0.16, 0.15, 0.14), vec3(0.40, 0.42, 0.46), hemi);
float kd = max(dot(n, key), 0.0);
float fd = max(dot(n, fill), 0.0);
vec3 diffuse = ambient + vec3(0.60) * kd + vec3(0.20) * fd;
float spec = 0.28 * pow(max(dot(n, normalize(key + v)), 0.0), 48.0)
+ 0.06 * pow(max(dot(n, normalize(fill + v)), 0.0), 24.0);
float rim = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 3.0);
return base * diffuse * (1.0 - 0.30 * rim) + vec3(spec + 0.08 * rim);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -319,9 +344,11 @@ void main()
// SSAO is applied in post-process pass. Keep base lighting unchanged here.
float shade = shadow_shade();
vec3 lit = (lighting_model == 1) ? studio_shade(color.rgb, normal, view_dir)
: (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS;
if (is_outline) {
vec3 shaded_rgb = (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade;
vec3 shaded_rgb = lit * shade;
vec4 shaded_color = vec4(clamp(shaded_rgb, vec3(0.0), vec3(1.0)), color.a);
float s = DetectSilho(gl_FragCoord.xy);
if (s < 0.01)
@@ -333,5 +360,5 @@ void main()
out_color = vec4(clamp((0.45 * texture(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + window_reflection + 0.8 * color.rgb * diffuse) * PHONG_BRIGHTNESS * shade, vec3(0.0), vec3(1.0)), color.a);
#endif
else
out_color = vec4(clamp((vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade, vec3(0.0), vec3(1.0)), color.a);
out_color = vec4(clamp(lit * shade, vec3(0.0), vec3(1.0)), color.a);
}
+22
View File
@@ -17,6 +17,7 @@
#include <TopTools_IndexedMapOfShape.hxx>
#include <TopTools_ListOfShape.hxx>
#include <TopTools_IndexedDataMapOfShapeListOfShape.hxx>
#include <TopTools_ListIteratorOfListOfShape.hxx>
#include <Poly_Triangulation.hxx>
#include <gp_Ax2.hxx>
#include <gp_Dir.hxx>
@@ -611,6 +612,27 @@ std::vector<Vec3d> GeometryEngine::sample_edge_world(const TopoDS_Edge& edge, do
return pts;
}
std::vector<std::vector<Vec3d>> GeometryEngine::display_edges(const TopoDS_Shape& shape, double chord_tol)
{
TopTools_IndexedDataMapOfShapeListOfShape faces_of_edge;
TopExp::MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, faces_of_edge);
std::vector<std::vector<Vec3d>> out;
for (int i = 1; i <= faces_of_edge.Extent(); ++i) {
const TopoDS_Edge& edge = TopoDS::Edge(faces_of_edge.FindKey(i));
if (BRep_Tool::Degenerated(edge))
continue;
bool seam = false;
for (TopTools_ListIteratorOfListOfShape it(faces_of_edge.FindFromIndex(i)); it.More() && !seam; it.Next())
seam = BRep_Tool::IsClosed(edge, TopoDS::Face(it.Value()));
if (seam)
continue;
std::vector<Vec3d> pts = sample_edge_world(edge, chord_tol);
if (pts.size() >= 2)
out.push_back(std::move(pts));
}
return out;
}
Vec3d GeometryEngine::face_centroid_world(const TopoDS_Face& face)
{
GProp_GProps props;
+4
View File
@@ -149,6 +149,10 @@ public:
static Vec3d face_normal_world(const TopoDS_Face& face);
// Sample an edge into a world-space polyline (>=2 pts) for pick-distance + highlight.
static std::vector<Vec3d> sample_edge_world(const TopoDS_Edge& edge, double chord_tol = 0.05);
// The edges a viewer draws over a body, each as a polyline: every edge of the shape once,
// without degenerate edges (a cone apex) and without the seam of a closed surface (the line
// down a cylinder's side), which is where OCCT closes the parameter space, not a real edge.
static std::vector<std::vector<Vec3d>> display_edges(const TopoDS_Shape& shape, double chord_tol);
// 0-based edge index into TopExp::MapShapes(shape, TopAbs_EDGE, map).
static int edge_count(const TopoDS_Shape& shape);
static TopoDS_Edge edge_by_index(const TopoDS_Shape& shape, int index);
+2
View File
@@ -46,6 +46,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// cannot be null. Borrow the plater's, as the editor canvases do.
m_canvas->set_process(&wxGetApp().plater()->background_process());
m_canvas->set_type(GLCanvas3D::ECanvasType::CanvasView3D);
m_canvas->set_studio_lighting(true); // see GLCanvas3D::m_studio_lighting and phong.fs
// CAD navigation, this canvas only: left-drag sweeps a selection rubber band, so orbit
// moves to middle-drag and pan to right-drag. Design is a different modality from
@@ -1347,6 +1348,7 @@ void DesignCanvas::set_body_hidden(bool on)
{
if (m_body_hidden == on) return;
m_body_hidden = on;
m_sketch_tool.set_body_edges_hidden(on);
reload(true); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
}
+91
View File
@@ -14,6 +14,10 @@
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLShader.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <BRepBndLib.hxx>
#include <Bnd_Box.hxx>
#include <Standard_Failure.hxx>
#include "libslic3r/TriangleMesh.hpp"
#include <GL/glew.h>
@@ -3424,9 +3428,94 @@ void DesignSketchTool::set_solid_pick(const std::vector<CadBody>* bodies, const
m_solid_bodies = bodies; m_solid_mesh = mesh; m_solid_tri_face = tri_face; m_solid_tri_body = tri_body;
m_solid_visible = visible; m_solid_xform = xform;
}
refresh_body_edges();
clear_solid_selection();
}
void DesignSketchTool::refresh_body_edges()
{
const size_t n = m_solid_bodies != nullptr ? m_solid_bodies->size() : 0;
m_body_edges.resize(n);
m_body_edges_key.resize(n, nullptr);
for (size_t b = 0; b < n; ++b) {
const TopoDS_Shape& shape = (*m_solid_bodies)[b].shape;
const void* key = shape.IsNull() ? nullptr : shape.TShape().get();
if (key == m_body_edges_key[b] && key != nullptr)
continue;
m_body_edges_key[b] = key;
m_body_edges[b].clear();
if (key == nullptr)
continue;
// A thousandth of the body's size: round edges stay round at any zoom that shows the
// whole body, without sampling a large import into millions of segments.
Bnd_Box box;
BRepBndLib::Add(shape, box);
const double diag = box.IsVoid() ? 1.0 : std::sqrt(box.SquareExtent());
try {
m_body_edges[b] = GeometryEngine::display_edges(shape, std::max(1e-3 * diag, 0.005));
} catch (const Standard_Failure&) {
m_body_edges[b].clear(); // an unsampleable edge costs its body the lines, nothing else
}
}
}
void DesignSketchTool::render_body_edges()
{
if (m_body_edges_hidden || m_solid_bodies == nullptr)
return;
using EPT = GLModel::Geometry::EPrimitiveType;
using EVL = GLModel::Geometry::EVertexLayout;
const Camera& cam = wxGetApp().plater()->get_camera();
const Vec3d vd = cam.get_dir_forward();
const double px = 1.0 / std::max(cam.get_zoom(), 1e-6);
const double hw = 0.75 * px; // ~1.5 px wide
// Pulled toward the eye by a few pixels, so the line wins the depth test against the two
// faces meeting at the edge while a face in front of it still hides it.
const Vec3d pull = -vd * (3.0 * px);
// Two passes: the edges of a body faded by body focus are fainter, like the body itself.
for (int pass = 0; pass < 2; ++pass) {
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
unsigned int base = 0;
for (int b = 0; b < int(m_body_edges.size()); ++b) {
if (m_solid_visible != nullptr && b < int(m_solid_visible->size()) && !(*m_solid_visible)[b])
continue;
const bool faded = m_pick_only_body >= 0 && m_pick_only_body < int(m_body_edges.size())
&& b != m_pick_only_body;
if (faded != (pass == 1))
continue;
for (const std::vector<Vec3d>& pl : m_body_edges[b])
for (size_t s = 1; s < pl.size(); ++s) {
const Vec3d a = body_xform_pt(b, pl[s - 1]) + pull, c = body_xform_pt(b, pl[s]) + pull;
Vec3d dir = c - a; if (dir.norm() < 1e-9) continue; dir.normalize();
Vec3d off = dir.cross(vd);
if (off.norm() < 1e-9) continue; // edge seen end-on: a point, nothing to draw
off = off.normalized() * hw;
g.add_vertex((Vec3f)(a + off).cast<float>());
g.add_vertex((Vec3f)(c + off).cast<float>());
g.add_vertex((Vec3f)(c - off).cast<float>());
g.add_vertex((Vec3f)(a - off).cast<float>());
g.add_triangle(base, base + 1, base + 2);
g.add_triangle(base, base + 2, base + 3); base += 4;
}
}
if (base == 0)
continue;
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthFunc(GL_LEQUAL));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
m_body_edges_model.reset();
m_body_edges_model.init_from(std::move(g));
m_body_edges_model.set_color(ColorRGBA(0.08f, 0.09f, 0.11f, pass == 0 ? 0.85f : 0.25f));
m_body_edges_model.render();
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glDepthFunc(GL_LESS));
glsafe(::glDisable(GL_BLEND));
glsafe(::glDisable(GL_DEPTH_TEST));
}
}
// Map a point sampled from the (untransformed) OCCT body shape through the body's display
// transform, so edge picking/highlight track a moved body. The pick MESH is already
// transformed by the host; only OCCT-sampled edges need this.
@@ -8929,6 +9018,8 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
shader->set_uniform("view_model_matrix", camera.get_view_matrix());
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
render_body_edges();
// Persistent committed sketches (e.g. an un-consumed sketch left visible after its
// extrude is removed): faces translucent, outlines orange. Each uses its own plane.
if (!m_display_sketches.empty()) {
+12
View File
@@ -198,6 +198,9 @@ public:
// another solid is reachable without hiding anything. -1 = no restriction.
// Survives set_solid_pick() — it is owned by the panel, not by the mesh feed.
void set_pick_only_body(int b) { m_pick_only_body = b; }
// Off while the bodies themselves are hidden (a dress-up previewing its result alone), so
// their edges do not float over the preview.
void set_body_edges_hidden(bool h) { m_body_edges_hidden = h; }
void clear_solid_selection();
bool has_solid_selection() const { return m_solid_sel != SolidSel::None; }
// Every picked edge when the selection is an edge set (Shift/Ctrl+click adds and removes
@@ -1204,6 +1207,14 @@ private:
int m_pick_only_body{-1}; // >=0: only this body catches clicks (body-focus x-ray for CoordSys picking)
const std::vector<Transform3d>* m_solid_xform{nullptr}; // per-body display transform (for edge sampling)
Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform
// The bodies' B-rep edges, drawn as dark lines over the solids so faces and features read
// apart. One polyline set per body in its own shape coordinates, resampled only for a body
// whose shape changed (keyed by the TShape), since set_solid_pick runs on every recompute.
std::vector<std::vector<std::vector<Vec3d>>> m_body_edges;
std::vector<const void*> m_body_edges_key;
bool m_body_edges_hidden{false};
void refresh_body_edges();
void render_body_edges();
bool body_pickable(int b) const; // false when the body is explicitly hidden
SolidSel m_solid_sel{SolidSel::None};
int m_sel_body{-1}; // which body the face/edge selection is on
@@ -1269,6 +1280,7 @@ private:
GLModel m_mc_fill_model; // the face treatment's shaded facets
GLModel m_solid_face_model;
GLModel m_solid_edge_model;
GLModel m_body_edges_model;
GLModel m_solid_vertex_model;
int m_display_pick_region{-1}; // selected closed-region index within that feature (-1 none)
+9 -1
View File
@@ -8489,10 +8489,11 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
const bool realistic_mode = _is_realistic_view_enabled();
const bool realistic_phong = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_PHONG);
const std::string shader_name = (realistic_mode && realistic_phong) ? "phong" : "gouraud";
const std::string shader_name = (m_studio_lighting || (realistic_mode && realistic_phong)) ? "phong" : "gouraud";
GLShaderProgram* shader = wxGetApp().get_shader(shader_name);
if (shader == nullptr && shader_name != "gouraud")
shader = wxGetApp().get_shader("gouraud");
const bool studio = m_studio_lighting && shader != nullptr && shader->get_name() == "phong";
ECanvasType canvas_type = this->m_canvas_type;
bool partly_inside_enable = canvas_type == ECanvasType::CanvasAssembleView ? false : true;
// The edited printer's per-extruder printable heights feed the object shader's
@@ -8505,6 +8506,13 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
const bool phong_ssao = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SSAO);
shader->set_uniform("enable_ssao", phong_ssao);
// Set on every use: the program is shared, so a canvas that leaves it unset would inherit
// the last canvas's choice.
shader->set_uniform("lighting_model", studio ? 1 : 0);
if (studio) {
const Transform3d& view = wxGetApp().plater()->get_camera().get_view_matrix();
shader->set_uniform("world_up_eye", Vec3f((view.matrix().block<3, 3>(0, 0) * Vec3d::UnitZ()).cast<float>()));
}
// Object-on-object and self shadows: sample the depth map built in _render_shadow_map_pass().
// shadow_intensity == 0 disables the effect entirely (unchanged behavior when off / unsupported).
+4
View File
@@ -590,6 +590,9 @@ private:
//BBS: add canvas type for assemble view usage
ECanvasType m_canvas_type;
// Objects drawn with the phong shader's studio lighting whatever the realistic-view settings
// (the Design tab's canvas). Off for every canvas of the slicer, which render as before.
bool m_studio_lighting{false};
std::array<ClippingPlane, 2> m_clipping_planes;
ClippingPlane m_camera_clipping_plane;
bool m_use_clipping_planes;
@@ -815,6 +818,7 @@ public:
void set_context(wxGLContext* context) { m_context = context; }
void set_type(ECanvasType type) { m_canvas_type = type; }
void set_studio_lighting(bool on) { m_studio_lighting = on; }
ECanvasType get_canvas_type() { return m_canvas_type; }
wxGLCanvas* get_wxglcanvas() { return m_canvas; }
+28
View File
@@ -32,6 +32,8 @@
#include <BRepGProp.hxx>
#include <GProp_GProps.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepPrimAPI_MakeCone.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepBuilderAPI_Transform.hxx>
#include <gp_Trsf.hxx>
#include <gp_Vec.hxx>
@@ -8920,3 +8922,29 @@ TEST_CASE("CadDocument: a profile on a body face touches it, one in free space d
CHECK(doc.body_touching_sketch(-1) == -1);
CHECK(doc.body_touching_sketch(base + 1) == -1); // the extrude: not a sketch
}
TEST_CASE("display edges: every real edge once, no seams, no degenerate apex", "[CadDocument][display]")
{
const auto box = GeometryEngine::display_edges(BRepPrimAPI_MakeBox(10., 20., 30.).Shape(), 0.01);
CHECK(box.size() == 12);
for (const auto& pl : box) {
REQUIRE(pl.size() >= 2);
const double len = (pl.back() - pl.front()).norm();
CHECK((std::abs(len - 10.) < 1e-6 || std::abs(len - 20.) < 1e-6 || std::abs(len - 30.) < 1e-6));
}
// A cylinder has three edges in OCCT: the two rims and the seam down its side. Only the rims
// are drawn, each sampled finely enough to look round and closed.
const auto cyl = GeometryEngine::display_edges(BRepPrimAPI_MakeCylinder(5., 8.).Shape(), 0.01);
REQUIRE(cyl.size() == 2);
for (const auto& pl : cyl) {
CHECK(pl.size() > 16);
CHECK((pl.front() - pl.back()).norm() < 1e-6);
for (const Vec3d& p : pl)
CHECK(std::abs(std::hypot(p.x(), p.y()) - 5.) < 0.02);
}
// A cone keeps its base rim; the apex is a degenerate edge and the side has a seam.
const auto cone = GeometryEngine::display_edges(BRepPrimAPI_MakeCone(5., 0., 8.).Shape(), 0.01);
CHECK(cone.size() == 1);
}