hexx 0.24.0

Hexagonal utilities
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
use super::{FaceOptions, InsetOptions, MeshInfo, face::Quad};
use crate::{EdgeDirection, Hex, HexLayout, PlaneMeshBuilder, UVOptions, storage::HexStore};
use glam::{Quat, Vec3};
use std::{ops::RangeInclusive, sync::Arc};

type MapFringeHeightFn = dyn Fn(Hex) -> f32;
type CapOptionsFn = dyn Fn(Hex) -> Option<FaceOptions>;
type SideOptionsFn = dyn Fn(Hex, Hex) -> Option<FaceOptions>;

/// Builder struct to customize hex column heightmap mesh generation.
///
/// # Example
///
/// ```rust
/// # use hexx::*;
/// # use std::collections::HashMap;
///
/// let map: HashMap<Hex, f32> = [
///     (hex(0, 0), 0.0),
///     (hex(1, 0), 2.0),
///     // ...
/// ]
/// .into();
/// let layout = HexLayout::default();
/// let mesh = HeightMapMeshBuilder::new(&layout, &map)
///     .with_offset(Vec3::new(1.2, 3.45, 6.7))
///     .with_height_range(0.0..=5.0)
///     .without_top_face()
///     .build();
/// ```
///
/// # Filling holes
///
/// The builder will iterate through the entire `map`, and check for neighboring
/// heights to construct the vertical side quads. Meaning that there will always
/// be some missing neighbors:
/// * Inside the map if it is *sparse*
/// * On the "edge" of the map if it is *dense*
///
/// By default the builder will simply not generate the quads in those cases,
/// leading to vertical holes (See the `heightmap_builder` example]).
/// To fix this you have two options:
/// * Either define a "default height" ([`Self::with_default_height`]) which the
///   builder will consider to be the height of all those missing neighbors,
///   usually `0.0` or the miminim of your `height_range`.
/// * Provide the real height of those missing neighbors
///   ([`Self::with_fringe_heights`]) which the builder will use. This is
///   typically in the case of a large heightmap which is divided in smaller
///   meshes and you wish for all those meshes to connect seamlessly
///
/// # Notes
///
/// Transform operations (Scale, Rotate, Translate) through the methods
///
/// - Scale: [`Self::with_scale`]
/// - Rotate: [`Self::with_rotation`]
/// - Translate: [`Self::with_offset`]
///
/// Are executed in that order, or **SRT**
pub struct HeightMapMeshBuilder<'l, 'm, HeightMap> {
    /// The hexagonal layout, used to compute vertex positions
    pub layout: &'l HexLayout,
    /// Optional custom range. Otherwise computed from `map` values
    pub height_range: Option<RangeInclusive<f32>>,
    /// Map between the coordinates and the associated height
    pub map: &'m HeightMap,
    /// Top hexagonal face options. If `None` no top faces will be generated
    pub top_face_options: Option<FaceOptions>,
    /// Side quad face options. If `None` no side quads will be generated
    pub side_options: Option<FaceOptions>,
    /// Optional custom offset for the mesh vertex positions
    pub offset: Option<Vec3>,
    /// Optional custom scale factor for the mesh vertex positions
    pub scale: Option<Vec3>,
    /// Optional rotation quaternion, useful to have the mesh already
    /// rotated
    ///
    /// By default the mesh is *facing* up (**Y** axis)
    pub rotation: Option<Quat>,
    /// If set to `true`, the mesh will ignore [`HexLayout::origin`]
    pub center_aligned: bool,
    /// Specifies the height for side quads to be generated at the fringe
    /// of the `map` (Map edge and potential holes in sparse maps).
    ///
    /// This function will be called on direct neighbors of map coordinates
    /// if that neighbor has no associated height in `map`
    pub fringe_heights: Option<Arc<MapFringeHeightFn>>,
    /// Optional function pointer to specify custom [`FaceOptions`] for some
    /// top faces
    pub custom_caps_options: Option<Arc<CapOptionsFn>>,
    /// Optional function pointer to specify custom [`FaceOptions`] for some
    /// side quads.
    pub custom_sides_options: Option<Arc<SideOptionsFn>>,
}

impl<'l, 'm, HeightMap: HexStore<f32>> HeightMapMeshBuilder<'l, 'm, HeightMap> {
    /// Setup a new builder using the given `layout` and height `map`.
    ///
    /// # Arguments
    ///
    /// * `layout` - the associated hexagonal horizontal layout
    /// * `map` - The heightmap values.
    ///
    /// Accepted values for `map` are:
    /// - [`HexagonalMap<f32>`](crate::storage::HexagonalMap)
    /// - [`RombusMap<f32>`](crate::storage::RombusMap)
    /// - [`HashMap<Hex, f32>`](std::collections::HashMap)
    #[must_use]
    pub const fn new(layout: &'l HexLayout, map: &'m HeightMap) -> Self {
        Self {
            layout,
            map,
            height_range: None,
            top_face_options: Some(FaceOptions::new()),
            side_options: Some(FaceOptions::new()),
            offset: None,
            scale: None,
            rotation: None,
            center_aligned: false,
            fringe_heights: None,
            custom_caps_options: None,
            custom_sides_options: None,
        }
    }

