cranpose-ui-graphics 0.15.0

Geometry, units, paths and render data for Cranpose UI
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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
//! The eye a window's scene is seen from: where it stands among the scene's
//! space and the spaces behind its doorways, which way it looks, and how it
//! walks among them, stopped by solid surfaces and let through doorways.

use std::{
    ops::Range,
    sync::{Arc, Mutex, PoisonError},
};

use super::{
    math::{Mat4, Vec3},
    mesh::Mesh,
    scene::Camera,
};
use crate::{Point, Rect, Size};

/// The direction a space calls down: its y.
const DOWN: Vec3 = Vec3::new(0.0, 1.0, 0.0);

/// How many doorways one step can take an eye through.
const MAX_CROSSINGS: usize = 4;

/// How many times a step pushes the eye out of the solids it overlaps: more
/// than once lets it settle in a corner between two.
const PUSHES: usize = 3;

/// How many pieces a step is walked in at most, each no longer than half
/// the eye's radius below this many, so it cannot pass through a thin wall.
const MAX_PIECES: f32 = 256.0;

/// How far an eye looks up or down at most, in radians from level.
pub const PITCH_LIMIT: f32 = 1.4;

/// Where an eye stands and which way it looks.
#[derive(Clone, Debug, PartialEq)]
pub struct EyePose {
    /// The scene nodes of the doorways the eye walked through from the
    /// window's scene, outermost first: it stands in the space of the last,
    /// or in the window scene's own when there is none.
    pub doorways: Arc<[usize]>,
    /// Where it stands, in the space it stands in.
    pub position: Vec3,
    /// The direction it looks, of length one.
    pub forward: Vec3,
    /// The direction that shows as down, square to `forward`, of length
    /// one.
    pub down: Vec3,
}

impl EyePose {
    /// An eye in the window scene's own space at `position`, turned `yaw`
    /// radians to the right of the space's z and `pitch` up from level.
    pub fn standing(position: Vec3, yaw: f32, pitch: f32) -> Self {
        let mut pose = Self {
            doorways: Arc::from([]),
            position,
            forward: Vec3::new(0.0, 0.0, 1.0),
            down: DOWN,
        };
        pose.turn(yaw, pitch);
        pose
    }

    /// The camera the eye sees through, with `focal_length`, in the space it
    /// stands in.
    pub fn camera(&self, focal_length: f32) -> Camera {
        Camera::look_at(
            self.position,
            self.position + self.forward,
            self.down,
            focal_length,
        )
    }

    /// How far the eye looks up from level, in radians.
    pub fn pitch(&self) -> f32 {
        (-self.forward.dot(DOWN)).clamp(-1.0, 1.0).asin()
    }

    /// Turns the eye `yaw` radians to the right about its space's down, then
    /// `pitch` up, no further than [`PITCH_LIMIT`] from level.
    pub fn turn(&mut self, yaw: f32, pitch: f32) {
        self.forward = rotated(self.forward, DOWN, yaw);
        self.down = rotated(self.down, DOWN, yaw);
        let level = self.pitch();
        let pitch = (level + pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT) - level;
        let right = self.down.cross(self.forward);
        if let Some(right) = right.normalized() {
            self.forward = rotated(self.forward, right, pitch);
            self.down = rotated(self.down, right, pitch);
        }
        self.square_up();
    }

    /// The level directions the eye walks along: ahead, as it looks but
    /// level with its space's floor, and to its right.
    pub fn walking_axes(&self) -> (Vec3, Vec3) {
        let ahead = (self.forward - DOWN * self.forward.dot(DOWN))
            .normalized()
            .unwrap_or(self.forward);
        (ahead, DOWN.cross(ahead))
    }

    /// Keeps `forward` and `down` square to each other and of length one.
    fn square_up(&mut self) {
        let forward = self
            .forward
            .normalized()
            .unwrap_or(Vec3::new(0.0, 0.0, 1.0));
        let down = (self.down - forward * self.down.dot(forward))
            .normalized()
            .unwrap_or(DOWN);
        self.forward = forward;
        self.down = down;
    }
}

