Compare commits
No commits in common. "master" and "qt5" have entirely different histories.
9
.clang-format
Normal file
9
.clang-format
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
BasedOnStyle: LLVM
|
||||
UseTab: Always
|
||||
TabWidth: 4
|
||||
IndentWidth: 4
|
||||
AccessModifierOffset: -4
|
||||
IndentCaseLabels: false
|
||||
ColumnLimit: 0
|
||||
PointerAlignment: Left
|
||||
PackConstructorInitializers: Never
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
/target
|
||||
/build
|
||||
|
|
|
|||
18
CMakeLists.txt
Normal file
18
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
cmake_minimum_required(VERSION 3.28...4.2)
|
||||
|
||||
project(gear-cutter VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets X11Extras)
|
||||
find_program(CARGO cargo REQUIRED)
|
||||
set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/cargo")
|
||||
|
||||
include(cbindgen.cmake)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
add_subdirectory(ui)
|
||||
3109
Cargo.lock
generated
3109
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
|
|
@ -1,13 +1,25 @@
|
|||
[workspace]
|
||||
members = ["ui"]
|
||||
|
||||
[package]
|
||||
name = "gears"
|
||||
name = "gear-cutter"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[profile.dev]
|
||||
panic = 'abort'
|
||||
|
||||
[profile.dev.package."*"]
|
||||
debug = false
|
||||
opt-level = 3
|
||||
|
||||
[profile.test.package."*"]
|
||||
debug = false
|
||||
opt-level = 3
|
||||
|
||||
[dependencies]
|
||||
flo_draw = "0.3.1"
|
||||
futures = "0.3.31"
|
||||
glam = "0.30.5"
|
||||
bytemuck = "1.25.2"
|
||||
glam = { version = "0.30.9", features = ["bytemuck"] }
|
||||
pollster = "0.4.0"
|
||||
wgpu = "27.0.1"
|
||||
winit = "0.30.12"
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
# Gear teeth cutting
|
||||
|
||||
A simple demo showing shaping a bevel gear and a similar involute gear.
|
||||
# Gear Cutter
|
||||
|
|
|
|||
15
cbindgen.cmake
Normal file
15
cbindgen.cmake
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
option(LOCAL_CBINDGEN "Download and build `cbindgen` even if one can be found on the system")
|
||||
|
||||
if(NOT LOCAL_CBINDGEN)
|
||||
find_program(CBINDGEN cbindgen)
|
||||
endif()
|
||||
|
||||
if(NOT CBINDGEN)
|
||||
message(CONFIGURE_LOG "cbindgen not found, downloading...")
|
||||
set(crates "${CMAKE_BINARY_DIR}/crates")
|
||||
file(MAKE_DIRECTORY ${crates})
|
||||
execute_process(COMMAND ${CARGO} install --root ${crates} cbindgen@0.29.4)
|
||||
set(CBINDGEN "${crates}/bin/cbindgen")
|
||||
endif()
|
||||
|
||||
set(CBINDGEN "${CBINDGEN}" CACHE FILEPATH "Path to the cbindgen executable." FORCE)
|
||||
11
format.sh
Executable file
11
format.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
find src/ ui/ \
|
||||
\( \
|
||||
-name '*.cxx' -o \
|
||||
-name '*.hxx' -o \
|
||||
-name '*.c' -o \
|
||||
-name '*.h' \
|
||||
\) \
|
||||
-print \
|
||||
-exec 'clang-format' '-i' '{}' \;
|
||||
147
src/bevel_gear.rs
Normal file
147
src/bevel_gear.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
use std::f32::consts::PI;
|
||||
|
||||
use glam::{Affine3A, Vec2, Vec3, Vec3Swizzles, vec3};
|
||||
|
||||
use crate::{crown_gear::CrownGearDesc, profile::Profile, render::mesh::MeshIndexed};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BevelGearDesc {
|
||||
pub profile: Profile,
|
||||
pub teeth: u32,
|
||||
pub cone_halfangle: f32,
|
||||
}
|
||||
|
||||
impl BevelGearDesc {
|
||||
pub fn cutter(&self) -> CrownGearDesc {
|
||||
CrownGearDesc {
|
||||
profile: Profile {
|
||||
addendum: self.profile.dedendum,
|
||||
dedendum: self.profile.addendum,
|
||||
pressure_angle: self.profile.pressure_angle,
|
||||
},
|
||||
pitch_radius: self.pitch_radius() / self.cone_halfangle.sin(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pitch_radius(&self) -> f32 {
|
||||
0.5 * self.teeth as f32
|
||||
}
|
||||
|
||||
pub fn pitch_distance(&self) -> f32 {
|
||||
self.pitch_radius() / self.cone_halfangle.tan()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BevelGearShape {
|
||||
desc: BevelGearDesc,
|
||||
points: Vec<f32>,
|
||||
}
|
||||
|
||||
impl BevelGearShape {
|
||||
pub fn new_blank(desc: &BevelGearDesc, n_points: usize) -> Self {
|
||||
let r = 0.5 * desc.teeth as f32 + desc.profile.addendum + 0.1;
|
||||
BevelGearShape {
|
||||
desc: *desc,
|
||||
points: vec![r; n_points],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_points(&self) -> impl Iterator<Item = Vec2> {
|
||||
let angle = 2.0 * PI / self.points.len() as f32;
|
||||
self.points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(k, r)| Vec2::from_angle(k as f32 * angle) * r)
|
||||
}
|
||||
|
||||
pub fn cut(&mut self, cutter_tfm: Affine3A) {
|
||||
let cutter = self.desc.cutter();
|
||||
let z_proj = self.desc.pitch_distance();
|
||||
let cutting_line = cutter.to_shape(0.375..0.625, self.points.len());
|
||||
let cutting_line: Vec<_> = cutting_line
|
||||
.into_iter()
|
||||
.map(|pt| {
|
||||
let pt_3d = cutter_tfm.transform_point3(pt);
|
||||
let pt_2d = pt_3d.xy() * (z_proj / (z_proj - pt_3d.z));
|
||||
pt_2d
|
||||
})
|
||||
.collect();
|
||||
let angle = 2.0 * PI / self.points.len() as f32;
|
||||
for (k, pt) in self.points.iter_mut().enumerate() {
|
||||
let dir = Vec2::from_angle(k as f32 * angle);
|
||||
for (&a, &b) in cutting_line.iter().zip(&cutting_line[1..]) {
|
||||
if let Some(intersect) = intersect_ray_segment(dir, (a, b))
|
||||
&& intersect > 0.
|
||||
{
|
||||
*pt = pt.min(intersect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the intersection point of a line (from the origin along `dir`) and a line segment (from `a` to `b`).
|
||||
/// Returns the distance from the origin, in multiples of `dir`.
|
||||
fn intersect_ray_segment(dir: Vec2, (a, b): (Vec2, Vec2)) -> Option<f32> {
|
||||
const EPS: f32 = 1e-4;
|
||||
let den = dir.perp_dot(b - a);
|
||||
if den.abs() < EPS {
|
||||
// parallel or almost parallel
|
||||
return None;
|
||||
}
|
||||
let u = a.perp_dot(dir) / den; // intersection = a + u * (b - a)
|
||||
(0.0..1.0).contains(&u).then(|| a.perp_dot(b - a) / den)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum BevelGearCap {
|
||||
Flat,
|
||||
Conical,
|
||||
Spherical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BevelGearVisual {
|
||||
pub pad_in: f32,
|
||||
pub pad_out: f32,
|
||||
}
|
||||
|
||||
pub fn make_mesh(
|
||||
desc: &BevelGearDesc,
|
||||
visual: &BevelGearVisual,
|
||||
shape: &BevelGearShape,
|
||||
) -> MeshIndexed<Vec3> {
|
||||
let inner_scale = (desc.pitch_distance() - visual.pad_in) / desc.pitch_distance();
|
||||
let outer_scale = (desc.pitch_distance() + visual.pad_out) / desc.pitch_distance();
|
||||
|
||||
let sides = shape.points.len();
|
||||
let mut mesh = MeshIndexed::new();
|
||||
mesh.vertices.reserve_exact(2 * sides + 2);
|
||||
mesh.faces.reserve_exact(4 * sides);
|
||||
for pt in shape.to_points() {
|
||||
let pt = vec3(pt.x, pt.y, 0.);
|
||||
mesh.vertices.extend([
|
||||
outer_scale * pt - visual.pad_out * Vec3::Z,
|
||||
inner_scale * pt + visual.pad_in * Vec3::Z,
|
||||
]);
|
||||
}
|
||||
let caps = mesh.vertices.len();
|
||||
mesh.vertices
|
||||
.extend([vec3(0., 0., -visual.pad_out), vec3(0., 0., visual.pad_in)]);
|
||||
for k in 0..sides {
|
||||
let base1 = 2 * k;
|
||||
let base2 = 2 * ((k + 1) % sides);
|
||||
mesh.faces.extend(
|
||||
[
|
||||
[base1, caps, base2],
|
||||
[base1, base2, base1 + 1],
|
||||
[base2 + 1, base1 + 1, base2],
|
||||
[base2 + 1, caps + 1, base1 + 1],
|
||||
]
|
||||
.map(|face| face.map(|index| u16::try_from(index).unwrap())),
|
||||
);
|
||||
}
|
||||
|
||||
mesh
|
||||
}
|
||||
141
src/camera.rs
Normal file
141
src/camera.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use glam::{Affine3A, Mat3, Mat4, vec3};
|
||||
|
||||
pub(crate) fn rotation(yaw: f32, pitch: f32) -> Mat3 {
|
||||
let (ys, yc) = yaw.sin_cos();
|
||||
let (ps, pc) = pitch.sin_cos();
|
||||
Mat3::from_cols_array_2d(&[[0., 0., -1.], [-1., 0., 0.], [0., 1., 0.]])
|
||||
* Mat3::from_cols_array_2d(&[[pc, 0., -ps], [0., 1., 0.], [ps, 0., pc]])
|
||||
* Mat3::from_cols_array_2d(&[[yc, -ys, 0.], [ys, yc, 0.], [0., 0., 1.]])
|
||||
}
|
||||
|
||||
pub(crate) fn view(yaw: f32, pitch: f32, distance: f32) -> Affine3A {
|
||||
Affine3A::from_mat3_translation(rotation(-yaw, -pitch), vec3(0., 0., -distance))
|
||||
}
|
||||
|
||||
pub(crate) fn projection(fovy: f32, aspect: f32, base_distance: f32) -> Mat4 {
|
||||
Mat4::perspective_rh(fovy, aspect, 0.1 * base_distance, 10.0 * base_distance)
|
||||
}
|
||||
|
||||
pub(crate) struct Camera {
|
||||
pub y_frac: f32,
|
||||
pub y_angle: f32,
|
||||
pub y_range: f32,
|
||||
pub z_min: f32,
|
||||
pub z_max: f32,
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
pub fn clamp_z_min(&mut self, min_z: f32) -> &mut Self {
|
||||
let (s, c) = (0.5 * self.y_angle).sin_cos();
|
||||
// let distance = 0.5 * self.y_len * c / s;
|
||||
let distance_scaled = 0.5 * self.y_range * c;
|
||||
if distance_scaled + self.z_min * s < min_z * s {
|
||||
self.z_min = min_z - distance_scaled / s;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn to_mat4(&self) -> Mat4 {
|
||||
let tan_fov = (0.5 * self.y_angle).tan();
|
||||
let z_range = self.z_max - self.z_min;
|
||||
let y_far = 0.5 * self.y_range + self.z_max * tan_fov;
|
||||
let u = -y_far / z_range;
|
||||
Mat4::from_cols_array_2d(&[
|
||||
[self.y_frac, 0., 0., 0.],
|
||||
[0., 1., 0., 0.],
|
||||
[0., 0., u, -tan_fov],
|
||||
[0., 0., u * self.z_min, 0.5 * self.y_range],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use glam::Vec3;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
#[test]
|
||||
fn test_rotation() {
|
||||
let mut ok = true;
|
||||
for (yaw, pitch, x, y, z) in [
|
||||
(0.0, 0.0, [0., 0., -1.], [-1., 0., 0.], [0., 1., 0.]),
|
||||
(0.5, 0.0, [1., 0., 0.], [0., 0., -1.], [0., 1., 0.]),
|
||||
(0.0, 0.5, [0., -1., 0.], [-1., 0., 0.], [0., 0., -1.]),
|
||||
(0.5, 0.5, [1., 0., 0.], [0., -1., 0.], [0., 0., -1.]),
|
||||
] {
|
||||
let m = rotation(yaw * PI, pitch * PI);
|
||||
if !m.abs_diff_eq(Mat3::from_cols_array_2d(&[x, y, z]), 1e-3) {
|
||||
ok = false;
|
||||
println!("Wrong rotation for {}, {}", 180. * yaw, 180. * pitch);
|
||||
println!("X: expected: {}, actual: {}", Vec3::from_array(x), m.x_axis);
|
||||
println!("Y: expected: {}, actual: {}", Vec3::from_array(y), m.y_axis);
|
||||
println!("Z: expected: {}, actual: {}", Vec3::from_array(z), m.z_axis);
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "was not updated with the LH/RH change"]
|
||||
fn test_view() {
|
||||
let mut ok = true;
|
||||
for (yaw, pitch, x1, y1, z1) in [
|
||||
(0.0, 0.0, [0., 0., 2.], [-1., 0., 3.], [0., 1., 3.]),
|
||||
(0.5, 0.0, [1., 0., 3.], [0., 0., 2.], [0., 1., 3.]),
|
||||
(0.0, 0.5, [0., -1., 3.], [-1., 0., 3.], [0., 0., 2.]),
|
||||
(0.5, 0.5, [1., 0., 3.], [0., -1., 3.], [0., 0., 2.]),
|
||||
] {
|
||||
let m = view(yaw * PI, pitch * PI, 3.);
|
||||
for (label, world, expected) in
|
||||
[('X', Vec3::X, x1), ('Y', Vec3::Y, y1), ('Z', Vec3::Z, z1)]
|
||||
{
|
||||
let expected = Vec3::from_array(expected);
|
||||
let actual = m.transform_point3(world);
|
||||
if !actual.abs_diff_eq(expected, 1e-3) {
|
||||
ok = false;
|
||||
println!(
|
||||
"Wrong transform for {}, {}: for {label}, expected: {expected}, got: {actual}",
|
||||
180. * yaw,
|
||||
180. * pitch
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proj() {
|
||||
for (y_range, y_angle_deg) in [
|
||||
(10.000000, 53.130102f32),
|
||||
(11.547005, 60.000000f32),
|
||||
(20.000000, 90.000000f32),
|
||||
(34.641016, 120.000000f32),
|
||||
(40.000000, 126.869898f32),
|
||||
] {
|
||||
let y_angle = y_angle_deg.to_radians();
|
||||
let camera = Camera {
|
||||
y_frac: 0.5,
|
||||
y_angle,
|
||||
y_range,
|
||||
z_min: -9.0,
|
||||
z_max: 90.0,
|
||||
};
|
||||
let actual = camera.to_mat4();
|
||||
let expected = projection(y_angle, 1.0 / camera.y_frac, 10.0)
|
||||
* Affine3A::from_translation(vec3(0., 0., -10.0));
|
||||
let c = expected.x_axis.x / actual.x_axis.x;
|
||||
assert!(
|
||||
(c * actual).abs_diff_eq(expected, 1e-3),
|
||||
"projection at {y_angle_deg}d is wrong\nexpected: {expected}\nexpected: {}\ncomputed: {actual}",
|
||||
expected / c,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
140
src/crown_gear.rs
Normal file
140
src/crown_gear.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use std::{f32::consts::PI, ops::Range};
|
||||
|
||||
use glam::{Mat3A, Vec3, vec3};
|
||||
|
||||
use crate::{
|
||||
profile::Profile,
|
||||
render::mesh::{MeshIndexed, Vertex},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CrownGearDesc {
|
||||
pub profile: Profile,
|
||||
pub pitch_radius: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CrownGearVisual {
|
||||
pub inner_radius: f32,
|
||||
pub outer_radius: f32,
|
||||
pub base_thickness: f32,
|
||||
}
|
||||
|
||||
impl CrownGearDesc {
|
||||
pub fn teeth(&self) -> f32 {
|
||||
2. * self.pitch_radius
|
||||
}
|
||||
|
||||
pub fn to_shape(&self, part: Range<f32>, sides: usize) -> Vec<Vec3> {
|
||||
let teeth = 2.0 * self.pitch_radius;
|
||||
let mut ret = Vec::new();
|
||||
ret.reserve_exact(sides);
|
||||
for k in 0..sides {
|
||||
let t0 = k as f32 / sides as f32;
|
||||
let t = part.start + t0 * (part.end - part.start);
|
||||
let rot = Mat3A::from_rotation_z(2.0 * PI * t);
|
||||
let tooth = t * teeth;
|
||||
let z = self.profile.y_from_tooth(tooth);
|
||||
ret.push(rot * vec3(self.pitch_radius, 0.0, z));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_mesh_smooth(
|
||||
desc: &CrownGearDesc,
|
||||
visual: &CrownGearVisual,
|
||||
sides: usize,
|
||||
) -> MeshIndexed {
|
||||
let teeth = 2.0 * desc.pitch_radius;
|
||||
let inner_scale = visual.inner_radius / desc.pitch_radius;
|
||||
let outer_scale = visual.outer_radius / desc.pitch_radius;
|
||||
|
||||
let mut mesh = MeshIndexed::new();
|
||||
mesh.vertices.reserve_exact(8 * sides);
|
||||
mesh.faces.reserve_exact(8 * sides);
|
||||
for k in 0..sides {
|
||||
let t = k as f32 / sides as f32;
|
||||
let rot = Mat3A::from_rotation_z(2.0 * PI * t);
|
||||
|
||||
let tooth = t * teeth;
|
||||
let y = desc.profile.y_from_tooth(tooth);
|
||||
|
||||
let n_top = {
|
||||
let eps = 1e-3;
|
||||
let dy_dt = teeth
|
||||
* (desc.profile.y_from_tooth(tooth + eps) - desc.profile.y_from_tooth(tooth - eps))
|
||||
/ (2.0 * eps);
|
||||
vec3(-2.0 * PI * y, -dy_dt, 2.0 * PI * desc.pitch_radius)
|
||||
};
|
||||
|
||||
let vertex = |x, z, n| Vertex {
|
||||
position: rot * vec3(x, 0.0, z),
|
||||
normal: rot * n,
|
||||
};
|
||||
mesh.vertices.extend([
|
||||
// top
|
||||
vertex(visual.outer_radius, outer_scale * y, n_top),
|
||||
vertex(visual.inner_radius, inner_scale * y, n_top),
|
||||
// bottom
|
||||
vertex(visual.inner_radius, -visual.base_thickness, -Vec3::Z),
|
||||
vertex(visual.outer_radius, -visual.base_thickness, -Vec3::Z),
|
||||
// inner
|
||||
vertex(visual.inner_radius, inner_scale * y, -Vec3::X),
|
||||
vertex(visual.inner_radius, -visual.base_thickness, -Vec3::X),
|
||||
// outer
|
||||
vertex(visual.outer_radius, -visual.base_thickness, Vec3::X),
|
||||
vertex(visual.outer_radius, outer_scale * y, Vec3::X),
|
||||
]);
|
||||
|
||||
let base1 = 8 * k;
|
||||
let base2 = 8 * ((k + 1) % sides);
|
||||
for j in 0..4 {
|
||||
let j1 = 2 * j;
|
||||
let j2 = 2 * j + 1;
|
||||
mesh.faces
|
||||
.push([base1 + j1, base2 + j1, base1 + j2].map(|index| index.try_into().unwrap()));
|
||||
mesh.faces
|
||||
.push([base2 + j2, base1 + j2, base2 + j1].map(|index| index.try_into().unwrap()));
|
||||
}
|
||||
}
|
||||
mesh
|
||||
}
|
||||
|
||||
pub fn make_mesh_simple(
|
||||
desc: &CrownGearDesc,
|
||||
visual: &CrownGearVisual,
|
||||
sides: usize,
|
||||
) -> MeshIndexed<Vec3> {
|
||||
let inner_scale = visual.inner_radius / desc.pitch_radius;
|
||||
let outer_scale = visual.outer_radius / desc.pitch_radius;
|
||||
let shape = desc.to_shape(0.0..1.0, sides);
|
||||
assert_eq!(shape.len(), sides);
|
||||
|
||||
let mut mesh = MeshIndexed::new();
|
||||
mesh.vertices.reserve_exact(4 * sides);
|
||||
mesh.faces.reserve_exact(8 * sides);
|
||||
for pt in shape {
|
||||
let inner = inner_scale * pt;
|
||||
let outer = outer_scale * pt;
|
||||
mesh.vertices.extend([
|
||||
inner.with_z(-visual.base_thickness),
|
||||
outer.with_z(-visual.base_thickness),
|
||||
outer,
|
||||
inner,
|
||||
]);
|
||||
}
|
||||
|
||||
for k in 0..sides {
|
||||
let base1 = 4 * k;
|
||||
let base2 = 4 * ((k + 1) % sides);
|
||||
for j1 in 0..4 {
|
||||
let j2 = (j1 + 1) % 4;
|
||||
mesh.faces
|
||||
.push([base1 + j1, base2 + j1, base1 + j2].map(|index| index.try_into().unwrap()));
|
||||
mesh.faces
|
||||
.push([base2 + j2, base1 + j2, base2 + j1].map(|index| index.try_into().unwrap()));
|
||||
}
|
||||
}
|
||||
mesh
|
||||
}
|
||||
343
src/lib.rs
Normal file
343
src/lib.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
f32::consts::{FRAC_PI_2, PI},
|
||||
};
|
||||
|
||||
use glam::{Affine3A, FloatExt, Mat4, Quat, UVec2, Vec3, Vec4, vec3, vec4};
|
||||
|
||||
use crate::render::mesh::GpuParams;
|
||||
|
||||
mod bevel_gear;
|
||||
mod camera;
|
||||
mod crown_gear;
|
||||
mod profile;
|
||||
mod render;
|
||||
|
||||
const BACKGROUND: Vec4 = vec4(0.00, 0.39, 0.52, 1.00);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct ResetArgs {
|
||||
pub tooth: u32,
|
||||
pub cone_halfangle: f32,
|
||||
}
|
||||
|
||||
impl Default for ResetArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tooth: 24,
|
||||
cone_halfangle: std::f32::consts::FRAC_PI_4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct RedrawArgs {
|
||||
pub cam_yaw: f32,
|
||||
pub cam_pitch: f32,
|
||||
pub cam_view_y: f32,
|
||||
pub cam_view_angle: f32,
|
||||
}
|
||||
|
||||
impl Default for RedrawArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cam_yaw: 3. * std::f32::consts::FRAC_PI_4,
|
||||
cam_pitch: std::f32::consts::FRAC_PI_6,
|
||||
cam_view_y: 80.,
|
||||
cam_view_angle: std::f32::consts::FRAC_PI_2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Gpu {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
surface: wgpu::Surface<'static>,
|
||||
}
|
||||
|
||||
pub struct Core {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
surface: wgpu::Surface<'static>,
|
||||
depth: wgpu::Texture,
|
||||
|
||||
mesh_pipeline: render::mesh::Pipeline,
|
||||
|
||||
mesh_cutter: render::mesh::GpuMeshIndexed,
|
||||
|
||||
params_gear: render::mesh::GpuParams,
|
||||
params_cutter: render::mesh::GpuParams,
|
||||
|
||||
bevel_gear_desc: bevel_gear::BevelGearDesc,
|
||||
bevel_gear_shape: bevel_gear::BevelGearShape,
|
||||
crown_gear_desc: crown_gear::CrownGearDesc,
|
||||
t: f32,
|
||||
}
|
||||
|
||||
fn srgb_to_linear(color: Vec3) -> Vec3 {
|
||||
color.map(|x| {
|
||||
if x > 0.04045 {
|
||||
((x + 0.055) / 1.055).powf(2.4)
|
||||
} else {
|
||||
x / 12.92
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn srgba_to_linear(color: Vec4) -> Vec4 {
|
||||
let alpha = color.w;
|
||||
let color = srgb_to_linear(vec3(color.x, color.y, color.z));
|
||||
vec4(color.x, color.y, color.z, alpha)
|
||||
}
|
||||
|
||||
fn color_to_wgpu(color: Vec4) -> wgpu::Color {
|
||||
wgpu::Color {
|
||||
r: color.x.into(),
|
||||
g: color.y.into(),
|
||||
b: color.z.into(),
|
||||
a: color.w.into(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Core {
|
||||
pub fn new(gpu: Gpu, pixel_size: UVec2, args: &ResetArgs) -> Self {
|
||||
let Gpu {
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
} = gpu;
|
||||
|
||||
let depth = Self::create_depth_buffer(&device, pixel_size);
|
||||
let mesh_pipeline = render::mesh::Pipeline::new(&device);
|
||||
|
||||
let bevel_gear_desc = bevel_gear::BevelGearDesc {
|
||||
profile: Default::default(),
|
||||
teeth: args.tooth,
|
||||
cone_halfangle: args.cone_halfangle,
|
||||
};
|
||||
let bevel_gear_shape =
|
||||
bevel_gear::BevelGearShape::new_blank(&bevel_gear_desc, 16 * args.tooth as usize);
|
||||
|
||||
let crown_gear_desc = bevel_gear_desc.cutter();
|
||||
let crown_gear_mesh = crown_gear::make_mesh_simple(
|
||||
&crown_gear_desc,
|
||||
&crown_gear::CrownGearVisual {
|
||||
inner_radius: crown_gear_desc.pitch_radius / 2.,
|
||||
outer_radius: crown_gear_desc.pitch_radius
|
||||
+ 2. * bevel_gear_desc.cone_halfangle.tan(),
|
||||
base_thickness: 2.0,
|
||||
},
|
||||
(16. * crown_gear_desc.teeth()).floor() as usize,
|
||||
)
|
||||
.to_mesh()
|
||||
.to_standard_mesh()
|
||||
.into_indexed();
|
||||
|
||||
let mesh_cutter = render::mesh::GpuMeshIndexed::new(&device, &crown_gear_mesh);
|
||||
|
||||
let params_gear = GpuParams::new(&device, &mesh_pipeline);
|
||||
let params_cutter = GpuParams::new(&device, &mesh_pipeline);
|
||||
|
||||
Self::configure_surface(&surface, &device, pixel_size);
|
||||
|
||||
Self {
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
depth,
|
||||
mesh_pipeline,
|
||||
mesh_cutter,
|
||||
params_gear,
|
||||
params_cutter,
|
||||
bevel_gear_desc,
|
||||
bevel_gear_shape,
|
||||
crown_gear_desc,
|
||||
t: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
fn gear_tfm(&self) -> Affine3A {
|
||||
let turns = -self.t / self.bevel_gear_desc.teeth as f32;
|
||||
Affine3A::from_rotation_translation(Quat::from_rotation_z(2. * PI * turns), Vec3::ZERO)
|
||||
}
|
||||
|
||||
fn cutter_tfm(&self) -> Affine3A {
|
||||
let skip_teeth = self.crown_gear_desc.pitch_radius.floor();
|
||||
let turns = (self.t.fract_gl() + skip_teeth) / self.crown_gear_desc.teeth();
|
||||
Affine3A::from_rotation_translation(
|
||||
Quat::from_rotation_y(FRAC_PI_2 + self.bevel_gear_desc.cone_halfangle)
|
||||
* Quat::from_rotation_z(2. * PI * turns),
|
||||
vec3(0., 0., self.bevel_gear_desc.pitch_distance()),
|
||||
)
|
||||
}
|
||||
|
||||
fn render(&self, output: &wgpu::Texture, args: &RedrawArgs) {
|
||||
let y_frac = {
|
||||
let size = output.size();
|
||||
let w = size.width as f32;
|
||||
let h = size.height as f32;
|
||||
h / w
|
||||
};
|
||||
let proj = camera::Camera {
|
||||
y_frac,
|
||||
y_angle: args.cam_view_angle,
|
||||
y_range: args.cam_view_y,
|
||||
z_min: -0.5 * args.cam_view_y,
|
||||
z_max: 0.5 * args.cam_view_y,
|
||||
}
|
||||
.clamp_z_min(0.01 * args.cam_view_y)
|
||||
.to_mat4();
|
||||
let view = Mat4::from_mat3(camera::rotation(-args.cam_yaw, -args.cam_pitch));
|
||||
let light = vec3(0.7, 0.5, 1.0).normalize();
|
||||
|
||||
self.mesh_pipeline.set_params(
|
||||
&self.queue,
|
||||
render::mesh::SceneParams {
|
||||
projection: proj * view,
|
||||
light,
|
||||
_zero: 0.,
|
||||
},
|
||||
);
|
||||
|
||||
self.params_gear.set(
|
||||
&self.queue,
|
||||
render::mesh::ObjectParams {
|
||||
transform: self.gear_tfm().into(),
|
||||
color: srgba_to_linear(vec4(0.2, 0.8, 0.1, 1.0)),
|
||||
},
|
||||
);
|
||||
self.params_cutter.set(
|
||||
&self.queue,
|
||||
render::mesh::ObjectParams {
|
||||
transform: self.cutter_tfm().into(),
|
||||
color: srgba_to_linear(vec4(0.8, 0.2, 0.1, 1.0)),
|
||||
},
|
||||
);
|
||||
|
||||
let bevel_gear_mesh = bevel_gear::make_mesh(
|
||||
&self.bevel_gear_desc,
|
||||
&bevel_gear::BevelGearVisual {
|
||||
pad_in: 4.,
|
||||
pad_out: 0.,
|
||||
},
|
||||
&self.bevel_gear_shape,
|
||||
)
|
||||
.to_mesh()
|
||||
.to_standard_mesh()
|
||||
.into_indexed();
|
||||
let mesh_gear = render::mesh::GpuMeshIndexed::new(&self.device, &bevel_gear_mesh);
|
||||
|
||||
let view = output.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let depth_view = self
|
||||
.depth
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(color_to_wgpu(srgba_to_linear(BACKGROUND))),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.),
|
||||
store: wgpu::StoreOp::Discard,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
self.mesh_cutter
|
||||
.render(&mut pass, &self.mesh_pipeline, &self.params_cutter);
|
||||
mesh_gear.render(&mut pass, &self.mesh_pipeline, &self.params_gear);
|
||||
drop(pass);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
|
||||
fn create_depth_buffer(device: &wgpu::Device, pixel_size: UVec2) -> wgpu::Texture {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("depth buffer"),
|
||||
size: wgpu::Extent3d {
|
||||
width: pixel_size.x,
|
||||
height: pixel_size.y,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: render::DEPTH_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
fn configure_surface(surface: &wgpu::Surface, device: &wgpu::Device, pixel_size: UVec2) {
|
||||
surface.configure(
|
||||
device,
|
||||
&wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
|
||||
format: render::OUTPUT_FORMAT,
|
||||
width: pixel_size.x,
|
||||
height: pixel_size.y,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Configures the renderer for a given target size.
|
||||
pub fn configure(&mut self, pixel_size: UVec2) {
|
||||
Self::configure_surface(&self.surface, &self.device, pixel_size);
|
||||
self.depth = Self::create_depth_buffer(&self.device, pixel_size);
|
||||
}
|
||||
|
||||
/// Redraws the entire surface.
|
||||
///
|
||||
/// [`Self::configure`] must be called at least once before this.
|
||||
pub fn redraw(&mut self, args: &RedrawArgs) {
|
||||
self.bevel_gear_shape
|
||||
.cut(self.gear_tfm().inverse() * self.cutter_tfm());
|
||||
let output = self.surface.get_current_texture().unwrap();
|
||||
self.render(&output.texture, args);
|
||||
output.present();
|
||||
self.t += 1. / 60.;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init_gpu_inner<E: Error + 'static>(
|
||||
make_surface: impl FnOnce(&wgpu::Instance) -> Result<wgpu::Surface<'static>, E>,
|
||||
) -> Result<Gpu, Box<dyn Error>> {
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::PRIMARY,
|
||||
..Default::default()
|
||||
});
|
||||
let surface = make_surface(&instance)?;
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor::default())
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(Gpu {
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
})
|
||||
}
|
||||
358
src/main.rs
358
src/main.rs
|
|
@ -1,304 +1,80 @@
|
|||
use std::{f32::consts::PI, time::Duration};
|
||||
use std::sync::Arc;
|
||||
|
||||
use flo_draw::{
|
||||
DrawEvent, Key,
|
||||
canvas::{Color::Rgba, GraphicsContext, GraphicsPrimitives},
|
||||
create_drawing_window_with_events, with_2d_graphics,
|
||||
use gear_cutter::{Core, RedrawArgs, ResetArgs, init_gpu_inner};
|
||||
use glam::uvec2;
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||
window::Window,
|
||||
};
|
||||
use glam::{FloatExt, Vec2, mat3, vec2, vec3};
|
||||
|
||||
const fn rad(deg: f32) -> f32 {
|
||||
deg * (PI / 180.0)
|
||||
const TITLE: &str = "Gear Cutter";
|
||||
|
||||
struct MainWindow {
|
||||
window: Arc<Window>,
|
||||
core: Core,
|
||||
}
|
||||
|
||||
const ADDENDUM: f32 = 1.00;
|
||||
const DEDENDUM: f32 = 1.25;
|
||||
const PRESSURE_ANGLE: f32 = rad(20.0);
|
||||
impl MainWindow {
|
||||
fn new(event_loop: &ActiveEventLoop) -> Self {
|
||||
let window = event_loop
|
||||
.create_window(Window::default_attributes().with_title(TITLE))
|
||||
.unwrap();
|
||||
let window = Arc::new(window);
|
||||
let gpu = pollster::block_on(init_gpu_inner(|instance| {
|
||||
instance.create_surface(Arc::clone(&window))
|
||||
}))
|
||||
.unwrap();
|
||||
let core = Core::new(gpu, uvec2(1, 1), &ResetArgs::default());
|
||||
Self { window, core }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Profile {
|
||||
addemdum: f32,
|
||||
dedendum: f32,
|
||||
pressure_angle: f32,
|
||||
fn event(&mut self, event_loop: &ActiveEventLoop, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::Resized(physical_size) => self
|
||||
.core
|
||||
.configure(uvec2(physical_size.width, physical_size.height)),
|
||||
WindowEvent::RedrawRequested => self.core.redraw(&RedrawArgs::default()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rack_shape(x: f32, profile: &Profile) -> f32 {
|
||||
let x = (x / PI).fract_gl() * PI;
|
||||
let a = profile.addemdum * vec2(profile.pressure_angle.tan(), 1.0);
|
||||
let d = profile.dedendum * vec2(profile.pressure_angle.tan(), -1.0);
|
||||
let slope = 1.0 / profile.pressure_angle.tan();
|
||||
if x < d.x {
|
||||
return -x * slope;
|
||||
}
|
||||
if x <= 0.5 * PI - d.x {
|
||||
return d.y;
|
||||
}
|
||||
if x <= 0.5 * PI + a.x {
|
||||
return (x - 0.5 * PI) * slope;
|
||||
}
|
||||
if x <= PI - a.x {
|
||||
return a.y;
|
||||
}
|
||||
return (PI - x) * slope;
|
||||
struct Application {
|
||||
main_window: Option<MainWindow>,
|
||||
}
|
||||
|
||||
const FPS: f32 = 60.0;
|
||||
const TPS: f32 = 1.0;
|
||||
impl Application {
|
||||
fn new() -> Self {
|
||||
Self { main_window: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for Application {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.main_window = Some(MainWindow::new(event_loop));
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: winit::window::WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
let window = self
|
||||
.main_window
|
||||
.as_mut()
|
||||
.expect("window must exist to recieve events");
|
||||
assert_eq!(window.window.id(), window_id);
|
||||
window.event(event_loop, event);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
with_2d_graphics(|| {
|
||||
let title = "Gear shape test";
|
||||
use futures::StreamExt;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
let (canvas, mut events) = create_drawing_window_with_events(title);
|
||||
let mut t = 0.0;
|
||||
|
||||
let n_rack_teeth = 6;
|
||||
let n_crown_teeth = 4;
|
||||
|
||||
let n_teeth = 24;
|
||||
let n_teeth_sample = n_teeth;
|
||||
let r_pitch = 0.5 * n_teeth as f32;
|
||||
|
||||
let n_other = 16;
|
||||
let r_other = 0.5 * n_other as f32;
|
||||
let shaft_angle = 0.5 * PI;
|
||||
|
||||
let r_crown = {
|
||||
let (s, c) = shaft_angle.sin_cos();
|
||||
let pt = vec2(r_other * s, r_pitch + r_other * c);
|
||||
let x = pt.x + pt.y * (c / s);
|
||||
let y = r_pitch;
|
||||
vec2(x, y).length()
|
||||
};
|
||||
|
||||
let crown_angle = (r_pitch / r_crown).asin();
|
||||
dbg!(r_pitch, r_crown, crown_angle * 180.0 / PI);
|
||||
let crown_rot = {
|
||||
let (s, c) = crown_angle.sin_cos();
|
||||
mat3(vec3(1.0, 0.0, 0.0), vec3(0.0, c, s), vec3(0.0, -s, c))
|
||||
};
|
||||
|
||||
let adjust_offset = |off: f32| {
|
||||
let (s, c) = crown_angle.sin_cos();
|
||||
let pitch = vec2(r_crown * c, r_pitch);
|
||||
let offset = off * vec2(s, -c);
|
||||
let pt = pitch + offset;
|
||||
let intersect = pt * (pitch.x / pt.x);
|
||||
r_pitch - intersect.y
|
||||
};
|
||||
let adjust_angle = |angle: f32| (angle.tan() * crown_angle.cos()).atan();
|
||||
let gear_profile = Profile {
|
||||
addemdum: ADDENDUM,
|
||||
dedendum: DEDENDUM,
|
||||
pressure_angle: PRESSURE_ANGLE,
|
||||
};
|
||||
let cutter_profile = Profile {
|
||||
addemdum: gear_profile.dedendum,
|
||||
dedendum: gear_profile.addemdum,
|
||||
pressure_angle: gear_profile.pressure_angle,
|
||||
};
|
||||
let cutter_profile_adj = Profile {
|
||||
addemdum: adjust_offset(cutter_profile.addemdum),
|
||||
dedendum: -adjust_offset(-cutter_profile.dedendum),
|
||||
pressure_angle: adjust_angle(cutter_profile.pressure_angle),
|
||||
};
|
||||
let gear_profile_adj = Profile {
|
||||
addemdum: adjust_offset(gear_profile.addemdum),
|
||||
dedendum: -adjust_offset(-gear_profile.dedendum),
|
||||
pressure_angle: adjust_angle(gear_profile.pressure_angle),
|
||||
};
|
||||
|
||||
let n_points_per_tooth = 100;
|
||||
let n_points = n_points_per_tooth * n_teeth_sample;
|
||||
let mut inv_shape = vec![r_pitch + cutter_profile_adj.dedendum; n_points];
|
||||
let mut bevel_shape = vec![r_pitch + cutter_profile_adj.dedendum; n_points];
|
||||
let mut show_crown_gear = false;
|
||||
let mut show_crown_cutter = true;
|
||||
let mut show_rack_gear = false;
|
||||
let mut show_rack_cutter = true;
|
||||
'main: loop {
|
||||
while let Poll::Ready(event) =
|
||||
events.poll_next_unpin(&mut Context::from_waker(Waker::noop()))
|
||||
{
|
||||
let Some(event) = event else { break 'main };
|
||||
match event {
|
||||
DrawEvent::KeyDown(_, Some(Key::KeyTab)) => {
|
||||
(show_crown_gear, show_crown_cutter) = (show_crown_cutter, show_crown_gear);
|
||||
(show_rack_gear, show_rack_cutter) = (show_rack_cutter, show_rack_gear);
|
||||
}
|
||||
DrawEvent::Closed => break 'main,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
canvas.draw(|gc| {
|
||||
gc.clear_canvas(Rgba(0.1, 0.1, 0.1, 1.0));
|
||||
|
||||
gc.canvas_height(20.0);
|
||||
gc.line_width(0.05);
|
||||
|
||||
gc.stroke_color(Rgba(1.0, 1.0, 1.0, 0.1));
|
||||
gc.move_to(-20.0, 0.0);
|
||||
gc.line_to(20.0, 0.0);
|
||||
gc.move_to(0.0, -20.0);
|
||||
gc.line_to(0.0, 20.0);
|
||||
gc.stroke();
|
||||
|
||||
let gear_offset = t * TPS - (n_teeth_sample as f32 + 5.0);
|
||||
let rack_offset = (t * TPS).fract_gl();
|
||||
|
||||
let make_crown = |profile| {
|
||||
let mut crown_project = Vec::new();
|
||||
for tooth in -n_crown_teeth..n_crown_teeth {
|
||||
for k in 0..n_points_per_tooth {
|
||||
let tooth_frac = k as f32 / n_points_per_tooth as f32;
|
||||
let normal_height_at_r =
|
||||
rack_shape((tooth_frac - rack_offset) * PI, profile);
|
||||
let angle = PI * (tooth as f32 + tooth_frac) / r_crown;
|
||||
let (s, c) = angle.sin_cos();
|
||||
let pos_rel_to_crown =
|
||||
vec3(r_crown * s, normal_height_at_r, r_crown * c);
|
||||
let pos_rel_to_crown_center = crown_rot * pos_rel_to_crown;
|
||||
let pos_rel_to_center = r_crown
|
||||
* crown_angle.cos() * vec2(
|
||||
pos_rel_to_crown_center.x,
|
||||
pos_rel_to_crown_center.y,
|
||||
) / pos_rel_to_crown_center.z;
|
||||
let pos = vec2(pos_rel_to_center.x, pos_rel_to_center.y + r_pitch);
|
||||
crown_project.push(pos);
|
||||
}
|
||||
}
|
||||
crown_project
|
||||
};
|
||||
let crown_gear = make_crown(&gear_profile);
|
||||
let crown_cutter = make_crown(&cutter_profile);
|
||||
|
||||
let rack_y_at_x = |x, profile| rack_shape(x - rack_offset * PI, profile);
|
||||
let inv_cutter_y_at_x = |x| rack_y_at_x(x, &cutter_profile_adj);
|
||||
|
||||
let bevel_cutter: Vec<_> = crown_cutter
|
||||
.iter()
|
||||
.map(|pt| {
|
||||
let rel = vec2(r_pitch - pt.y, pt.x);
|
||||
let r = rel.length();
|
||||
let phi = rel.to_angle();
|
||||
(r, phi)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let kr_to_xy = |k, r: f32| {
|
||||
let u = k as f32 / n_points_per_tooth as f32;
|
||||
let angle = 2.0 * PI * (gear_offset + u) / n_teeth as f32;
|
||||
let (s, c) = angle.sin_cos();
|
||||
vec2(s * r, r_pitch - c * r)
|
||||
};
|
||||
|
||||
for (k, r) in inv_shape.iter_mut().enumerate() {
|
||||
let mut test_r = *r;
|
||||
while test_r > r_pitch - gear_profile_adj.dedendum {
|
||||
let xy = kr_to_xy(k, test_r);
|
||||
let cut_y = inv_cutter_y_at_x(xy.x);
|
||||
if cut_y > xy.y {
|
||||
*r = test_r;
|
||||
}
|
||||
test_r -= 0.01;
|
||||
}
|
||||
}
|
||||
|
||||
for (k, r) in bevel_shape.iter_mut().enumerate() {
|
||||
let u = k as f32 / n_points_per_tooth as f32;
|
||||
let angle =
|
||||
2.0 * PI * (((gear_offset + u) / n_teeth as f32 + 0.5).fract_gl() - 0.5);
|
||||
for (&(mut r1, mut phi1), &(mut r2, mut phi2)) in
|
||||
bevel_cutter.iter().zip(&bevel_cutter[1..])
|
||||
{
|
||||
if phi2 < phi1 {
|
||||
(r1, phi1, r2, phi2) = (r2, phi2, r1, phi1);
|
||||
}
|
||||
if angle < phi1 || angle > phi2 {
|
||||
continue;
|
||||
}
|
||||
if *r < r1 && *r < r2 {
|
||||
continue;
|
||||
}
|
||||
*r = f32::min(r1, r2);
|
||||
}
|
||||
}
|
||||
|
||||
line(
|
||||
gc,
|
||||
inv_shape.iter().enumerate().map(|(k, r)| kr_to_xy(k, *r)),
|
||||
);
|
||||
gc.close_path();
|
||||
gc.stroke_color(Rgba(0.3, 0.3, 0.3, 0.4));
|
||||
gc.stroke();
|
||||
|
||||
gc.stroke_color(Rgba(0.3, 0.3, 0.3, 0.1));
|
||||
gc.circle(0.0, r_pitch, r_pitch - gear_profile_adj.dedendum);
|
||||
gc.circle(0.0, r_pitch, r_pitch);
|
||||
gc.circle(0.0, r_pitch, r_pitch + gear_profile_adj.addemdum);
|
||||
gc.stroke();
|
||||
|
||||
gc.stroke_color(Rgba(1.0, 0.0, 0.0, 0.1));
|
||||
gc.circle(0.0, r_pitch, r_pitch - cutter_profile_adj.addemdum);
|
||||
gc.circle(0.0, r_pitch, r_pitch + cutter_profile_adj.dedendum);
|
||||
gc.stroke();
|
||||
|
||||
line(
|
||||
gc,
|
||||
bevel_shape.iter().enumerate().map(|(k, r)| kr_to_xy(k, *r)),
|
||||
);
|
||||
gc.close_path();
|
||||
gc.stroke_color(Rgba(0.1, 0.6, 1.0, 0.4));
|
||||
gc.stroke();
|
||||
|
||||
let mut pts_rack = Vec::new();
|
||||
let mut pts_cutter = Vec::new();
|
||||
for tooth in -n_rack_teeth..n_rack_teeth {
|
||||
for k in 0..n_points_per_tooth {
|
||||
let x = (tooth as f32 + k as f32 / n_points_per_tooth as f32 + rack_offset)
|
||||
* PI;
|
||||
pts_rack.push(vec2(x, rack_y_at_x(x, &gear_profile_adj)));
|
||||
pts_cutter.push(vec2(x, inv_cutter_y_at_x(x)));
|
||||
}
|
||||
}
|
||||
|
||||
if show_crown_cutter {
|
||||
line(gc, crown_cutter);
|
||||
gc.stroke_color(Rgba(1.0, 0.8, 0.1, 0.4));
|
||||
gc.stroke();
|
||||
}
|
||||
if show_crown_gear {
|
||||
line(gc, crown_gear);
|
||||
gc.stroke_color(Rgba(0.1, 1.0, 0.1, 0.4));
|
||||
gc.stroke();
|
||||
}
|
||||
if show_rack_cutter {
|
||||
line(gc, pts_cutter);
|
||||
gc.stroke_color(Rgba(1.0, 0.1, 0.1, 0.4));
|
||||
gc.stroke();
|
||||
}
|
||||
if show_rack_gear {
|
||||
line(gc, pts_rack);
|
||||
gc.stroke_color(Rgba(0.8, 1.0, 0.1, 0.4));
|
||||
gc.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::sleep(Duration::from_secs_f32(1.0 / FPS));
|
||||
t += 1.0 / FPS;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn line(gc: &mut impl GraphicsContext, pts: impl IntoIterator<Item = Vec2>) {
|
||||
let mut pts = pts.into_iter();
|
||||
let Some(pt) = pts.next() else {
|
||||
return;
|
||||
};
|
||||
gc.move_to(pt.x, pt.y);
|
||||
for pt in pts {
|
||||
gc.line_to(pt.x, pt.y);
|
||||
}
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
let mut app = Application::new();
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
}
|
||||
|
|
|
|||
98
src/profile.rs
Normal file
98
src/profile.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use std::f32::consts::PI;
|
||||
|
||||
use glam::FloatExt;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Profile {
|
||||
pub addendum: f32,
|
||||
pub dedendum: f32,
|
||||
pub pressure_angle: f32,
|
||||
}
|
||||
|
||||
impl Default for Profile {
|
||||
fn default() -> Self {
|
||||
Profile {
|
||||
addendum: 1.0,
|
||||
dedendum: 1.25,
|
||||
pressure_angle: const { 20.0f32.to_radians() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub(crate) const PITCH: f32 = PI;
|
||||
|
||||
pub fn y_from_x(&self, x: f32) -> f32 {
|
||||
self.y_from_tooth(x / Self::PITCH)
|
||||
}
|
||||
|
||||
pub fn y_from_tooth(&self, t: f32) -> f32 {
|
||||
debug_assert!(self.addendum > 0.0);
|
||||
debug_assert!(self.dedendum > 0.0);
|
||||
debug_assert!(self.pressure_angle > 0.0);
|
||||
let t = t.fract_gl();
|
||||
let slope = PI * self.slope();
|
||||
let a = self.addendum / slope;
|
||||
let d = self.dedendum / slope;
|
||||
debug_assert!(2.0 * a + 2.0 * d < 1.0, "pressure angle is too high");
|
||||
if t < a {
|
||||
slope * t
|
||||
} else if t < 0.5 - a {
|
||||
self.addendum
|
||||
} else if t < 0.5 + d {
|
||||
-(t - 0.5) * slope
|
||||
} else if t < 1.0 - d {
|
||||
-self.dedendum
|
||||
} else {
|
||||
(t - 1.0) * slope
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slope(&self) -> f32 {
|
||||
1.0 / self.pressure_angle.tan()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_profile() {
|
||||
let profile = Profile::default();
|
||||
let eps = 0.01;
|
||||
for (t, y) in [
|
||||
(0.0, 0.0),
|
||||
(0.25, profile.addendum),
|
||||
(0.5, 0.0),
|
||||
(0.75, -profile.dedendum),
|
||||
(1.0, 0.0),
|
||||
(1.25, profile.addendum),
|
||||
] {
|
||||
let x = Profile::PITCH * t;
|
||||
let comp_y = profile.y_from_x(x);
|
||||
assert!(
|
||||
(comp_y - y).abs() < eps,
|
||||
"At t={t} (x={x}), expected y={y}, got y={comp_y}"
|
||||
);
|
||||
}
|
||||
for (t, slope) in [
|
||||
(0.0, 1.0),
|
||||
(eps, 1.0),
|
||||
(0.25, 0.0),
|
||||
(0.5, -1.0),
|
||||
(0.75, 0.0),
|
||||
(1.0 - eps, 1.0),
|
||||
(1.0, 1.0),
|
||||
(1.0 + eps, 1.0),
|
||||
] {
|
||||
let x = Profile::PITCH * t;
|
||||
let comp_slope = (profile.y_from_x(x + eps) - profile.y_from_x(x - eps)) / (2.0 * eps);
|
||||
let exp_slope = profile.slope() * slope;
|
||||
assert!(
|
||||
(comp_slope - exp_slope).abs() < eps,
|
||||
"At t={t} (x={x}), expected slope={exp_slope}, got slope={comp_slope}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
233
src/render/mesh.rs
Normal file
233
src/render/mesh.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
use std::mem::offset_of;
|
||||
|
||||
use bytemuck::{Pod, Zeroable, bytes_of, cast_slice};
|
||||
use glam::{Mat4, Vec3, Vec4};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
pub use cpu::*;
|
||||
|
||||
mod cpu;
|
||||
|
||||
static SHADER: &str = include_str!("mesh.wgsl");
|
||||
|
||||
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct Vertex {
|
||||
pub position: Vec3,
|
||||
pub normal: Vec3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct SceneParams {
|
||||
pub projection: Mat4,
|
||||
pub light: Vec3,
|
||||
pub _zero: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct ObjectParams {
|
||||
pub transform: Mat4,
|
||||
pub color: Vec4,
|
||||
}
|
||||
|
||||
pub struct Pipeline {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
params: wgpu::Buffer,
|
||||
bindings: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
pub struct GpuMesh {
|
||||
vertices: wgpu::Buffer,
|
||||
vertex_count: u32,
|
||||
}
|
||||
|
||||
pub struct GpuMeshIndexed {
|
||||
vertices: wgpu::Buffer,
|
||||
indices: wgpu::Buffer,
|
||||
index_count: u32,
|
||||
}
|
||||
|
||||
pub struct GpuParams {
|
||||
params: wgpu::Buffer,
|
||||
bindings: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
impl GpuMesh {
|
||||
pub fn new(device: &wgpu::Device, data: &CpuMesh) -> Self {
|
||||
let vertices = cast_slice::<_, Vertex>(&data.faces);
|
||||
let vertex_count = vertices.len().try_into().expect("too many faces");
|
||||
let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
contents: cast_slice(&vertices),
|
||||
});
|
||||
Self {
|
||||
vertices,
|
||||
vertex_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuMeshIndexed {
|
||||
pub fn new(device: &wgpu::Device, data: &CpuMeshIndexed) -> Self {
|
||||
let vertex_count = data.vertices.len();
|
||||
if vertex_count >= 1 << 16 {
|
||||
panic!("too many vertices");
|
||||
}
|
||||
let indices = cast_slice::<_, u16>(&data.faces);
|
||||
let index_count = indices.len().try_into().expect("too many faces");
|
||||
for index in indices {
|
||||
if *index as usize >= vertex_count {
|
||||
panic!("vertex index out of bounds: {index} out of {vertex_count}");
|
||||
}
|
||||
}
|
||||
let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
contents: cast_slice(&data.vertices),
|
||||
});
|
||||
let indices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
contents: cast_slice(&indices),
|
||||
});
|
||||
Self {
|
||||
vertices,
|
||||
indices,
|
||||
index_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub fn new(device: &wgpu::Device) -> Self {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(SHADER.into()),
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: None,
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
buffers: &[wgpu::VertexBufferLayout {
|
||||
array_stride: size_of::<Vertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
shader_location: 0,
|
||||
offset: offset_of!(Vertex, position) as u64,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
shader_location: 1,
|
||||
offset: offset_of!(Vertex, normal) as u64,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: crate::render::DEPTH_FORMAT,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: None,
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: crate::render::OUTPUT_FORMAT,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
let params = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: size_of::<SceneParams>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let bindings = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &pipeline.get_bind_group_layout(0),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: params.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
Self {
|
||||
params,
|
||||
bindings,
|
||||
pipeline,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_params(&self, queue: &wgpu::Queue, params: SceneParams) {
|
||||
queue.write_buffer(&self.params, 0, bytes_of(¶ms));
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuParams {
|
||||
pub fn new(device: &wgpu::Device, pipeline: &Pipeline) -> Self {
|
||||
let params = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: size_of::<ObjectParams>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let bindings = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &pipeline.pipeline.get_bind_group_layout(1),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: params.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
Self { params, bindings }
|
||||
}
|
||||
|
||||
pub fn set(&self, queue: &wgpu::Queue, params: ObjectParams) {
|
||||
queue.write_buffer(&self.params, 0, bytes_of(¶ms));
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuMesh {
|
||||
pub fn render(&self, pass: &mut wgpu::RenderPass, pipeline: &Pipeline, params: &GpuParams) {
|
||||
pass.set_pipeline(&pipeline.pipeline);
|
||||
pass.set_bind_group(0, &pipeline.bindings, &[]);
|
||||
pass.set_bind_group(1, ¶ms.bindings, &[]);
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.draw(0..self.vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuMeshIndexed {
|
||||
pub fn render(&self, pass: &mut wgpu::RenderPass, pipeline: &Pipeline, params: &GpuParams) {
|
||||
pass.set_pipeline(&pipeline.pipeline);
|
||||
pass.set_bind_group(0, &pipeline.bindings, &[]);
|
||||
pass.set_bind_group(1, ¶ms.bindings, &[]);
|
||||
pass.set_vertex_buffer(0, self.vertices.slice(..));
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint16);
|
||||
pass.draw_indexed(0..self.index_count, 0, 0..1);
|
||||
}
|
||||
}
|
||||
39
src/render/mesh.wgsl
Normal file
39
src/render/mesh.wgsl
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
struct SceneParams {
|
||||
projection: mat4x4f,
|
||||
light: vec3f,
|
||||
_zero: f32,
|
||||
}
|
||||
|
||||
struct ObjectParams {
|
||||
transform: mat4x4f,
|
||||
color: vec4f,
|
||||
}
|
||||
|
||||
struct Vertex {
|
||||
@location(0) position: vec3f,
|
||||
@location(1) normal: vec3f,
|
||||
}
|
||||
|
||||
struct Varying {
|
||||
@builtin(position) screen: vec4f,
|
||||
@location(0) normal: vec3f,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> scene_params: SceneParams;
|
||||
@group(1) @binding(0) var<uniform> object_params: ObjectParams;
|
||||
|
||||
@vertex
|
||||
fn on_vertex(in: Vertex) -> Varying {
|
||||
let position = scene_params.projection * object_params.transform * vec4f(in.position, 1.0);
|
||||
let normal = normalize((object_params.transform * vec4(in.normal, 0.0)).xyz);
|
||||
return Varying(position, normal);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn on_fragment(in: Varying) -> @location(0) vec4f {
|
||||
let normal = normalize(in.normal);
|
||||
let light = dot(scene_params.light, normal);
|
||||
let u = 0.5 + 0.5 * light;
|
||||
let color = vec4f(u * object_params.color.xyz, object_params.color.w);
|
||||
return color;
|
||||
}
|
||||
98
src/render/mesh/cpu.rs
Normal file
98
src/render/mesh/cpu.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use glam::Vec3;
|
||||
|
||||
use super::Vertex;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mesh<T: Copy = Vertex> {
|
||||
pub faces: Vec<[T; 3]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MeshIndexed<T: Copy = Vertex> {
|
||||
pub vertices: Vec<T>,
|
||||
pub faces: Vec<[u16; 3]>,
|
||||
}
|
||||
|
||||
pub type CpuMesh = Mesh<Vertex>;
|
||||
pub type CpuMeshIndexed = MeshIndexed<Vertex>;
|
||||
|
||||
impl<T: Copy> Mesh<T> {
|
||||
pub fn new() -> Self {
|
||||
Self { faces: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn concat(meshes: impl IntoIterator<Item = Self>) -> Self {
|
||||
Self {
|
||||
faces: meshes
|
||||
.into_iter()
|
||||
.map(|mesh| mesh.faces)
|
||||
.flatten()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_indexed(self) -> MeshIndexed<T> {
|
||||
if self.faces.len() >= (1 << 16) / 3 {
|
||||
panic!("too many faces");
|
||||
}
|
||||
let n_faces = self.faces.len() as u16;
|
||||
MeshIndexed {
|
||||
vertices: self.faces.into_iter().flatten().collect(),
|
||||
faces: (0..n_faces)
|
||||
.map(|face| [3 * face, 3 * face + 1, 3 * face + 2])
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mesh<Vec3> {
|
||||
pub fn to_standard_mesh(&self) -> CpuMesh {
|
||||
CpuMesh {
|
||||
faces: self
|
||||
.faces
|
||||
.iter()
|
||||
.map(|face| {
|
||||
let [a, b, c] = face;
|
||||
let normal = (b - a).cross(c - a).normalize();
|
||||
face.map(|position| Vertex { position, normal })
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> MeshIndexed<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vertices: Vec::new(),
|
||||
faces: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn concat(meshes: impl IntoIterator<Item = Self>) -> Self {
|
||||
let mut ret = Self::new();
|
||||
for mesh in meshes {
|
||||
let base = ret.vertices.len() as u16;
|
||||
ret.vertices.extend(mesh.vertices);
|
||||
if ret.vertices.len() > 1 << 16 {
|
||||
panic!("too many vertices");
|
||||
}
|
||||
ret.faces.extend(
|
||||
mesh.faces
|
||||
.into_iter()
|
||||
.map(|face| face.map(|index| base + index)),
|
||||
);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn to_mesh(&self) -> Mesh<T> {
|
||||
Mesh {
|
||||
faces: self
|
||||
.faces
|
||||
.iter()
|
||||
.map(|face| face.map(|index| self.vertices[index as usize]))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/render/mod.rs
Normal file
4
src/render/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod mesh;
|
||||
|
||||
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus;
|
||||
pub const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb;
|
||||
13
ui/CMakeLists.txt
Normal file
13
ui/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
include(impl.cmake)
|
||||
|
||||
add_executable(gear-cutter
|
||||
src/api.cxx
|
||||
src/main.cxx
|
||||
src/main_window.cxx
|
||||
src/main_window.ui
|
||||
src/viewport.cxx
|
||||
)
|
||||
|
||||
target_link_libraries(gear-cutter PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets Qt5::X11Extras X11-xcb)
|
||||
target_link_libraries(gear-cutter PRIVATE gear_cutter_impl)
|
||||
target_include_directories(gear-cutter PRIVATE src)
|
||||
15
ui/Cargo.toml
Normal file
15
ui/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "gear-cutter-impl"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
gear-cutter = {path = "../"}
|
||||
|
||||
glam = { version = "0.30" }
|
||||
pollster = "0.4.0"
|
||||
raw-window-handle = "0.6.2"
|
||||
wgpu = "27.0.1"
|
||||
14
ui/cbindgen.toml
Normal file
14
ui/cbindgen.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
includes = ["types.hxx"]
|
||||
sys_includes = ["cstdint"]
|
||||
pragma_once = true
|
||||
no_includes = true
|
||||
namespace = "ffi"
|
||||
tab_width = 4
|
||||
language = "C++"
|
||||
|
||||
[parse]
|
||||
parse_deps = true
|
||||
include = ["gear-cutter"]
|
||||
|
||||
[export]
|
||||
exclude = ["Box"]
|
||||
45
ui/impl.cmake
Normal file
45
ui/impl.cmake
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# build the Rust side as a static library
|
||||
set(impl_basename "${CARGO_TARGET_DIR}/release/libgear_cutter_impl")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${impl_basename}.a
|
||||
COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} ${CARGO} build --release --package gear-cutter-impl
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
DEPFILE ${impl_basename}.d
|
||||
USES_TERMINAL
|
||||
JOB_SERVER_AWARE TRUE
|
||||
VERBATIM
|
||||
DEPENDS_EXPLICIT_ONLY
|
||||
)
|
||||
|
||||
# HACK ensure CMake *actually adds* the command above
|
||||
add_custom_target(build_impl
|
||||
DEPENDS ${impl_basename}.a
|
||||
)
|
||||
|
||||
add_library(gear_cutter_impl STATIC IMPORTED)
|
||||
|
||||
set_target_properties(gear_cutter_impl PROPERTIES
|
||||
IMPORTED_LOCATION ${impl_basename}.a
|
||||
)
|
||||
|
||||
# generate the bindings
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include")
|
||||
set(bindings_basename "${CMAKE_CURRENT_BINARY_DIR}/include/bindings")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${bindings_basename}.hxx
|
||||
COMMAND ${CBINDGEN} --only-target-dependencies --output "${bindings_basename}.hxx" --depfile "${bindings_basename}.d"
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
DEPFILE ${bindings_basename}.d
|
||||
VERBATIM
|
||||
CODEGEN
|
||||
DEPENDS_EXPLICIT_ONLY
|
||||
)
|
||||
|
||||
target_sources(gear_cutter_impl INTERFACE
|
||||
${bindings_basename}.hxx
|
||||
)
|
||||
target_include_directories(gear_cutter_impl INTERFACE
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/include"
|
||||
)
|
||||
50
ui/src/api.cxx
Normal file
50
ui/src/api.cxx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#include "api.hxx"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
BoxCore::BoxCore(BoxCore&& b)
|
||||
: ptr(std::exchange(b.ptr, {})) {
|
||||
}
|
||||
|
||||
BoxCore::~BoxCore() {
|
||||
reset();
|
||||
}
|
||||
|
||||
BoxCore& BoxCore::operator=(BoxCore&& b) {
|
||||
if (&b == this)
|
||||
return *this;
|
||||
std::swap(ptr, b.ptr);
|
||||
b.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
BoxCore BoxCore::from_xcb(xcb_connection_t* connection, std::uint32_t window, std::uint32_t width, std::uint32_t height, const ResetArgs& args) {
|
||||
if (!connection)
|
||||
throw std::logic_error("attempt to use a null connection");
|
||||
if (!window)
|
||||
throw std::logic_error("attempt to use a null window");
|
||||
BoxCore out;
|
||||
out.ptr.ptr = ffi::rt4_viewport_create(connection, window, width, height, &args);
|
||||
return out;
|
||||
}
|
||||
|
||||
void BoxCore::reset() {
|
||||
auto viewport = std::exchange(ptr, {});
|
||||
if (viewport)
|
||||
ffi::rt4_viewport_destroy(viewport.use());
|
||||
}
|
||||
|
||||
ffi::Core* MutCore::use() const {
|
||||
if (!ptr)
|
||||
throw std::logic_error("attempt to use a null Core");
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void MutCore::configure(std::uint32_t width, std::uint32_t height) const {
|
||||
rt4_viewport_configure(use(), width, height);
|
||||
}
|
||||
|
||||
void MutCore::redraw(const RedrawArgs& args) const {
|
||||
rt4_viewport_redraw(use(), &args);
|
||||
}
|
||||
44
ui/src/api.hxx
Normal file
44
ui/src/api.hxx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "bindings.hxx"
|
||||
|
||||
struct xcb_connection_t;
|
||||
|
||||
using ffi::ResetArgs;
|
||||
using ffi::RedrawArgs;
|
||||
|
||||
class MutCore {
|
||||
friend class BoxCore;
|
||||
|
||||
public:
|
||||
explicit operator bool() const { return ptr; }
|
||||
|
||||
void configure(std::uint32_t width, std::uint32_t height) const;
|
||||
void redraw(const RedrawArgs& args) const;
|
||||
|
||||
private:
|
||||
ffi::Core* ptr = nullptr;
|
||||
ffi::Core* use() const;
|
||||
};
|
||||
|
||||
class BoxCore {
|
||||
public:
|
||||
BoxCore() = default;
|
||||
BoxCore(const BoxCore&) = delete;
|
||||
BoxCore(BoxCore&&);
|
||||
BoxCore& operator=(const BoxCore&) = delete;
|
||||
BoxCore& operator=(BoxCore&&);
|
||||
~BoxCore();
|
||||
|
||||
explicit operator bool() const { return !!ptr; }
|
||||
const MutCore* operator->() const { return &ptr; }
|
||||
|
||||
void reset();
|
||||
|
||||
static BoxCore from_xcb(xcb_connection_t* connection, std::uint32_t window, std::uint32_t width, std::uint32_t height, const ResetArgs& args);
|
||||
|
||||
private:
|
||||
MutCore ptr;
|
||||
};
|
||||
50
ui/src/lib.rs
Normal file
50
ui/src/lib.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use std::{ffi::c_void, num::NonZero, ptr::NonNull};
|
||||
|
||||
use gear_cutter::{Core, RedrawArgs, ResetArgs, init_gpu_inner};
|
||||
use glam::{UVec2, uvec2};
|
||||
use raw_window_handle::{RawDisplayHandle, RawWindowHandle, XcbDisplayHandle, XcbWindowHandle};
|
||||
|
||||
unsafe fn create_viewport(
|
||||
display: impl Into<RawDisplayHandle>,
|
||||
window: impl Into<RawWindowHandle>,
|
||||
size: UVec2,
|
||||
args: &ResetArgs,
|
||||
) -> Box<Core> {
|
||||
let target = wgpu::SurfaceTargetUnsafe::RawHandle {
|
||||
raw_display_handle: display.into(),
|
||||
raw_window_handle: window.into(),
|
||||
};
|
||||
let gpu = pollster::block_on(init_gpu_inner(|instance| unsafe {
|
||||
instance.create_surface_unsafe(target)
|
||||
}))
|
||||
.unwrap();
|
||||
Box::new(Core::new(gpu, size, args))
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
unsafe extern "C" fn rt4_viewport_create(
|
||||
connection: NonNull<c_void>,
|
||||
window: NonZero<u32>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
args: &ResetArgs,
|
||||
) -> Box<Core> {
|
||||
let display = XcbDisplayHandle::new(Some(connection), 0);
|
||||
let window = XcbWindowHandle::new(window);
|
||||
unsafe { create_viewport(display, window, uvec2(width, height), args) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
unsafe extern "C" fn rt4_viewport_destroy(viewport: Box<Core>) {
|
||||
drop(viewport);
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
unsafe extern "C" fn rt4_viewport_configure(viewport: &mut Core, width: u32, height: u32) {
|
||||
viewport.configure(uvec2(width, height));
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
unsafe extern "C" fn rt4_viewport_redraw(viewport: &mut Core, args: &RedrawArgs) {
|
||||
viewport.redraw(args);
|
||||
}
|
||||
15
ui/src/main.cxx
Normal file
15
ui/src/main.cxx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#include "main_window.hxx"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// only X11 is supported so, use XCB unless overriden
|
||||
setenv("QT_QPA_PLATFORM", "xcb", 0);
|
||||
|
||||
QApplication app(argc, argv);
|
||||
|
||||
auto w = new GearCutter;
|
||||
w->show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
52
ui/src/main_window.cxx
Normal file
52
ui/src/main_window.cxx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#include "main_window.hxx"
|
||||
#include "ui_main_window.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QtMath>
|
||||
|
||||
GearCutter::GearCutter(QWidget* parent)
|
||||
: QMainWindow(parent),
|
||||
m_ui(new Ui::MainWindow) {
|
||||
m_ui->setupUi(this);
|
||||
updateParams();
|
||||
updateView();
|
||||
}
|
||||
|
||||
GearCutter::~GearCutter() = default;
|
||||
|
||||
void GearCutter::updateParams() {
|
||||
const std::uint32_t tooth = m_ui->bevTooth->value();
|
||||
const std::uint32_t mate_tooth = m_ui->bevMateTooth->value();
|
||||
const float shaft_angle = qDegreesToRadians((float)m_ui->bevShaftAngle->value());
|
||||
|
||||
const float r_gear = 0.5f * tooth;
|
||||
const float r_mate = 0.5f * mate_tooth;
|
||||
const float s = std::sin(shaft_angle);
|
||||
const float c = std::cos(shaft_angle);
|
||||
const float x_mate = s * r_mate;
|
||||
const float y_mate = r_gear + c * r_mate;
|
||||
const float h_gear = x_mate + y_mate * c / s;
|
||||
const float cone_halfangle = std::atan2(r_gear, h_gear);
|
||||
|
||||
m_ui->bevAngle->setValue(qRadiansToDegrees(cone_halfangle));
|
||||
m_ui->bevMateAngle->setValue(qRadiansToDegrees(shaft_angle - cone_halfangle));
|
||||
|
||||
const ResetArgs args{
|
||||
.tooth = tooth,
|
||||
.cone_halfangle = cone_halfangle,
|
||||
};
|
||||
m_ui->viewport->setParams(args);
|
||||
}
|
||||
|
||||
void GearCutter::updateView() {
|
||||
const RedrawArgs args{
|
||||
.cam_yaw = qDegreesToRadians((float)m_ui->camYaw->value()),
|
||||
.cam_pitch = qDegreesToRadians((float)m_ui->camPitch->value()),
|
||||
.cam_view_y = (float)m_ui->camFoVLin->value(),
|
||||
.cam_view_angle = qDegreesToRadians((float)m_ui->camFoVAng->value()),
|
||||
};
|
||||
m_ui->viewport->setView(args);
|
||||
}
|
||||
|
||||
#include "moc_main_window.cpp"
|
||||
23
ui/src/main_window.hxx
Normal file
23
ui/src/main_window.hxx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
#include <QMainWindow>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
|
||||
class GearCutter : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GearCutter(QWidget* parent = nullptr);
|
||||
~GearCutter() override;
|
||||
|
||||
public slots:
|
||||
void updateParams();
|
||||
void updateView();
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Ui::MainWindow> m_ui;
|
||||
};
|
||||
459
ui/src/main_window.ui
Normal file
459
ui/src/main_window.ui
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1600</width>
|
||||
<height>1200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Gear Cutter</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="Viewport" name="viewport" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1600</width>
|
||||
<height>33</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QDockWidget" name="dockWidget">
|
||||
<property name="features">
|
||||
<set>QDockWidget::DockWidgetFeature::DockWidgetFloatable|QDockWidget::DockWidgetFeature::DockWidgetMovable</set>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Camera</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Yaw</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="camYaw">
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-180.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>180.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>5.000000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>135.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Pitch</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="camPitch">
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-90.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>90.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>5.000000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>30.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Linear size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="camFoVLin">
|
||||
<property name="minimum">
|
||||
<double>10.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>100.000000000000000</double>
|
||||
</property>
|
||||
<property name="stepType">
|
||||
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>80.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Angular size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="camFoVAng">
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>150.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>5.000000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>90.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Bevel gear</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Tooth</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="bevTooth">
|
||||
<property name="minimum">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>500</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Cone half-angle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="bevAngle">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>15.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>90.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>5.000000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>90.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Mate tooth</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="bevMateTooth">
|
||||
<property name="minimum">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>500</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>32</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Mate half-angle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="bevMateAngle">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>90.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>5.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Shaft angle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="bevShaftAngle">
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>15.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>150.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>5.000000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>90.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>385</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>Viewport</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>viewport.hxx</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>camYaw</tabstop>
|
||||
<tabstop>camPitch</tabstop>
|
||||
<tabstop>camFoVLin</tabstop>
|
||||
<tabstop>camFoVAng</tabstop>
|
||||
<tabstop>bevTooth</tabstop>
|
||||
<tabstop>bevAngle</tabstop>
|
||||
<tabstop>bevMateTooth</tabstop>
|
||||
<tabstop>bevMateAngle</tabstop>
|
||||
<tabstop>bevShaftAngle</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>camYaw</sender>
|
||||
<signal>valueChanged(double)</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>updateView()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1507</x>
|
||||
<y>136</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1475</x>
|
||||
<y>183</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>camPitch</sender>
|
||||
<signal>valueChanged(double)</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>updateView()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1507</x>
|
||||
<y>214</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1479</x>
|
||||
<y>280</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>camFoVLin</sender>
|
||||
<signal>valueChanged(double)</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>updateView()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1536</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1478</x>
|
||||
<y>345</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>camFoVAng</sender>
|
||||
<signal>valueChanged(double)</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>updateView()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1541</x>
|
||||
<y>325</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1476</x>
|
||||
<y>432</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>bevTooth</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>updateParams()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1557</x>
|
||||
<y>431</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1464</x>
|
||||
<y>488</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>bevMateTooth</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>updateParams()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1509</x>
|
||||
<y>556</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1462</x>
|
||||
<y>561</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>bevShaftAngle</sender>
|
||||
<signal>valueChanged(double)</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>updateParams()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>1489</x>
|
||||
<y>681</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>1461</x>
|
||||
<y>689</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<slots>
|
||||
<slot>updateView()</slot>
|
||||
<slot>updateParams()</slot>
|
||||
</slots>
|
||||
</ui>
|
||||
12
ui/src/types.hxx
Normal file
12
ui/src/types.hxx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
struct alignas(16) Vec4 {
|
||||
float x, y, z, w;
|
||||
};
|
||||
|
||||
namespace ffi {
|
||||
|
||||
template <typename T>
|
||||
using Box = T*;
|
||||
|
||||
} // namespace ffi
|
||||
84
ui/src/viewport.cxx
Normal file
84
ui/src/viewport.cxx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#include "viewport.hxx"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
#include <QGuiApplication>
|
||||
#include <QX11Info>
|
||||
|
||||
// must be the last as it is macro-heavy
|
||||
#include <X11/Xlib-xcb.h>
|
||||
|
||||
Viewport::Viewport(QWidget* parent, Qt::WindowFlags f)
|
||||
: QWidget(parent, f) {
|
||||
setAttribute(Qt::WA_DontCreateNativeAncestors);
|
||||
setAttribute(Qt::WA_NativeWindow);
|
||||
setAttribute(Qt::WA_PaintOnScreen);
|
||||
setAttribute(Qt::WA_NoSystemBackground);
|
||||
}
|
||||
|
||||
Viewport::~Viewport() = default;
|
||||
|
||||
void Viewport::setParams(ResetArgs new_args) {
|
||||
setup = new_args;
|
||||
recreate();
|
||||
update();
|
||||
}
|
||||
|
||||
void Viewport::setView(RedrawArgs new_args) {
|
||||
args = new_args;
|
||||
update();
|
||||
}
|
||||
|
||||
QPaintEngine* Viewport::paintEngine() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Viewport::event(QEvent* event) {
|
||||
switch (event->type()) {
|
||||
case QEvent::Type::WinIdChange:
|
||||
recreate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
void Viewport::paintEvent(QPaintEvent* event) {
|
||||
if (!core)
|
||||
recreate();
|
||||
core->redraw(args);
|
||||
update();
|
||||
}
|
||||
|
||||
void Viewport::resizeEvent(QResizeEvent* event) {
|
||||
if (!core)
|
||||
return;
|
||||
updateSize();
|
||||
QWidget::resizeEvent(event);
|
||||
}
|
||||
|
||||
void Viewport::recreate() try {
|
||||
auto* xlib_display = QX11Info::display();
|
||||
if (!xlib_display)
|
||||
throw std::runtime_error("got no X11 display");
|
||||
|
||||
auto* xcb_connection = XGetXCBConnection(xlib_display);
|
||||
if (!xcb_connection)
|
||||
throw std::runtime_error("failed to get the XCB connection from the X11 display");
|
||||
|
||||
std::uint32_t x11_window = winId();
|
||||
|
||||
fprintf(stderr, "connection %p, window %#08x\n", xcb_connection, x11_window);
|
||||
|
||||
const QSize device_size = size() * devicePixelRatio();
|
||||
core.reset();
|
||||
core = BoxCore::from_xcb(xcb_connection, x11_window, device_size.width(), device_size.height(), setup);
|
||||
} catch (const std::exception& e) {
|
||||
fprintf(stderr, "failed to recreate the viewport: %s", e.what());
|
||||
}
|
||||
|
||||
void Viewport::updateSize() {
|
||||
const QSize device_size = size() * devicePixelRatio();
|
||||
core->configure(device_size.width(), device_size.height());
|
||||
}
|
||||
30
ui/src/viewport.hxx
Normal file
30
ui/src/viewport.hxx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "api.hxx"
|
||||
|
||||
class Viewport : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Viewport(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
|
||||
~Viewport() override;
|
||||
QPaintEngine* paintEngine() const override;
|
||||
|
||||
void setParams(ResetArgs new_args);
|
||||
void setView(RedrawArgs new_args);
|
||||
|
||||
protected:
|
||||
bool event(QEvent* event) override;
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
private:
|
||||
BoxCore core;
|
||||
ResetArgs setup = {};
|
||||
RedrawArgs args = {};
|
||||
|
||||
void recreate();
|
||||
void updateSize();
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user