    /// Specify a custom height range for the map. This is to be used only
    /// if you are generating only part of the global hashmap and don't want
    /// to rely on the automatic min/max calculation based on `map` values.
    ///
    /// These values are used to remap UV coordinates of side quads.
    ///
    /// # Notes
    /// * It is *heavily* recommended to specify a height range to avoid
    ///   inconsistent UVs
    /// * The range will *not* be checked, if out of range heights are found it
    ///   will have unexpected behaviour on UV calculations
    /// * The range should cover the full height map. If you intend for this
    ///   mesh to be part of a larger heightmap (See
    ///   [`Self::with_fringe_heights`]) then the range should include all
    ///   possible heights in the full map
    #[must_use]
    pub const fn with_height_range(mut self, range: RangeInclusive<f32>) -> Self {
        self.height_range = Some(range);
        self
    }

    /// Specify a custom rotation for the whole mesh
    #[must_use]
    pub const fn with_rotation(mut self, rotation: Quat) -> Self {
        self.rotation = Some(rotation);
        self
    }

    /// Specify a cusom offset for the whole mesh. This can be used to customize
    /// the anchor/pivot of the mesh.
    #[must_use]
    #[inline]
    pub const fn with_offset(mut self, offset: Vec3) -> Self {
        self.offset = Some(offset);
        self
    }

    /// Specify a custom scale factor for the whole mesh
    #[must_use]
    pub const fn with_scale(mut self, scale: Vec3) -> Self {
        self.scale = Some(scale);
        self
    }

    /// The mesh will not include a *top* hexagon face
    #[must_use]
    #[inline]
    pub const fn without_top_face(mut self) -> Self {
        self.top_face_options = None;
        self
    }

    #[must_use]
    #[inline]
    /// Specify global face options for the top cap triangles
    pub const fn with_cap_options(mut self, options: FaceOptions) -> Self {
        self.top_face_options = Some(options);
        self
    }

    #[must_use]
    #[inline]
    /// Specify global uv options for the top cap triangles
    ///
    /// Notes:
    /// * This won't have any effect if [`Self::top_face_options`] is disabled
    ///   by [`Self::without_top_face`]
    /// * This method will override or be overidden by
    ///   [`Self::with_cap_options`]
    pub const fn with_cap_uv_options(mut self, uv_options: UVOptions) -> Self {
        if let Some(opts) = &mut self.top_face_options {
            opts.uv = uv_options;
        }
        self
    }

    /// Specify global insetting option for the top cap face
    ///
    /// Notes:
    /// * This won't have any effect if [`Self::top_face_options`] is disabled
    ///   by [`Self::without_top_face`]
    /// * This method will override or be overidden by
    ///   [`Self::with_cap_options`]
    #[must_use]
    #[inline]
    pub const fn with_cap_inset_options(mut self, inset: InsetOptions) -> Self {
        if let Some(opts) = &mut self.top_face_options {
            opts.insetting = Some(inset);
        }
        self
    }

    /// Specify custom face options for the top cap faces to override the global
    /// `top_face_options` parameters.
    ///
    /// For each coordinate in the heightmap the function will be called. If it
    /// returns a `Some(opts)` then `opts` will be used for that face, otherwise
    /// the global `top_face_options` will be used
    ///
    /// Notes:
    /// * This won't have any effect if [`Self::top_face_options`] is disabled
    ///   by [`Self::without_top_face`]
    #[must_use]
    #[inline]
    pub fn with_custom_cap_options(
        mut self,
        func: impl Fn(Hex) -> Option<FaceOptions> + 'static,
    ) -> Self {
        self.custom_caps_options = Some(Arc::new(func));
        self
    }

    /// The mesh will not include a side faces
    #[must_use]
    #[inline]
    pub const fn without_sides(mut self) -> Self {
        self.side_options = None;
        self
    }

    #[must_use]
    #[inline]
    /// Specify global face options for the column sides
    pub const fn with_side_options(mut self, options: FaceOptions) -> Self {
        self.side_options = Some(options);
        self
    }

    /// Specify custom sides options for the side faces to override the global
    /// `side_options` parameters.
    ///
    /// For each neighboring coordinate pair in the heightmap the function will
    /// be called. If it returns a `Some(opts)` then `opts` will be used for
    /// that face, otherwise the global `side_option` will be used
    ///
    /// Notes:
    /// * This won't have any effect if [`Self::side_options`] is disabled by
    ///   [`Self::without_sides`]
    /// * Each hexagonal pair will be called *twice* but applied only *once*,
    ///   including coordinates at the fringe of the map (See
    ///   [`Self::with_fringe_heights`])
    #[must_use]
    #[inline]
    pub fn with_custom_sides_options(
        mut self,
        func: impl Fn(Hex, Hex) -> Option<FaceOptions> + 'static,
    ) -> Self {
        self.custom_sides_options = Some(Arc::new(func));
        self
    }