/// `vector` turned `angle` radians about the unit `axis`, by the right-hand
/// rule.
fn rotated(vector: Vec3, axis: Vec3, angle: f32) -> Vec3 {
    let (sin, cos) = angle.sin_cos();
    vector * cos + axis.cross(vector) * sin + axis * (axis.dot(vector) * (1.0 - cos))
}

/// Where a window's eye stands among the spaces of its scene, as the scene
/// carries it to the renderer.
#[derive(Clone, Debug, PartialEq)]
pub struct EyePlace {
    /// The scene nodes of the doorways the eye walked through, as
    /// [`EyePose::doorways`] holds them.
    pub doorways: Arc<[usize]>,
    /// Where the renderer leaves the eye's world each time it places the
    /// scene.
    pub world: WorldSink,
}

/// Where the renderer leaves an eye's [`EyeWorld`] each time it places the
/// scene, and where the eye reads it when it walks.
#[derive(Clone, Debug, Default)]
pub struct WorldSink(Arc<Mutex<Arc<EyeWorld>>>);

impl WorldSink {
    /// Builds the world again with `build`, in the storage of the last one
    /// while no walk holds it.
    pub fn rebuild(&self, build: impl FnOnce(&mut EyeWorld)) {
        let mut held = self.0.lock().unwrap_or_else(PoisonError::into_inner);
        if let Some(world) = Arc::get_mut(&mut held) {
            world.clear();
            build(world);
            return;
        }
        let mut world = EyeWorld::default();
        build(&mut world);
        *held = Arc::new(world);
    }

    /// The world the renderer left last.
    pub fn current(&self) -> Arc<EyeWorld> {
        Arc::clone(&self.0.lock().unwrap_or_else(PoisonError::into_inner))
    }
}

impl PartialEq for WorldSink {
    fn eq(&self, other: &Self) -> bool {
        Arc::ptr_eq(&self.0, &other.0)
    }
}

/// What an eye can bump into and walk through around where it stood when
/// the renderer last placed the scene: the space it stood in, the spaces
/// next to it through doorways, their solid surfaces and the doorways
/// between them, all in the coordinates of the space it stood in.
#[derive(Debug, Default)]
pub struct EyeWorld {
    /// The scene node of the window's scene, whose own space an eye stands
    /// in through no doorway.
    window_scene: Option<usize>,
    spaces: Vec<WorldSpace>,
    solids: Vec<WorldSolid>,
    holes: Vec<Rect>,
    doorways: Vec<WorldDoorway>,
}

#[derive(Debug)]
struct WorldSpace {
    doorways: Arc<[usize]>,
    to_base: Mat4,
    from_base: Mat4,
    /// How long a unit of the space is in the world's coordinates.
    scale: f32,
}

#[derive(Debug)]
struct WorldSolid {
    space: usize,
    mesh: Arc<Mesh>,
    transform: Mat4,
    size: Size,
    holes: Range<usize>,
    low: Vec3,
    high: Vec3,
}

#[derive(Debug)]
struct WorldDoorway {
    outer: usize,
    inner: usize,
    /// The doorway's plane, positive in the inner space.
    plane: [f32; 4],
    /// Maps the world onto the unit square the doorway spans.
    to_door: Mat4,
}

impl EyeWorld {
    fn clear(&mut self) {
        self.window_scene = None;
        self.spaces.clear();
        self.solids.clear();
        self.holes.clear();
        self.doorways.clear();
    }

    /// Names `scene` as the scene node of the window's scene, whose own
    /// space is the one an eye stands in through no doorway.
    pub fn set_window_scene(&mut self, scene: usize) {
        self.window_scene = Some(scene);
    }

    /// The doorways an eye walks through from the window's scene into the
    /// space of the scene node `scene`, when the world holds that space: the
    /// space the eye stood in, or one a doorway from it.
    pub fn doorways_to(&self, scene: usize) -> Option<Arc<[usize]>> {
        self.space_of(scene)
            .map(|space| Arc::clone(&space.doorways))
    }

