Procedural / Extrusion Geometry
Page Info
Audience: Intermediate to Advanced
Prerequisites: comfortable with
RefMeshbasics and transformsTime: 20 minutes
Output: Parametric/extruded organ geometry and low-level control (Level 3 concept page)
PlantGeom supports two complementary geometry workflows:
shared geometry with
RefMesh+Geometry(ref_mesh=..., transformation=...)shared geometry with per-node shape deformation via
PointMappedGeometryper-node procedural geometry with
ExtrudedTubeGeometryand extrusion helpers
If you already know the RefMesh workflow, this page is the extrusion counterpart.
When to Use Which Workflow
Use RefMesh + Geometry when:
many nodes share the same base organ mesh
only node transform differs (scale/rotation/translation)
you want the classic OPF-style "instantiate once, transform many" pattern
Use PointMappedGeometry when:
many nodes share the same base organ topology, but each organ bends differently
you want to keep a reusable
RefMeshwhile warping it with a concrete point maporgan shape is easier to describe from a midrib/guide curve than from rigid transforms alone
Use procedural extrusion when:
each axis/organ needs its own path or profile
geometry is easier to define from centerlines and section evolution
you want to build geometry directly from node parameters
Face Orientation and Normals
A flat lamina_mesh has its upper (adaxial) face toward local +Z. Closed extrusions have outward faces and end caps for convex sections in the local XY plane, consistent scale handedness and regular, unfolded sweeps. Reflections supplied as known affine transforms reverse triangle winding automatically, preserving the authored side of the surface. Ground meshes face upward.
For a custom deformation, declare its handedness when it is known throughout the mesh domain:
blade = lamina_refmesh("oriented_blade"; length=1.0, max_width=0.2)
mirror = OrientedPointMap(p -> (-p[1], p[2], p[3]); orientation=-1)
mirrored_blade = PlantGeom.geometry_to_mesh(PointMappedGeometry(blade, mirror))
(vertices=PlantGeom.nvertices(mirrored_blade), faces=PlantGeom.nelements(mirrored_blade))(vertices = 225, faces = 384)orientation=1 declares an orientation-preserving map; orientation=-1 declares an orientation-reversing map. Arbitrary callables without that declaration retain their face order. A single orientation declaration applies throughout the domain; it does not make a folded or self-intersecting mesh valid.
PlantGeom.apply(transform, ref_mesh) also updates stored normals. Affine transforms use normalized inverse-transpose transport; singular transforms retain a direction where the transformed surface has one and use zero for collapsed normals. Other transforms recompute vertex/face normals from their materialized triangles. Shared reference geometry, texture coordinates and material metadata are preserved. Prototype normalization applies the same normal transport when rescaling its reference mesh.
Point-Mapped Geometry
PointMappedGeometry fills the gap between rigid instancing (Geometry) and fully procedural mesh generation (ExtrudedTubeGeometry): the base organ mesh is still reusable, but each node can carry its own deformation map.
PlantGeom now includes a small midrib-driven lamina toolkit for that workflow:
RationalBezierCurve: NURBS-like weighted Bezier midriblamina_midrib: convenience weighted curve builderLaminaMidribMap: point map that wraps a flat lamina around a midribLaminaTwistRollMap: local lamina torsion and edge-roll mapLaminaAnticlasticWaveMap: anticlastic margin map (one side up, the other down)BiomechanicalBendingTransform: cantilever-style organ bending from Young's moduluscompose_point_maps: compose multiple point maps into onelamina_mesh/lamina_refmesh: reusable flat blade reference mesh
For these cereal point maps, parameters are normalized for a unit blade frame (x ∈ [0, 1], |y| ≤ 0.5). Use lamina_refmesh(...; max_width=1.0) and apply physical width later via node transforms or reconstruction attributes. Note: scaling local z strongly also scales down bend amplitude, because midrib deflection is represented in local z by the point map. To define dimensions before deformation, use with_point_map_frame(...). When used through PointMappedGeometry, the input RefMesh coordinates are normalized to blade space automatically (x → [0,1], centered y) before the point-map pipeline, so framed mappings remain stable across source mesh units.
For cereal-like anticlastic ripples (opposite side displacement), use LaminaAnticlasticWaveMap with:
lateral_strength=0.0vertical_strength=1.0
This creates opposite-sign side displacement in the normal direction (+Y and -Y move in opposite Z directions), not a side-to-side zig-zag outline.
Biomechanical Cantilever Bending
For organs that should bend under gravity without explicit biomass simulation, PlantGeom provides:
final_angle: maximal beam deflection angle (from vertical, radians)local_flexion: local incremental bending contributioncalculate_segment_angles: angle profile along segment boundariesBiomechanicalBendingTransform: fast sampled non-linear transform to apply on meshes
BiomechanicalBendingTransform precomputes centerline/frame samples once, so per-vertex evaluation is interpolation only. You can apply it directly to Geometry nodes with transform_mesh! (or compose it with other transforms).
segments = collect(range(0.0, 1.0; length=8))
angles = calculate_segment_angles(25.0, deg2rad(30.0), 1.0, 0.55, segments)
bend = BiomechanicalBendingTransform(
25.0,
deg2rad(30.0),
1.0,
0.55;
x_min=0.0,
x_max=1.0,
n_samples=96,
)
(angles[1], angles[end], bend(Point(1.0, 0.0, 0.0)))(0.5235987755982988, 3.1365276012323497, [0.0175334011851262, 0.0, -0.9902165353601948])beam_length should use the same unit as your mesh local x axis (unless you explicitly adjust x_min/x_max). By default, the helper equations use length_scale=100.0 to match legacy centimeter-based formulations.
For segmented organs modeled as consecutive MTG nodes (legacy leaflet style), you can write angles directly on each segment node with update_segment_angles!.
function segmented_polyline(segment_positions, angle_deg_values)
boundaries = vcat(segment_positions, 1.0)
pts = Point2f[(0.0, 0.0)]
for i in eachindex(angle_deg_values)
θ = deg2rad(angle_deg_values[i])
ds = boundaries[i + 1] - boundaries[i]
x_prev = pts[end][1]
z_prev = pts[end][2]
push!(pts, Point2f(x_prev + ds * sin(θ), z_prev + ds * cos(θ)))
end
pts
end
mtg = Node(NodeMTG(:/, :Plant, 1, 1))
leaflet_soft = Node(mtg, NodeMTG(:+, :Leaflet, 1, 2))
leaflet_stiff = Node(mtg, NodeMTG(:+, :Leaflet, 2, 2))
segment_positions = [0.0, 0.20, 0.42, 0.66, 0.88]
for (i, pos) in enumerate(segment_positions)
s_soft = Node(leaflet_soft, NodeMTG(:/, :LeafletSegment, i, 3))
s_stiff = Node(leaflet_stiff, NodeMTG(:/, :LeafletSegment, i, 3))
s_soft[:segment_boundaries] = pos
s_stiff[:segment_boundaries] = pos
end
update_segment_angles!(
leaflet_soft,
18.0,
deg2rad(26.0),
1.0,
0.55;
segment_symbol=:LeafletSegment,
position_key=:segment_boundaries,
angle_key=:zenithal_angle,
degrees=true,
)
update_segment_angles!(
leaflet_stiff,
220.0,
deg2rad(26.0),
1.0,
0.55;
segment_symbol=:LeafletSegment,
position_key=:segment_boundaries,
angle_key=:zenithal_angle,
degrees=true,
)
soft_segments = descendants(leaflet_soft, symbol=:LeafletSegment)
stiff_segments = descendants(leaflet_stiff, symbol=:LeafletSegment)
angles_soft = [s[:zenithal_angle] for s in soft_segments]
angles_stiff = [s[:zenithal_angle] for s in stiff_segments]
soft_poly = segmented_polyline(segment_positions, angles_soft)
stiff_poly = segmented_polyline(segment_positions, angles_stiff)
fig = Figure(size=(780, 320))
ax = Axis(fig[1, 1], xlabel="Projected x", ylabel="Projected z", title="Segment-chain bending (soft vs stiff)")
lines!(ax, soft_poly, color=RGBA(0.14, 0.53, 0.26, 0.95), linewidth=4, label="soft (low Young)")
scatter!(ax, soft_poly, color=RGBA(0.14, 0.53, 0.26, 0.95), markersize=8)
lines!(ax, stiff_poly, color=RGBA(0.16, 0.36, 0.70, 0.95), linewidth=4, label="stiff (high Young)")
scatter!(ax, stiff_poly, color=RGBA(0.16, 0.36, 0.70, 0.95), markersize=8)
axislegend(ax, position=:rb)
fig
leaf_ref = lamina_refmesh(
"CerealBlade";
length=1.0,
max_width=1.0,
n_long=28,
n_half=4,
material=RGB(0.22, 0.62, 0.24),
)
base_leaf = PointMappedGeometry(
leaf_ref,
with_point_map_frame(compose_point_maps(
LaminaTwistRollMap(tip_twist_deg=3.0, roll_strength=0.05),
LaminaMidribMap(base_angle_deg=46.0, bend=0.02, tip_drop=0.00),
); length=1.0, width=0.12, z_scale=1.0),
)
steeper_leaf = PointMappedGeometry(
leaf_ref,
with_point_map_frame(compose_point_maps(
LaminaTwistRollMap(
tip_twist_deg=26.0,
roll_strength=0.32,
roll_exponent=1.2,
),
LaminaMidribMap(base_angle_deg=22.0, bend=1.0, tip_drop=1.0),
); length=1.0, width=0.12, z_scale=1.0);
transformation=PlantGeom.compose(
PlantGeom.Translation(0.0, 0.22, 0.0),
),
)
fig = Figure(size=(920, 360))
ax = Axis3(fig[1, 1], title="PointMappedGeometry: cereal leaf angle + bend", aspect = :data)
mesh!(ax, PlantGeom.geometry_to_mesh(base_leaf), color=RGBA(0.30, 0.70, 0.28, 0.95))
mesh!(ax, PlantGeom.geometry_to_mesh(steeper_leaf), color=RGBA(0.12, 0.50, 0.18, 0.95))
fig
The same pattern scales to a small cereal plant:
mtg = Node(NodeMTG(:/, :Plant, 1, 1))
stem = Node(mtg, NodeMTG(:/, :Stem, 1, 2))
stem_path = [
Point(0.0, 0.0, 0.0),
Point(0.0, 0.0, 0.44),
Point(0.0, 0.0, 0.92),
Point(0.0, 0.0, 1.26),
]
stem[:geometry] = ExtrudedTubeGeometry(
stem_path;
n_sides=14,
radius=0.022,
radii=[1.0, 0.90, 0.74, 0.50],
torsion=false,
cap_ends=true,
material=RGB(0.54, 0.76, 0.38),
)
blade_ref = lamina_refmesh(
"CerealBlade";
length=1.0,
max_width=1.0,
n_long=40,
n_half=8,
material=RGB(0.20, 0.60, 0.22),
)
leaf_specs = [
(z=0.20, azimuth_deg=-35.0, insertion_tilt_deg=42.0, base_angle_deg=34.0, bend=0.16, tip_drop=0.04, twist=8.0, roll=0.18, wave_amp=0.008, wave_len=0.23, scale=0.82),
(z=0.56, azimuth_deg=84.0, insertion_tilt_deg=48.0, base_angle_deg=44.0, bend=0.30, tip_drop=0.08, twist=18.0, roll=0.30, wave_amp=0.010, wave_len=0.19, scale=0.96),
(z=0.90, azimuth_deg=208.0, insertion_tilt_deg=54.0, base_angle_deg=54.0, bend=0.46, tip_drop=0.14, twist=34.0, roll=0.44, wave_amp=0.012, wave_len=0.15, scale=1.05),
]
for (i, spec) in enumerate(leaf_specs)
leaf = Node(stem, NodeMTG(:+, :Leaf, i, 2))
point_map = compose_point_maps(
LaminaAnticlasticWaveMap(
amplitude=spec.wave_amp / 0.12,
wavelength=spec.wave_len,
edge_exponent=1.6,
progression_exponent=1.1,
base_damping=5.0,
phase_deg=25.0 * i,
asymmetry=0.10,
lateral_strength=0.0,
vertical_strength=1.0,
),
LaminaTwistRollMap(
tip_twist_deg=spec.twist,
roll_strength=spec.roll,
roll_exponent=1.2,
),
LaminaMidribMap(
base_angle_deg=spec.base_angle_deg,
bend=spec.bend,
tip_drop=spec.tip_drop,
),
)
leaf[:geometry] = PointMappedGeometry(
blade_ref,
with_point_map_frame(
point_map;
length=spec.scale,
width=0.12 * spec.scale,
z_scale=spec.scale,
);
transformation=PlantGeom.compose(
PlantGeom.Translation(0.0, 0.0, spec.z),
PlantGeom.LinearMap(PlantGeom.RotZ(deg2rad(spec.azimuth_deg))),
PlantGeom.LinearMap(PlantGeom.RotMatrix(PlantGeom.AngleAxis(deg2rad(-spec.insertion_tilt_deg), 0.0, 1.0, 0.0))),
),
)
end
terminal_leaf = Node(stem, NodeMTG(:+, :Leaf, length(leaf_specs) + 1, 2))
stem_top_z = stem_path[end][3]
terminal_leaf[:geometry] = PointMappedGeometry(
blade_ref,
with_point_map_frame(compose_point_maps(
LaminaAnticlasticWaveMap(
amplitude=0.008 / 0.12,
wavelength=0.20,
edge_exponent=1.6,
progression_exponent=1.1,
base_damping=5.0,
phase_deg=10.0,
asymmetry=0.05,
lateral_strength=0.0,
vertical_strength=1.0,
),
LaminaTwistRollMap(
tip_twist_deg=10.0,
roll_strength=0.20,
roll_exponent=1.1,
),
LaminaMidribMap(
base_angle_deg=62.0,
bend=0.22,
tip_drop=0.05,
),
); length=0.76, width=0.12 * 0.76, z_scale=0.76);
transformation=PlantGeom.compose(
PlantGeom.Translation(0.0, 0.0, stem_top_z),
PlantGeom.LinearMap(PlantGeom.RotZ(deg2rad(6.0))),
PlantGeom.LinearMap(PlantGeom.RotMatrix(PlantGeom.AngleAxis(deg2rad(-48.0), 0.0, 1.0, 0.0))),
),
)
plantviz(mtg, color=Dict("CerealBlade" => RGB(0.20, 0.60, 0.22), "ExtrudedTube" => RGB(0.54, 0.76, 0.38)))
Cereal Anticlastic Wave (Normal Direction)
This focused comparison isolates the anticlastic effect only: same base blade and bending, with or without LaminaAnticlasticWaveMap.
compare_ref = lamina_refmesh(
"CerealBladeCompare";
length=1.0,
max_width=1.0,
n_long=72,
n_half=14,
material=RGB(0.20, 0.60, 0.22),
)
smooth_leaf = PointMappedGeometry(
compare_ref,
compose_point_maps(
LaminaTwistRollMap(tip_twist_deg=20.0, roll_strength=0.32, roll_exponent=1.15),
LaminaMidribMap(base_angle_deg=34.0, bend=0.56, tip_drop=0.16),
);
transformation=PlantGeom.compose(
PlantGeom.Translation(0.0, -0.20, 0.0),
PlantGeom.LinearMap(Diagonal([1.0, 0.14, 0.14])),
),
)
wavy_leaf = PointMappedGeometry(
compare_ref,
compose_point_maps(
LaminaAnticlasticWaveMap(
amplitude=0.022 / 0.14,
wavelength=0.145,
edge_exponent=1.7,
progression_exponent=1.1,
base_damping=4.5,
phase_deg=18.0,
lateral_strength=0.0,
vertical_strength=1.0,
),
LaminaTwistRollMap(tip_twist_deg=20.0, roll_strength=0.32, roll_exponent=1.15),
LaminaMidribMap(base_angle_deg=34.0, bend=0.56, tip_drop=0.16),
);
transformation=PlantGeom.compose(
PlantGeom.Translation(0.0, 0.20, 0.0),
PlantGeom.LinearMap(Diagonal([1.0, 0.14, 0.14])),
),
)
fig = Figure(size=(1200, 520))
ax = Axis3(
fig[1, 1];
title="Cereal leaf anticlastic wave (top: wavy, bottom: smooth)",
azimuth=1.45,
elevation=0.36,
perspectiveness=0.7,
)
mesh!(ax, PlantGeom.geometry_to_mesh(smooth_leaf), color=RGBA(0.18, 0.58, 0.22, 0.95))
mesh!(ax, PlantGeom.geometry_to_mesh(wavy_leaf), color=RGBA(0.14, 0.50, 0.18, 0.95))
Makie.xlims!(ax, -0.03, 1.05)
Makie.ylims!(ax, -0.33, 0.33)
Makie.zlims!(ax, -0.26, 0.56)
fig
This deforms the blade itself. If you instead want topology-driven component bending across segmented organs, use the AMAP stiffness/orthotropy pipeline from Conventions Reference.
Node-Level Procedural Geometry
ExtrudedTubeGeometry is a procedural node geometry source. It does not point to an existing RefMesh; the local mesh is generated from path/section parameters.
using PlantGeom
using MultiScaleTreeGraph
using Colors
using GeometryBasics
mtg = Node(NodeMTG(:/, :Plant, 1, 1))
axis = Node(mtg, NodeMTG(:/, :Internode, 1, 2))
axis[:geometry] = ExtrudedTubeGeometry(
[
Point(0.0, 0.0, 0.0),
Point(0.3, 0.02, 0.01),
Point(0.7, 0.07, 0.04),
Point(1.1, 0.12, 0.06),
];
n_sides=16,
radius=0.045,
radii=[1.0, 0.9, 0.75, 0.6], # taper
torsion=false,
cap_ends=true,
material=RGB(0.45, 0.35, 0.25),
transformation=PlantGeom.Translation(0.2, 0.0, 0.0),
)Main parameters:
path: centerline pointsn_sides,radius: base circular sectionradii: isotropic scaling along the pathwidths,heights: anisotropic scaling (overrideradiiwhen provided)path_normals,torsion: local frame controlcap_ends: optional end caps for closed circular sectionstransformation: post-extrusion transform
Reusable Extrusion as RefMesh
If many nodes share the same procedural shape, generate it once and wrap it in a RefMesh, then keep using classic Geometry.
leaf_section = leaflet_midrib_profile(; lamina_angle_deg=42.0, scale=0.5)
leaf_path = [Point(0.0, 0.0, 0.0), Point(0.5, 0.0, 0.08), Point(1.0, 0.0, 0.12)]
leaf_ref = extrude_profile_refmesh(
"leaf_extruded",
leaf_section,
leaf_path;
widths=[1.0, 0.8, 0.5],
heights=[1.0, 0.9, 0.7],
torsion=true,
close_section=false,
cap_ends=false,
material=RGB(0.15, 0.55, 0.25),
)You can also build mesh first (extrude_profile_mesh, extrude_tube_mesh) then manually wrap it with RefMesh(...).
Path, Profile, and Lathe Helpers
The helpers below cover the full workflow:
section/profile builders
path interpolation
scalar interpolation
lathe mesh generation
1) Profile helpers
circle_section_profile(n_sides; radius, close_loop=true): circular section points.leaflet_midrib_profile(; lamina_angle_deg, scale): open V-shaped leaflet section.
circle = circle_section_profile(14; radius=0.45, close_loop=true)
leaflet = leaflet_midrib_profile(; lamina_angle_deg=44.0, scale=0.45)
fig = Figure(size=(820, 320))
ax1 = Axis(fig[1, 1], title="circle_section_profile", aspect=DataAspect())
lines!(ax1, [p[1] for p in circle], [p[2] for p in circle], color=:steelblue, linewidth=3)
scatter!(ax1, [p[1] for p in circle], [p[2] for p in circle], color=:steelblue, markersize=7)
ax2 = Axis(fig[1, 2], title="leaflet_midrib_profile", aspect=DataAspect())
lines!(ax2, [p[1] for p in leaflet], [p[2] for p in leaflet], color=:forestgreen, linewidth=3)
scatter!(ax2, [p[1] for p in leaflet], [p[2] for p in leaflet], color=:forestgreen, markersize=9)
fig
2) Path interpolation helpers
extrusion_make_path(n, key_points; key_tangents=nothing): Hermite-style interpolation.extrusion_make_spline(n, key_points): Catmull-Rom spline interpolation.
key_points = [
Point(0.0, 0.0, 0.0),
Point(0.2, 0.1, 0.04),
Point(0.6, 0.14, 0.10),
Point(1.0, 0.0, 0.16),
]
path_hermite = extrusion_make_path(70, key_points)
path_spline = extrusion_make_spline(70, key_points)
fig = Figure(size=(860, 420))
ax = Axis3(fig[1, 1], title="Path helper comparison")
lines!(
ax,
[p[1] for p in path_hermite],
[p[2] for p in path_hermite],
[p[3] for p in path_hermite],
color=:dodgerblue,
linewidth=3,
label="extrusion_make_path",
)
lines!(
ax,
[p[1] for p in path_spline],
[p[2] for p in path_spline],
[p[3] for p in path_spline],
color=:tomato,
linewidth=3,
label="extrusion_make_spline",
)
scatter!(
ax,
[p[1] for p in key_points],
[p[2] for p in key_points],
[p[3] for p in key_points],
color=:black,
markersize=12,
label="key points",
)
axislegend(ax, position=:rb)
fig
3) Scalar interpolation helpers
extrusion_make_interpolation(n, key_values): linear interpolation of key scalar values.extrusion_make_curve(z_keys, r_keys, n): AMAP-like extrema-preserving curve sampling.
key_values = [1.0, 0.92, 0.70, 0.42]
interp = extrusion_make_interpolation(60, key_values)
x_interp = range(0.0, 1.0; length=length(interp))
z_keys = [0.0, 0.22, 0.58, 1.0]
r_keys = [0.35, 0.24, 0.29, 0.10]
z_samples, r_samples = extrusion_make_curve(z_keys, r_keys, 120)
fig = Figure(size=(860, 360))
ax1 = Axis(fig[1, 1], title="extrusion_make_interpolation")
lines!(ax1, x_interp, interp, color=:purple4, linewidth=3)
scatter!(ax1, range(0.0, 1.0; length=length(key_values)), key_values, color=:black, markersize=8)
ax2 = Axis(fig[1, 2], title="extrusion_make_curve")
lines!(ax2, z_samples, r_samples, color=:darkorange3, linewidth=3)
scatter!(ax2, z_keys, r_keys, color=:black, markersize=8)
fig
4) Visual extrusion from profile + path
You can combine helper outputs directly with extrude_profile_mesh.
section = leaflet_midrib_profile(; lamina_angle_deg=40.0, scale=0.35)
path = extrusion_make_spline(70, key_points)
widths = extrusion_make_interpolation(70, [1.0, 0.95, 0.7, 0.45])
heights = extrusion_make_interpolation(70, [1.0, 1.0, 0.85, 0.60])
mesh_leaflet = extrude_profile_mesh(
section,
path;
widths=widths,
heights=heights,
torsion=true,
close_section=false,
cap_ends=false,
)
fig = Figure(size=(860, 420))
ax = Axis3(fig[1, 1], title="extrude_profile_mesh (leaflet-like)")
mesh!(ax, mesh_leaflet, color=RGBA(0.18, 0.58, 0.28, 0.95))
lines!(
ax,
[p[1] for p in path],
[p[2] for p in path],
[p[3] for p in path],
color=:black,
linewidth=2,
)
fig
5) Lathe helpers
There are two ways to build lathe geometry:
lathe_mesh/lathe_refmesh: start from sparse key profile data.lathe_gen_mesh/lathe_gen_refmesh: start from already sampled(z, radius)arrays.
lathe_mesh(...; method=:curve) uses extrusion_make_curve (extrema-preserving), while method=:spline and method=:path use spline/Hermite interpolation.
z_keys_lathe = [0.0, 0.22, 0.58, 1.0]
r_keys_lathe = [0.34, 0.24, 0.28, 0.09]
lathe_curve = lathe_mesh(18, 90, z_keys_lathe, r_keys_lathe; method=:curve, axis=:x, cap_ends=true)
lathe_spline = lathe_mesh(18, 90, z_keys_lathe, r_keys_lathe; method=:spline, axis=:x, cap_ends=true)
fig = Figure(size=(980, 390))
ax1 = Axis3(fig[1, 1], title="lathe_mesh(method=:curve)")
mesh!(ax1, lathe_curve, color=RGBA(0.58, 0.44, 0.30, 0.95))
ax2 = Axis3(fig[1, 2], title="lathe_mesh(method=:spline)")
mesh!(ax2, lathe_spline, color=RGBA(0.30, 0.48, 0.70, 0.95))
fig
z_samples = collect(range(0.0, 1.0; length=80))
r_samples = [0.31 * (1 - z)^0.85 + 0.02 for z in z_samples]
lathe_gen = lathe_gen_mesh(18, z_samples, r_samples; axis=:x, cap_ends=true)
fig = Figure(size=(520, 380))
ax = Axis3(fig[1, 1], title="lathe_gen_mesh (pre-sampled profile)")
mesh!(ax, lathe_gen, color=RGBA(0.46, 0.36, 0.62, 0.95))
fig
Recommended Pattern
Repeated geometry: use cached procedural
RefMeshconstructors.Unique per-node geometry: use
ExtrudedTubeGeometrydirectly on nodes.In both cases, render with the same
plantvizpipeline.