Compare commits

..

No commits in common. "qt5" and "master" have entirely different histories.
qt5 ... master

32 changed files with 1813 additions and 3847 deletions

View File

@ -1,9 +0,0 @@
BasedOnStyle: LLVM
UseTab: Always
TabWidth: 4
IndentWidth: 4
AccessModifierOffset: -4
IndentCaseLabels: false
ColumnLimit: 0
PointerAlignment: Left
PackConstructorInitializers: Never

1
.gitignore vendored
View File

@ -1,2 +1 @@
/target
/build

View File

@ -1,18 +0,0 @@
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)

3075
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,13 @@
[workspace]
members = ["ui"]
[package]
name = "gear-cutter"
name = "gears"
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]
bytemuck = "1.25.2"
glam = { version = "0.30.9", features = ["bytemuck"] }
pollster = "0.4.0"
wgpu = "27.0.1"
winit = "0.30.12"
flo_draw = "0.3.1"
futures = "0.3.31"
glam = "0.30.5"

View File

@ -1 +1,3 @@
# Gear Cutter
# Gear teeth cutting
A simple demo showing shaping a bevel gear and a similar involute gear.

View File

@ -1,15 +0,0 @@
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)

View File

@ -1,11 +0,0 @@
#!/bin/sh
find src/ ui/ \
\( \
-name '*.cxx' -o \
-name '*.hxx' -o \
-name '*.c' -o \
-name '*.h' \
\) \
-print \
-exec 'clang-format' '-i' '{}' \;

View File

@ -1,147 +0,0 @@
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
}

View File

@ -1,141 +0,0 @@
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,
);
}
}
}

View File

@ -1,140 +0,0 @@
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
}

View File

@ -1,343 +0,0 @@
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,
})
}

View File

@ -1,80 +1,304 @@
use std::sync::Arc;
use std::{f32::consts::PI, time::Duration};
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 flo_draw::{
DrawEvent, Key,
canvas::{Color::Rgba, GraphicsContext, GraphicsPrimitives},
create_drawing_window_with_events, with_2d_graphics,
};
use glam::{FloatExt, Vec2, mat3, vec2, vec3};
const fn rad(deg: f32) -> f32 {
deg * (PI / 180.0)
}
const ADDENDUM: f32 = 1.00;
const DEDENDUM: f32 = 1.25;
const PRESSURE_ANGLE: f32 = rad(20.0);
#[derive(Debug, Clone, Copy)]
struct Profile {
addemdum: f32,
dedendum: f32,
pressure_angle: f32,
}
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;
}
const FPS: f32 = 60.0;
const TPS: f32 = 1.0;
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()
};
const TITLE: &str = "Gear Cutter";
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))
};
struct MainWindow {
window: Arc<Window>,
core: Core,
}
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),
};
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 }
}
fn event(&mut self, event_loop: &ActiveEventLoop, event: WindowEvent) {
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 {
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()),
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));
struct Application {
main_window: Option<MainWindow>,
}
gc.canvas_height(20.0);
gc.line_width(0.05);
impl Application {
fn new() -> Self {
Self { main_window: None }
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;
}
}
impl ApplicationHandler for Application {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.main_window = Some(MainWindow::new(event_loop));
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);
}
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);
if angle < phi1 || angle > phi2 {
continue;
}
if *r < r1 && *r < r2 {
continue;
}
*r = f32::min(r1, r2);
}
}
fn main() {
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();
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);
}
}

View File

@ -1,98 +0,0 @@
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}"
);
}
}
}

View File

@ -1,233 +0,0 @@
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(&params));
}
}
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(&params));
}
}
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, &params.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, &params.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);
}
}

View File

@ -1,39 +0,0 @@
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;
}

View File

@ -1,98 +0,0 @@
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(),
}
}
}

View File

@ -1,4 +0,0 @@
pub mod mesh;
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus;
pub const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb;

View File

@ -1,13 +0,0 @@
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)

View File

@ -1,15 +0,0 @@
[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"

View File

@ -1,14 +0,0 @@
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"]

View File

@ -1,45 +0,0 @@
# 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"
)

View File

@ -1,50 +0,0 @@
#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);
}

View File

@ -1,44 +0,0 @@
#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;
};

View File

@ -1,50 +0,0 @@
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);
}

View File

@ -1,15 +0,0 @@
#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();
}

View File

@ -1,52 +0,0 @@
#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"

View File

@ -1,23 +0,0 @@
#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;
};

View File

@ -1,459 +0,0 @@
<?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>

View File

@ -1,12 +0,0 @@
#pragma once
struct alignas(16) Vec4 {
float x, y, z, w;
};
namespace ffi {
template <typename T>
using Box = T*;
} // namespace ffi

View File

@ -1,84 +0,0 @@
#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());
}

View File

@ -1,30 +0,0 @@
#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();
};