    /// `pose`, of an eye in a space of the world, in the coordinates of the
    /// space of the scene node `scene`; `None` when the world holds either
    /// space not.
    pub fn pose_in(&self, pose: &EyePose, scene: usize) -> Option<EyePose> {
        let from = self
            .spaces
            .iter()
            .find(|space| space.doorways == pose.doorways)?;
        let to = self.space_of(scene)?;
        let map = from.to_base.then(&to.from_base);
        let mut seen = EyePose {
            doorways: Arc::clone(&to.doorways),
            position: map.transform_point(pose.position),
            forward: map.transform_vector(pose.forward),
            down: map.transform_vector(pose.down),
        };
        seen.square_up();
        Some(seen)
    }

    fn space_of(&self, scene: usize) -> Option<&WorldSpace> {
        self.spaces
            .iter()
            .find(|space| match space.doorways.last() {
                Some(&last) => last == scene,
                None => self.window_scene == Some(scene),
            })
    }

    /// Adds the space an eye that walked through `doorways` stands in, whose
    /// coordinates `to_base` maps into the world's, and returns its number.
    pub fn add_space(&mut self, doorways: Arc<[usize]>, to_base: Mat4) -> usize {
        self.spaces.push(WorldSpace {
            doorways,
            from_base: to_base.inverse().unwrap_or(Mat4::IDENTITY),
            scale: to_base.length_scale(),
            to_base,
        });
        self.spaces.len() - 1
    }

    /// Adds a surface of space `space` that an eye cannot walk through:
    /// `mesh` placed by `transform` in the world's coordinates, carrying
    /// content of `size` in which `holes` are doorways the eye passes.
    pub fn add_solid(
        &mut self,
        (space, mesh): (usize, Arc<Mesh>),
        transform: Mat4,
        (size, holes): (Size, impl IntoIterator<Item = Rect>),
    ) {
        let start = self.holes.len();
        self.holes.extend(holes);
        let (low, high) = mesh.bounds_in(&transform);
        self.solids.push(WorldSolid {
            space,
            mesh,
            transform,
            size,
            holes: start..self.holes.len(),
            low,
            high,
        });
    }

    /// Adds the doorway from space `outer` into space `inner` that `door`
    /// spans: the unit square placed in the world's coordinates, with
    /// `inner` lying beyond it along its z.
    pub fn add_doorway(&mut self, (outer, inner): (usize, usize), door: Mat4) {
        let corner = door.transform_point(Vec3::ZERO);
        let normal = door
            .transform_vector(Vec3::new(0.0, 0.0, 1.0))
            .normalized()
            .unwrap_or(Vec3::new(0.0, 0.0, 1.0));
        self.doorways.push(WorldDoorway {
            outer,
            inner,
            plane: [normal.x, normal.y, normal.z, -normal.dot(corner)],
            to_door: door.inverse().unwrap_or(Mat4::IDENTITY),
        });
    }

    /// Where an eye at `from`, a ball of `radius`, ends up when it walks
    /// `step`, both in the units of the space it stands in: through the
    /// doorways the step passes into the spaces behind them, and pushed out
    /// of the solid surfaces of the space it walks in, along which it
    /// slides. Its ball keeps `radius` in the units of each space it walks
    /// into. The step goes in pieces of at most half the radius in the
    /// smallest space, so it cannot pass through a wall. An eye whose space
    /// the world does not hold walks freely.
    pub fn walk(&self, from: &EyePose, step: Vec3, radius: f32) -> EyePose {
        let Some(start) = self
            .spaces
            .iter()
            .position(|space| space.doorways == from.doorways)
        else {
            return EyePose {
                position: from.position + step,
                ..from.clone()
            };
        };
        let base = &self.spaces[start];
        let step = base.to_base.transform_vector(step);
        let smallest = self
            .spaces
            .iter()
            .map(|space| space.scale)
            .fold(f32::INFINITY, f32::min);
        let pieces = (step.length() / (radius * smallest * 0.5).max(f32::EPSILON))
            .ceil()
            .clamp(1.0, MAX_PIECES);
        let piece = step * (1.0 / pieces);
        let mut at = base.to_base.transform_point(from.position);
        let mut space = start;
        for _ in 0..pieces as usize {
            let end = at + piece;
            let mut along = at;
            for _ in 0..MAX_CROSSINGS {
                let crossed = self
                    .doorways
                    .iter()
                    .find_map(|door| door.crossing(space, (along, end)));
                let Some((next, crossing)) = crossed else {
                    break;
                };
                space = next;
                along = crossing;
            }
            let reach = radius * self.spaces[space].scale;
            at = self.kept_behind_doorways(space, self.pushed_out(space, end, reach), reach);
        }
        let target = &self.spaces[space];
        let turn = base.to_base.then(&target.from_base);
        let mut pose = EyePose {
            doorways: Arc::clone(&target.doorways),
            position: target.from_base.transform_point(at),
            forward: turn.transform_vector(from.forward),
            down: turn.transform_vector(from.down),
        };
        pose.square_up();
        pose
    }