    /// Specifies a custom height for *out of bounds* or *missing* columns in
    /// order to generate side quads for columns on the edge of the map or
    /// to fill holes in the heightmap
    ///
    /// This is useful if you have multiple heightmaps which should connect to
    /// each other seamlessly
    ///
    /// # Notes
    ///
    /// * It is *recommended* to also call [`Self::with_height_range`] in this
    ///   case. As the returned fringe heights won't be used in UV remapping,
    ///   leading to inconsistent results
    /// * This method will override or be overidden by
    ///   [`Self::with_default_height`]
    #[must_use]
    #[inline]
    pub fn with_fringe_heights(mut self, func: impl Fn(Hex) -> f32 + 'static) -> Self {
        self.fringe_heights = Some(Arc::new(func));
        self
    }

    /// Specifies a global "default" height for *out of bounds* or missing
    /// columns in order to generate side quads for columns at the fringe of
    /// the map or to fill holes in the heightmap
    ///
    /// This is useful if you have multiple heightmaps which should connect to
    /// each other seamlessly
    ///
    /// # Notes
    ///
    /// * It is *recommended* to also call [`Self::with_height_range`] in this
    ///   case. As this `default_height` won't be used in UV remapping, leading
    ///   to inconsistent results
    /// * This method will override or be overidden by
    ///   [`Self::with_fringe_heights`]
    #[must_use]
    #[inline]
    pub fn with_default_height(mut self, default_height: f32) -> Self {
        self.fringe_heights = Some(Arc::new(move |_| default_height));
        self
    }

    #[must_use]
    #[inline]
    /// Ignores the [`HexLayout::origin`] offset, generating a mesh centered
    /// around `(0.0, 0.0)`.
    pub const fn center_aligned(mut self) -> Self {
        self.center_aligned = true;
        self
    }

    /// Comsumes the builder to return the computed mesh data
    pub fn build(self) -> MeshInfo {
        // We create the final mesh
        let mut mesh = MeshInfo::default();

        let [min, max] = match self.height_range {
            Some(r) => [*r.start(), *r.end()],
            None => [
                self.map.values().copied().reduce(f32::min).unwrap_or(0.0),
                self.map.values().copied().reduce(f32::max).unwrap_or(0.0),
            ],
        };

        for (hex, &height) in self.map.iter() {
            if let Some(opts) = self.top_face_options {
                // Maybe custom options
                let opts = self
                    .custom_caps_options
                    .as_ref()
                    .and_then(|f| f(hex))
                    .unwrap_or(opts);

                let mut plane = PlaneMeshBuilder::new(self.layout)
                    .at(hex)
                    .center_aligned()
                    .with_offset(Vec3::Y * height)
                    .with_uv_options(opts.uv);
                if let Some(inset) = opts.insetting {
                    plane = plane.with_inset_options(inset);
                }
                mesh.merge_with(plane.build());
            }
            if let Some(side_opts) = self.side_options {
                let corners = self.layout.hex_edge_corners(hex);
                for dir in EdgeDirection::ALL_DIRECTIONS {
                    let neighbor = hex + dir;
                    let opt_height = self.map.get(hex + dir).copied();
                    // Maybe custom options
                    let side_opts = self
                        .custom_sides_options
                        .as_ref()
                        .and_then(|f| f(hex, neighbor))
                        .unwrap_or(side_opts);

                    let [a, b] = corners[dir.index() as usize];
                    let Some(neighbor_height) =
                        opt_height.or_else(|| self.fringe_heights.as_ref().map(|f| f(neighbor)))
                    else {
                        continue;
                    };
                    if neighbor_height >= height {
                        continue;
                    }
                    let quad = Quad::new_bounded([a, b], neighbor_height, height, [min, max]);
                    mesh.merge_with(quad.apply_options(&side_opts));
                }
            }
        }
        // **S** - We apply optional scale
        if let Some(scale) = self.scale {
            mesh = mesh.with_scale(scale);
        }
        // **R** - We rotate the mesh to face the given direction
        if let Some(rotation) = self.rotation {
            mesh = mesh.rotated(rotation);
        }
        // **T** - We offset the vertex positions after scaling and rotating
        if let Some(offset) = self.offset {
            mesh = mesh.with_offset(offset);
        }
        if !self.center_aligned {
            mesh = mesh.with_offset(Vec3::new(self.layout.origin.x, 0.0, self.layout.origin.y));
        }
        mesh
    }
}