extrude_custom/
extrude_custom.rs

1// Copyright 2025 Jordan Johnson
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::f64::consts::PI;
5
6use glam::{DVec2, DVec3};
7use lerp::Lerp;
8use lib_curveball::curve::CurveResult;
9use lib_curveball::curve::extrude::path::Path;
10use lib_curveball::curve::extrude::profile::Profile;
11use lib_curveball::curve::extrude::{FrenetFrame, ProfileOrientation, extrude};
12use lib_curveball::map::{
13    entity::SimpleWorldspawn,
14    geometry::Brush,
15    qmap::{QEntity, QMap},
16};
17
18// This example creates a custom profile and path.
19
20fn brushes_to_string(brushes: Vec<Brush>) -> CurveResult<String> {
21    let simple_worldspawn = SimpleWorldspawn::new(brushes);
22    let entity = QEntity::from(simple_worldspawn);
23    let map = QMap::new(vec![entity]).with_tb_neverball_metadata();
24    Ok(String::from(format!("{map}")))
25}
26
27// ==================== MyRevolve path ====================
28// To create a custom path, make a struct that implements the
29// Path trait. Doing so requires you to define two functions
30// that take in t: point(), and frame().
31//
32// extrude() will continually call point() and frame() with
33// the parameter t gradually increasing from 0.0 up to 1.0.
34//
35// frame() is required to use extrude() with
36// ProfileOrientation::FollowPath. If you plan to just use
37// ProfileOrientation::Constant, just have frame() return
38// some placeholder vectors like DVec3::default().
39//
40// To know what the different values of ProfileOrientation
41// do, play around with the Curveball app. It'll be much
42// easier to understand than if I explained it here.
43// ========================================================
44
45#[derive(Debug, Clone)]
46pub struct MyRevolve {
47    radius: f64,
48    start_angle_rad: f64,
49    end_angle_rad: f64,
50}
51
52impl MyRevolve {
53    pub fn new(radius: f64, start_angle: f64, end_angle: f64) -> Self {
54        Self {
55            radius,
56            start_angle_rad: start_angle * PI / 180.0,
57            end_angle_rad: end_angle * PI / 180.0,
58        }
59    }
60}
61
62impl Path for MyRevolve {
63    fn point(&self, t: f64) -> DVec3 {
64        let theta = self.start_angle_rad.lerp(self.end_angle_rad, t);
65        DVec3 {
66            x: self.radius * theta.cos(),
67            y: self.radius * theta.sin(),
68            z: 0.0,
69        }
70    }
71    fn frame(&self, t: f64) -> FrenetFrame {
72        let theta = self.start_angle_rad.lerp(self.end_angle_rad, t);
73        FrenetFrame {
74            tangent: DVec3 {
75                x: -theta.sin(),
76                y: theta.cos(),
77                z: 0.0,
78            },
79            normal: DVec3 {
80                x: -theta.cos(),
81                y: -theta.sin(),
82                z: 0.0,
83            },
84            binormal: DVec3 {
85                x: 0.0,
86                y: 0.0,
87                z: 1.0,
88            },
89        }
90    }
91}
92
93// ==================== MyRectangle profile ====================
94// To create a custom profile, make a struct that implements the
95// Profile trait. Doing so requires you to define one function
96// that takes in t and returns a Vec<DVec2>: profile().
97//
98// extrude() will continually call profile() with
99// the parameter t gradually increasing from 0.0 up to 1.0.
100//
101// Often, your profile will just be a constant shape and will
102// not depend on t, so you can just leave this parameter unused.
103// The example profile below does this.
104//
105// The returned profile must be a convex polygon.
106//
107// You can also create a profile that returns multiple polygons
108// at each step. The Annulus profile in Curveball does this.
109// To do this, implement CompoundProfile instead of Profile.
110// ============================================================
111
112#[derive(Debug, Clone)]
113pub struct MyRectangle {
114    width: f64,
115    height: f64,
116}
117
118impl MyRectangle {
119    pub fn new(width: f64, height: f64) -> MyRectangle {
120        Self { width, height }
121    }
122}
123
124impl Profile for MyRectangle {
125    fn profile(&self, _t: f64) -> Vec<DVec2> {
126        vec![
127            DVec2 {
128                x: self.width / 2.0,
129                y: self.height / 2.0,
130            },
131            DVec2 {
132                x: self.width / 2.0,
133                y: -self.height / 2.0,
134            },
135            DVec2 {
136                x: -self.width / 2.0,
137                y: self.height / 2.0,
138            },
139            DVec2 {
140                x: -self.width / 2.0,
141                y: -self.height / 2.0,
142            },
143        ]
144    }
145}
146
147// ==================== Produce the curve ====================
148
149fn main() {
150    // Create the rectangle profile
151    let width = 32.0;
152    let height = 8.0;
153    let rectangle_profile = MyRectangle::new(width, height);
154
155    // Create the catenary path
156    let radius = 128.0;
157    let start_angle = 0.0;
158    let end_angle = 180.0;
159    let revolve_profile = MyRevolve::new(radius, start_angle, end_angle);
160
161    // Extrude to create the path
162    let num_segments = 12;
163    let profile_orientation = ProfileOrientation::FollowPath;
164    let brushes = extrude(
165        num_segments,
166        &rectangle_profile,
167        &revolve_profile,
168        profile_orientation,
169    )
170    .unwrap();
171
172    // Print the map out
173    let string = brushes_to_string(brushes).unwrap();
174    println!("{}", string);
175}