    /// `at` kept a ball of `radius` behind the planes of the doorways into
    /// `space`, but where it stands within a doorway's rect: a space lies
    /// behind the doorways it is entered by, and only through them does the
    /// eye leave it.
    fn kept_behind_doorways(&self, space: usize, mut at: Vec3, radius: f32) -> Vec3 {
        for door in self.doorways.iter().filter(|door| door.inner == space) {
            let [x, y, z, offset] = door.plane;
            let normal = Vec3::new(x, y, z);
            let behind = normal.dot(at) + offset;
            let on_door = door.to_door.transform_point(at);
            let within = (0.0..=1.0).contains(&on_door.x) && (0.0..=1.0).contains(&on_door.y);
            if behind < radius && !within {
                at = at + normal * (radius - behind);
            }
        }
        at
    }

    /// `at` pushed out of the solids of `space` it lies within `radius` of.
    fn pushed_out(&self, space: usize, mut at: Vec3, radius: f32) -> Vec3 {
        for _ in 0..PUSHES {
            let mut pushed = false;
            for solid in self.solids.iter().filter(|solid| solid.space == space) {
                if let Some(push) = solid.push(at, radius, &self.holes[solid.holes.clone()]) {
                    at = at + push;
                    pushed = true;
                }
            }
            if !pushed {
                break;
            }
        }
        at
    }
}

impl WorldSolid {
    /// How far to move a ball of `radius` at `at` so it no longer reaches
    /// into the surface; `None` when it does not, or reaches it where a
    /// doorway in `holes` opens.
    fn push(&self, at: Vec3, radius: f32, holes: &[Rect]) -> Option<Vec3> {
        let outside =
            |value: f32, low: f32, high: f32| value < low - radius || value > high + radius;
        if outside(at.x, self.low.x, self.high.x)
            || outside(at.y, self.low.y, self.high.y)
            || outside(at.z, self.low.z, self.high.z)
        {
            return None;
        }
        let contact = self.mesh.nearest(at, &self.transform)?;
        if contact.distance >= radius {
            return None;
        }
        let content = Point::new(
            contact.uv[0] * self.size.width,
            contact.uv[1] * self.size.height,
        );
        if holes.iter().any(|hole| hole.contains(content.x, content.y)) {
            return None;
        }
        let away = (at - contact.point).normalized()?;
        Some(away * (radius - contact.distance))
    }
}

impl WorldDoorway {
    /// The space a step from `from` to `to` in `space` passes into through
    /// this doorway, and where it passes; `None` when it does not.
    fn crossing(&self, space: usize, (from, to): (Vec3, Vec3)) -> Option<(usize, Vec3)> {
        let (next, inward) = if space == self.outer {
            (self.inner, true)
        } else if space == self.inner {
            (self.outer, false)
        } else {
            return None;
        };
        let [x, y, z, offset] = self.plane;
        let side = |point: Vec3| x * point.x + y * point.y + z * point.z + offset;
        let (before, after) = (side(from), side(to));
        let passes = if inward {
            before < 0.0 && after >= 0.0
        } else {
            before >= 0.0 && after < 0.0
        };
        if !passes {
            return None;
        }
        let at = from + (to - from) * (before / (before - after));
        let on_door = self.to_door.transform_point(at);
        ((0.0..=1.0).contains(&on_door.x) && (0.0..=1.0).contains(&on_door.y)).then_some((next, at))
    }
}