diff --git a/.gitignore b/.gitignore index ef3ee9c..ad995cf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ _opam/ _esy/ examples/scads +examples/pngs/incl_*.png diff --git a/OSCADml.opam b/OSCADml.opam index c8ba67e..08711ce 100644 --- a/OSCADml.opam +++ b/OSCADml.opam @@ -9,7 +9,7 @@ authors: [ "Masaki Nakano" ] license: "GPL-2.0-or-later" -tags: ["OCADml" "CAD" "OpenSCAD"] +tags: ["OCADml" "CAD" "OpenSCAD" "CSG"] homepage: "https://github.com/OCADml/OSCADml" doc: "https://ocadml.github.io/OSCADml" bug-reports: "https://github.com/OCADml/OSCADml/issues" @@ -18,7 +18,7 @@ depends: [ "ocaml" {>= "4.14.0"} "gg" {>= "1.0.0"} "cairo2" {>= "0.6.2"} - "OCADml" {>= "0.5.0"} + "OCADml" {>= "0.6.0"} "odoc" {with-doc} ] build: [ diff --git a/docs/index.mld b/docs/index.mld index 0282503..aa29c18 100644 --- a/docs/index.mld +++ b/docs/index.mld @@ -85,6 +85,7 @@ should serve as a helpful reference. {2 Skins and Morphs} - {{!page-"profile_skinning"} {b Profile skinning}} +- {{!page-"spline_skinning"} {b Continuous skinning with Bézier splines}} - {{!page-"morphing_sweeps"} {b Morphing sweeps and extrusions}} {2 Function Plotting} diff --git a/dune-project b/dune-project index c17bae2..73aee87 100644 --- a/dune-project +++ b/dune-project @@ -27,7 +27,7 @@ (description "OSCADml is an OCaml front-end to the OpenSCAD CAD programming language.") (tags - ("OCADml" "CAD" "OpenSCAD")) + ("OCADml" "CAD" "OpenSCAD" "CSG")) (depends (ocaml (>= 4.14.0)) @@ -36,4 +36,4 @@ (cairo2 (>= 0.6.2)) (OCADml - (>= 0.5.0)))) + (>= 0.6.0)))) diff --git a/examples/arc_points.ml b/examples/arc_points.ml index bf59f7b..39554e9 100644 --- a/examples/arc_points.ml +++ b/examples/arc_points.ml @@ -33,9 +33,7 @@ let () = in let wedge = Scad.extrude ~height:1. @@ Scad.of_path2 arc and marks = - Debug.show_path2 show arc - |> Scad.translate (v3 0. 0. 1.1) - |> Scad.color ~alpha:0.8 Color.Magenta + Scad.color ~alpha:0.8 Color.Magenta @@ Scad.ztrans 1.1 (Debug.show_path2 show arc) in Scad.to_file "arc_wedge_2d.scad" @@ Scad.union [ wedge; marks ] diff --git a/examples/docs/arc_points.mld b/examples/docs/arc_points.mld index 59cdb5b..2e4414c 100644 --- a/examples/docs/arc_points.mld +++ b/examples/docs/arc_points.mld @@ -42,9 +42,7 @@ let () = in let wedge = Scad.extrude ~height:1. @@ Scad.of_path2 arc and marks = - Debug.show_path2 show arc - |> Scad.translate (v3 0. 0. 1.1) - |> Scad.color ~alpha:0.8 Color.Magenta + Scad.color ~alpha:0.8 Color.Magenta @@ Scad.ztrans 1.1 (Debug.show_path2 show arc) in Scad.to_file "arc_wedge_2d.scad" @@ Scad.union [ wedge; marks ] ]} diff --git a/examples/docs/morphing_sweeps.mld b/examples/docs/morphing_sweeps.mld index ee4c90d..4e666c2 100644 --- a/examples/docs/morphing_sweeps.mld +++ b/examples/docs/morphing_sweeps.mld @@ -18,7 +18,7 @@ let () = V3.[ v 0. 0. 2.; v 0. 20. 20.; v 40. 20. 10.; v 30. 0. 10. ] |> Path3.quaternion (Quaternion.make (v3 1. 1. 0.) (Float.pi /. -5.)) in - Bezier3.curve ~fn:60 @@ Bezier3.of_path ~size:(`Flat (`Rel 0.3)) control + Bezier3.curve ~fn:80 @@ Bezier3.of_path ~size:(`Flat (`Rel 0.3)) control and caps = Mesh.Cap.(capped ~bot:(round @@ circ (`Radius 0.5)) ~top:(round @@ circ (`Radius 0.5))) and a = Poly2.ring ~fn:5 ~thickness:(v2 2.5 2.5) (v2 6. 6.) diff --git a/examples/docs/polyholes.mld b/examples/docs/polyholes.mld index 3a75f61..fe635a1 100644 --- a/examples/docs/polyholes.mld +++ b/examples/docs/polyholes.mld @@ -23,7 +23,7 @@ let () = |> Scad.translate (v3 0. 0. (-3.)) |> Scad.color ~alpha:0.5 Color.BlueViolet in - Scad.union [ poly; reference ] + Scad.add poly reference in Scad.to_file "polyholes.scad" scad ]} diff --git a/examples/docs/profile_skinning.mld b/examples/docs/profile_skinning.mld index 5d6aa67..28f6553 100644 --- a/examples/docs/profile_skinning.mld +++ b/examples/docs/profile_skinning.mld @@ -15,14 +15,13 @@ connector. {[ let profiles = - let fn = 32 - and up h = Path3.translate (v3 0. 0. h) in + let fn = 32 in let base = let sq = Path3.square ~center:true (v2 2. 4.) in Path3.(roundover ~fn (Round.flat ~corner:(Round.circ (`Radius 0.5)) sq)) - and c r h = up h @@ Path3.circle ~fn r in + and c r h = Path3.ztrans h @@ Path3.circle ~fn r in let cones = List.map (fun h -> [ c 0.6 h; c 0.5 (h +. 1.) ]) [ 4.; 5.; 6. ] in - List.flatten @@ ([ base; up 2. base; c 0.5 3.; c 0.5 4. ] :: cones) + List.flatten ([ base; Path3.ztrans 2. base; c 0.5 3.; c 0.5 4. ] :: cones) ]} A quick look at the points of our profiles we are about to mesh over with @@ -56,15 +55,3 @@ let () =

%} - -{[ -let () = - Mesh.skin - ~refine:2 - ~slices:(`Flat 25) - ~mapping:(`Flat `Tangent) - Path3.[ circle ~fn:5 4.; translate (v3 0. 0. 3.) @@ circle ~fn:80 2. ] - |> Scad.of_mesh - |> Scad.to_file "tangent_skin_test.scad" -]} - diff --git a/examples/docs/spline.mld b/examples/docs/spline.mld index 0832a8f..01de021 100644 --- a/examples/docs/spline.mld +++ b/examples/docs/spline.mld @@ -10,7 +10,7 @@ Control points that our through. {[ -let control = [ v2 0. 10.; v2 10. 40.; v2 20. 40.; v2 30. (-20.); v2 40. (-40.) ] +let control = V2.[ v 0. 10.; v 10. 40.; v 20. 40.; v 30. (-20.); v 40. (-40.) ] ]} Mark our [control] points with the debugging helper @@ -38,7 +38,7 @@ let line = Union our control point [marks] and [line] sweep shapes and output to file. {[ -let () = Scad.to_file "spline.scad" (Scad.union [ line; marks ]) +let () = Scad.to_file "spline.scad" (Scad.add line marks) ]} {%html: diff --git a/examples/docs/spline_skinning.mld b/examples/docs/spline_skinning.mld new file mode 100644 index 0000000..1e2005b --- /dev/null +++ b/examples/docs/spline_skinning.mld @@ -0,0 +1,96 @@ +{0 Bézier Sklines} + +{[ +open OCADml +open OSCADml +]} + +{{!OCADml.Mesh.skline} [Mesh.skline]}, like {{!OCADml.Mesh.skin} +[Mesh.skin]} provides a means of generating a mesh that covers a series of +given profiles, but where [skin] linearly transistions between each profile +independently, [skline] splines through each of the paths formed by the +associated vertices from the first profile to the last (or in a loop). +to generate meshes that cover over series of profiles. However, unlike +[skin] only resampling is made available for mapping vertices between +incomensurate profiles as the point duplication methods (tangent and +distance) can easily lead to edge intersections. + +{[ +let handle_profiles = + let circ = Path3.circle ~fn:64 5. in + let base = Path3.scale (v3 1.2 1.2 1.) circ + and handle = Path3.scale (v3 0.7 0.7 1.) circ in + Path3. + [ ztrans (-3.) base + ; circ + ; translate (v3 15. 0. 20.) (yrot (Float.pi /. 2.) handle) + ; xtrans 30. (yrot Float.pi circ) + ; translate (v3 30. 0. (-3.)) (yrot Float.pi base) + ] +]} + +A quick look at the points of our profiles we are about to spline over with +alternating colours may help a bit to conceptualize what we are about to +give {{!OCADml.Mesh.skline} [Mesh.skline]} to work with. + +{[ +let () = + let show i = + let c = if i mod 2 = 0 then Color.Magenta else Color.Aquamarine in + Debug.show_path3 (fun _ -> Scad.(color c @@ sphere 0.2)) + in + List.mapi show handle_profiles |> Scad.union |> Scad.to_file "handle_points.scad" +]} + +{%html: +

+ +

%} + +Using the [?tangents] parameter of {{!OCADml.Bezier3.of_path} +[Bezier3.of_path]} we can specify the tangents we want for each profile, +rather than leaving them up to the automatically computed derivatives (that +may differ for each edge path tracing between the profiles). Here we +contstrain them to cardinals so we can get a handle that sticks closer to +right angles. + +{[ +let () = + let up = v3 0. 0. 1. in + let tangents = `Tangents [ up; up; v3 1. 0. 0.; V3.neg up; V3.neg up ] in + Mesh.skline ~fn:200 ~size:(`Flat (`Rel 0.5)) ~tangents handle_profiles + |> Scad.of_mesh + |> Scad.to_file ~incl:true "handle_skline.scad" +]} + +{%html: +

+ +

%} + +As mentioned above, continuous curvature loops are also possible. Here we +morph cyclically through circular and pentagonal profiles by specifying +[~endcaps:`Loop]. + +{[ +let () = + let circ = Path3.circle ~fn:64 5. in + let pent = Path3.(circle ~fn:5 5.) in + let profs = + Path3. + [ circ + ; translate (v3 15. 0. 20.) (yrot (Float.pi /. 2.) pent) + ; xtrans 30. (yrot Float.pi circ) + ; translate (v3 15. 0. (-20.)) (yrot (Float.pi *. 1.5) pent) + ] + in + Mesh.skline ~endcaps:`Loop ~fn:200 ~size:(`Flat (`Rel 0.1)) profs + |> Scad.of_mesh + |> Scad.to_file ~incl:true "edgy_loop.scad" +]} + +{%html: +

+ +

%} + diff --git a/examples/dune b/examples/dune index b03f582..c49dbb3 100644 --- a/examples/dune +++ b/examples/dune @@ -15,6 +15,7 @@ resampled_path default_vs_euler_sweeps profile_skinning + spline_skinning morphing_sweeps) (libraries OSCADml OCADml.PolyText)) diff --git a/examples/morphing_sweeps.ml b/examples/morphing_sweeps.ml index 73f695d..c834647 100644 --- a/examples/morphing_sweeps.ml +++ b/examples/morphing_sweeps.ml @@ -15,7 +15,7 @@ let () = V3.[ v 0. 0. 2.; v 0. 20. 20.; v 40. 20. 10.; v 30. 0. 10. ] |> Path3.quaternion (Quaternion.make (v3 1. 1. 0.) (Float.pi /. -5.)) in - Bezier3.curve ~fn:60 @@ Bezier3.of_path ~size:(`Flat (`Rel 0.3)) control + Bezier3.curve ~fn:80 @@ Bezier3.of_path ~size:(`Flat (`Rel 0.3)) control and caps = Mesh.Cap.(capped ~bot:(round @@ circ (`Radius 0.5)) ~top:(round @@ circ (`Radius 0.5))) and a = Poly2.ring ~fn:5 ~thickness:(v2 2.5 2.5) (v2 6. 6.) diff --git a/examples/pngs/arc_points_2d.png b/examples/pngs/arc_points_2d.png index 32bd0e5..4ed2f8d 100644 Binary files a/examples/pngs/arc_points_2d.png and b/examples/pngs/arc_points_2d.png differ diff --git a/examples/pngs/arc_points_3d.png b/examples/pngs/arc_points_3d.png index 8cda65d..9cb6d49 100644 Binary files a/examples/pngs/arc_points_3d.png and b/examples/pngs/arc_points_3d.png differ diff --git a/examples/pngs/arc_wedge_2d.png b/examples/pngs/arc_wedge_2d.png index c023b48..923e9d3 100644 Binary files a/examples/pngs/arc_wedge_2d.png and b/examples/pngs/arc_wedge_2d.png differ diff --git a/examples/pngs/bezier_spline_path.png b/examples/pngs/bezier_spline_path.png index 34ce2e4..32ea760 100644 Binary files a/examples/pngs/bezier_spline_path.png and b/examples/pngs/bezier_spline_path.png differ diff --git a/examples/pngs/cartesian_gravity_well.png b/examples/pngs/cartesian_gravity_well.png index 9caa9dd..e9380dc 100644 Binary files a/examples/pngs/cartesian_gravity_well.png and b/examples/pngs/cartesian_gravity_well.png differ diff --git a/examples/pngs/chamfered_loop.png b/examples/pngs/chamfered_loop.png index c17c6db..516c9cf 100644 Binary files a/examples/pngs/chamfered_loop.png and b/examples/pngs/chamfered_loop.png differ diff --git a/examples/pngs/chamfered_square_with_holes.png b/examples/pngs/chamfered_square_with_holes.png index 5a8f0eb..2670e95 100644 Binary files a/examples/pngs/chamfered_square_with_holes.png and b/examples/pngs/chamfered_square_with_holes.png differ diff --git a/examples/pngs/circular_rounding.png b/examples/pngs/circular_rounding.png index 7ed6a5a..20a1b77 100644 Binary files a/examples/pngs/circular_rounding.png and b/examples/pngs/circular_rounding.png differ diff --git a/examples/pngs/edgy_loop.png b/examples/pngs/edgy_loop.png new file mode 100644 index 0000000..c4e3d38 Binary files /dev/null and b/examples/pngs/edgy_loop.png differ diff --git a/examples/pngs/elbow.png b/examples/pngs/elbow.png index 0fdc6d1..ec4aff5 100644 Binary files a/examples/pngs/elbow.png and b/examples/pngs/elbow.png differ diff --git a/examples/pngs/handle_points.png b/examples/pngs/handle_points.png new file mode 100644 index 0000000..6574629 Binary files /dev/null and b/examples/pngs/handle_points.png differ diff --git a/examples/pngs/handle_skline.png b/examples/pngs/handle_skline.png new file mode 100644 index 0000000..2135949 Binary files /dev/null and b/examples/pngs/handle_skline.png differ diff --git a/examples/pngs/helix_extrude.png b/examples/pngs/helix_extrude.png index a305df5..18ddc80 100644 Binary files a/examples/pngs/helix_extrude.png and b/examples/pngs/helix_extrude.png differ diff --git a/examples/pngs/helix_path_points.png b/examples/pngs/helix_path_points.png index 3a2258d..b209e51 100644 Binary files a/examples/pngs/helix_path_points.png and b/examples/pngs/helix_path_points.png differ diff --git a/examples/pngs/incl_polar_rose.png b/examples/pngs/incl_polar_rose.png index dda26fd..895dba6 100644 Binary files a/examples/pngs/incl_polar_rose.png and b/examples/pngs/incl_polar_rose.png differ diff --git a/examples/pngs/offset_poly.png b/examples/pngs/offset_poly.png index aa8ddfe..ed47efe 100644 Binary files a/examples/pngs/offset_poly.png and b/examples/pngs/offset_poly.png differ diff --git a/examples/pngs/polar_rose.png b/examples/pngs/polar_rose.png index dda26fd..895dba6 100644 Binary files a/examples/pngs/polar_rose.png and b/examples/pngs/polar_rose.png differ diff --git a/examples/pngs/resampled_path.png b/examples/pngs/resampled_path.png index c889952..9d36bc0 100644 Binary files a/examples/pngs/resampled_path.png and b/examples/pngs/resampled_path.png differ diff --git a/examples/pngs/rounded_polyhole_sweep.png b/examples/pngs/rounded_polyhole_sweep.png index 5a023a9..f88666d 100644 Binary files a/examples/pngs/rounded_polyhole_sweep.png and b/examples/pngs/rounded_polyhole_sweep.png differ diff --git a/examples/pngs/rounded_prism_star.png b/examples/pngs/rounded_prism_star.png index 76383c4..e6b77bc 100644 Binary files a/examples/pngs/rounded_prism_star.png and b/examples/pngs/rounded_prism_star.png differ diff --git a/examples/pngs/spiral.png b/examples/pngs/spiral.png index 30f1699..e26828f 100644 Binary files a/examples/pngs/spiral.png and b/examples/pngs/spiral.png differ diff --git a/examples/pngs/sweep_path_default.png b/examples/pngs/sweep_path_default.png index e2e85af..5ff69f2 100644 Binary files a/examples/pngs/sweep_path_default.png and b/examples/pngs/sweep_path_default.png differ diff --git a/examples/pngs/sweep_path_euler.png b/examples/pngs/sweep_path_euler.png index 08c550e..9451644 100644 Binary files a/examples/pngs/sweep_path_euler.png and b/examples/pngs/sweep_path_euler.png differ diff --git a/examples/pngs/sweep_starburst_default.png b/examples/pngs/sweep_starburst_default.png index dff10a7..03c0fa1 100644 Binary files a/examples/pngs/sweep_starburst_default.png and b/examples/pngs/sweep_starburst_default.png differ diff --git a/examples/pngs/sweep_starburst_euler.png b/examples/pngs/sweep_starburst_euler.png index 893f6a1..72261e9 100644 Binary files a/examples/pngs/sweep_starburst_euler.png and b/examples/pngs/sweep_starburst_euler.png differ diff --git a/examples/pngs/tangent_morph_sweep.png b/examples/pngs/tangent_morph_sweep.png index 8f4a5c2..df88203 100644 Binary files a/examples/pngs/tangent_morph_sweep.png and b/examples/pngs/tangent_morph_sweep.png differ diff --git a/examples/pngs/tangent_skin_test.png b/examples/pngs/tangent_skin_test.png deleted file mode 100644 index f3de799..0000000 Binary files a/examples/pngs/tangent_skin_test.png and /dev/null differ diff --git a/examples/pngs/test_hull_3d.png b/examples/pngs/test_hull_3d.png index 1d73681..4977f00 100644 Binary files a/examples/pngs/test_hull_3d.png and b/examples/pngs/test_hull_3d.png differ diff --git a/examples/polyholes.ml b/examples/polyholes.ml index 02654d4..5210632 100644 --- a/examples/polyholes.ml +++ b/examples/polyholes.ml @@ -22,6 +22,6 @@ let () = |> Scad.translate (v3 0. 0. (-3.)) |> Scad.color ~alpha:0.5 Color.BlueViolet in - Scad.union [ poly; reference ] + Scad.add poly reference in Scad.to_file "polyholes.scad" scad diff --git a/examples/profile_skinning.ml b/examples/profile_skinning.ml index c15088f..2cbaaa4 100644 --- a/examples/profile_skinning.ml +++ b/examples/profile_skinning.ml @@ -12,14 +12,13 @@ open OSCADml connector. *) let profiles = - let fn = 32 - and up h = Path3.translate (v3 0. 0. h) in + let fn = 32 in let base = let sq = Path3.square ~center:true (v2 2. 4.) in Path3.(roundover ~fn (Round.flat ~corner:(Round.circ (`Radius 0.5)) sq)) - and c r h = up h @@ Path3.circle ~fn r in + and c r h = Path3.ztrans h @@ Path3.circle ~fn r in let cones = List.map (fun h -> [ c 0.6 h; c 0.5 (h +. 1.) ]) [ 4.; 5.; 6. ] in - List.flatten @@ ([ base; up 2. base; c 0.5 3.; c 0.5 4. ] :: cones) + List.flatten ([ base; Path3.ztrans 2. base; c 0.5 3.; c 0.5 4. ] :: cones) (** A quick look at the points of our profiles we are about to mesh over with alternating colours may help a bit to conceptualize what we are about to @@ -47,12 +46,3 @@ let () =

%} *) - -let () = - Mesh.skin - ~refine:2 - ~slices:(`Flat 25) - ~mapping:(`Flat `Tangent) - Path3.[ circle ~fn:5 4.; translate (v3 0. 0. 3.) @@ circle ~fn:80 2. ] - |> Scad.of_mesh - |> Scad.to_file "tangent_skin_test.scad" diff --git a/examples/spline.ml b/examples/spline.ml index 1e475b3..43e980b 100644 --- a/examples/spline.ml +++ b/examples/spline.ml @@ -6,7 +6,7 @@ open OSCADml (** Control points that our {{:https://mathworld.wolfram.com/CubicSpline.html}cubic spline} will pass through. *) -let control = [ v2 0. 10.; v2 10. 40.; v2 20. 40.; v2 30. (-20.); v2 40. (-40.) ] +let control = V2.[ v 0. 10.; v 10. 40.; v 20. 40.; v 30. (-20.); v 40. (-40.) ] (** Mark our [control] points with the debugging helper {{!OSCADml.Debug.show_path2} [Debug.show_path2]} for reference. We don't @@ -25,7 +25,7 @@ let line = Scad.of_mesh @@ Mesh.path_extrude ~path rectangle (** Union our control point [marks] and [line] sweep shapes and output to file. *) -let () = Scad.to_file "spline.scad" (Scad.union [ line; marks ]) +let () = Scad.to_file "spline.scad" (Scad.add line marks) (** {%html:

diff --git a/examples/spline_skinning.ml b/examples/spline_skinning.ml new file mode 100644 index 0000000..555019b --- /dev/null +++ b/examples/spline_skinning.ml @@ -0,0 +1,87 @@ +(** {0 Bézier Sklines} *) + +open OCADml +open OSCADml + +(** {{!OCADml.Mesh.skline} [Mesh.skline]}, like {{!OCADml.Mesh.skin} + [Mesh.skin]} provides a means of generating a mesh that covers a series of + given profiles, but where [skin] linearly transistions between each profile + independently, [skline] splines through each of the paths formed by the + associated vertices from the first profile to the last (or in a loop). + to generate meshes that cover over series of profiles. However, unlike + [skin] only resampling is made available for mapping vertices between + incomensurate profiles as the point duplication methods (tangent and + distance) can easily lead to edge intersections. *) + +let handle_profiles = + let circ = Path3.circle ~fn:64 5. in + let base = Path3.scale (v3 1.2 1.2 1.) circ + and handle = Path3.scale (v3 0.7 0.7 1.) circ in + Path3. + [ ztrans (-3.) base + ; circ + ; translate (v3 15. 0. 20.) (yrot (Float.pi /. 2.) handle) + ; xtrans 30. (yrot Float.pi circ) + ; translate (v3 30. 0. (-3.)) (yrot Float.pi base) + ] + +(** A quick look at the points of our profiles we are about to spline over with + alternating colours may help a bit to conceptualize what we are about to + give {{!OCADml.Mesh.skline} [Mesh.skline]} to work with. *) +let () = + let show i = + let c = if i mod 2 = 0 then Color.Magenta else Color.Aquamarine in + Debug.show_path3 (fun _ -> Scad.(color c @@ sphere 0.2)) + in + List.mapi show handle_profiles |> Scad.union |> Scad.to_file "handle_points.scad" + +(** {%html: +

+ +

%} + *) + +(** Using the [?tangents] parameter of {{!OCADml.Bezier3.of_path} + [Bezier3.of_path]} we can specify the tangents we want for each profile, + rather than leaving them up to the automatically computed derivatives (that + may differ for each edge path tracing between the profiles). Here we + contstrain them to cardinals so we can get a handle that sticks closer to + right angles. *) + +let () = + let up = v3 0. 0. 1. in + let tangents = `Tangents [ up; up; v3 1. 0. 0.; V3.neg up; V3.neg up ] in + Mesh.skline ~fn:200 ~size:(`Flat (`Rel 0.5)) ~tangents handle_profiles + |> Scad.of_mesh + |> Scad.to_file ~incl:true "handle_skline.scad" + +(** {%html: +

+ +

%} + *) + +(** As mentioned above, continuous curvature loops are also possible. Here we + morph cyclically through circular and pentagonal profiles by specifying + [~endcaps:`Loop]. *) + +let () = + let circ = Path3.circle ~fn:64 5. in + let pent = Path3.(circle ~fn:5 5.) in + let profs = + Path3. + [ circ + ; translate (v3 15. 0. 20.) (yrot (Float.pi /. 2.) pent) + ; xtrans 30. (yrot Float.pi circ) + ; translate (v3 15. 0. (-20.)) (yrot (Float.pi *. 1.5) pent) + ] + in + Mesh.skline ~endcaps:`Loop ~fn:200 ~size:(`Flat (`Rel 0.1)) profs + |> Scad.of_mesh + |> Scad.to_file ~incl:true "edgy_loop.scad" + +(** {%html: +

+ +

%} + *)