Skip to content

Commit c82ff83

Browse files
tychedeliacatilac
andauthored
Add processing_core crate and webcam support (#87)
Co-authored-by: Moon Davé <moon@softmoon.world>
1 parent 918a021 commit c82ff83

File tree

30 files changed

+1007
-310
lines changed

30 files changed

+1007
-310
lines changed

Cargo.lock

Lines changed: 445 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ workspace = true
1111
default = ["wayland"]
1212
wayland = ["processing_render/wayland"]
1313
x11 = ["processing_render/x11"]
14+
webcam = ["dep:processing_webcam"]
1415

1516
[workspace]
1617
resolver = "3"
@@ -26,14 +27,25 @@ bevy_naga_reflect = { git = "https://github.com/tychedelia/bevy_naga_reflect" }
2627
naga = { version = "28", features = ["wgsl-in"] }
2728
wesl = { version = "0.3", default-features = false }
2829
processing = { path = "." }
30+
processing_core = { path = "crates/processing_core" }
2931
processing_pyo3 = { path = "crates/processing_pyo3" }
3032
processing_render = { path = "crates/processing_render" }
3133
processing_midi = { path = "crates/processing_midi" }
34+
processing_webcam = { path = "crates/processing_webcam" }
3235

3336
[dependencies]
3437
bevy = { workspace = true }
38+
processing_core = { workspace = true }
3539
processing_render = { workspace = true }
3640
processing_midi = { workspace = true }
41+
processing_webcam = { workspace = true, optional = true }
42+
tracing = "0.1"
43+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
44+
45+
[target.'cfg(target_arch = "wasm32")'.dependencies]
46+
wasm-bindgen-futures = "0.4"
47+
js-sys = "0.3"
48+
web-sys = { version = "0.3", features = ["Window"] }
3749

3850
[dev-dependencies]
3951
glfw = "0.60.0"
@@ -89,6 +101,11 @@ path = "examples/midi.rs"
89101
name = "gltf_load"
90102
path = "examples/gltf_load.rs"
91103

104+
[[example]]
105+
name = "webcam"
106+
path = "examples/webcam.rs"
107+
required-features = ["webcam"]
108+
92109
[[example]]
93110
name = "stroke_2d"
94111
path = "examples/stroke_2d.rs"

crates/processing_core/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "processing_core"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lints]
7+
workspace = true
8+
9+
[dependencies]
10+
bevy = { workspace = true }
11+
raw-window-handle = "0.6"
12+
thiserror = "2"
13+
tracing = "0.1"
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
//! Options object for configuring various aspects of libprocessing.
2-
//!
3-
//! To add a new Config just add a new enum with associated value
4-
51
use bevy::prelude::Resource;
62
use std::collections::HashMap;
73

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ pub enum ProcessingError {
3636
UnknownMaterialProperty(String),
3737
#[error("GLTF load error: {0}")]
3838
GltfLoadError(String),
39+
#[error("Webcam not connected")]
40+
WebcamNotConnected,
3941
#[error("Shader compilation error: {0}")]
4042
ShaderCompilationError(String),
4143
#[error("Shader not found")]

crates/processing_core/src/lib.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
pub mod config;
2+
pub mod error;
3+
4+
use std::cell::RefCell;
5+
use std::sync::OnceLock;
6+
7+
use bevy::app::App;
8+
use tracing::debug;
9+
10+
static IS_INIT: OnceLock<()> = OnceLock::new();
11+
12+
thread_local! {
13+
static APP: RefCell<Option<App>> = const { RefCell::new(None) };
14+
}
15+
16+
pub fn app_mut<T>(cb: impl FnOnce(&mut App) -> error::Result<T>) -> error::Result<T> {
17+
let res = APP.with(|app_cell| {
18+
let mut app_borrow = app_cell.borrow_mut();
19+
let app = app_borrow
20+
.as_mut()
21+
.ok_or(error::ProcessingError::AppAccess)?;
22+
cb(app)
23+
})?;
24+
Ok(res)
25+
}
26+
27+
pub fn is_already_init() -> error::Result<bool> {
28+
let is_init = IS_INIT.get().is_some();
29+
let thread_has_app = APP.with(|app_cell| app_cell.borrow().is_some());
30+
if is_init && !thread_has_app {
31+
return Err(error::ProcessingError::AppAccess);
32+
}
33+
if is_init && thread_has_app {
34+
debug!("App already initialized");
35+
return Ok(true);
36+
}
37+
Ok(false)
38+
}
39+
40+
pub fn set_app(app: App) {
41+
APP.with(|app_cell| {
42+
IS_INIT.get_or_init(|| ());
43+
*app_cell.borrow_mut() = Some(app);
44+
});
45+
}
46+
47+
pub fn take_app() -> Option<App> {
48+
APP.with(|app_cell| app_cell.borrow_mut().take())
49+
}

crates/processing_pyo3/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ crate-type = ["cdylib"]
1414
default = ["wayland"]
1515
wayland = ["processing/wayland", "glfw/wayland"]
1616
x11 = ["processing/x11"]
17+
webcam = ["processing/webcam", "dep:processing_webcam"]
1718

1819
[dependencies]
1920
pyo3 = "0.27.0"
2021
processing = { workspace = true }
22+
processing_webcam = { workspace = true, optional = true }
2123
bevy = { workspace = true, features = ["file_watcher"] }
2224
glfw = { version = "0.60.0"}
2325
png = "0.18"

crates/processing_pyo3/src/graphics.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ impl Light {
5959
#[pyclass]
6060
#[derive(Debug)]
6161
pub struct Image {
62-
entity: Entity,
62+
pub(crate) entity: Entity,
63+
}
64+
65+
impl Image {
66+
#[expect(dead_code)] // it's only used by webcam atm
67+
pub(crate) fn from_entity(entity: Entity) -> Self {
68+
Self { entity }
69+
}
6370
}
6471

6572
impl Drop for Image {

crates/processing_pyo3/src/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ mod gltf;
1313
mod graphics;
1414
pub(crate) mod material;
1515
pub(crate) mod shader;
16+
#[cfg(feature = "webcam")]
17+
mod webcam;
1618

1719
use graphics::{Geometry, Graphics, Image, Light, Topology, get_graphics, get_graphics_mut};
1820
use material::Material;
@@ -93,6 +95,12 @@ fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> {
9395
m.add_function(wrap_pyfunction!(emissive, m)?)?;
9496
m.add_function(wrap_pyfunction!(unlit, m)?)?;
9597

98+
#[cfg(feature = "webcam")]
99+
{
100+
m.add_class::<webcam::Webcam>()?;
101+
m.add_function(wrap_pyfunction!(create_webcam, m)?)?;
102+
}
103+
96104
Ok(())
97105
}
98106

@@ -570,3 +578,14 @@ fn emissive(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult
570578
fn unlit(module: &Bound<'_, PyModule>) -> PyResult<()> {
571579
graphics!(module).unlit()
572580
}
581+
582+
#[cfg(feature = "webcam")]
583+
#[pyfunction]
584+
#[pyo3(signature = (width=None, height=None, framerate=None))]
585+
fn create_webcam(
586+
width: Option<u32>,
587+
height: Option<u32>,
588+
framerate: Option<u32>,
589+
) -> PyResult<webcam::Webcam> {
590+
webcam::Webcam::new(width, height, framerate)
591+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use bevy::prelude::Entity;
2+
use processing_webcam::{
3+
WebcamFormat, webcam_create, webcam_create_with_format, webcam_destroy, webcam_image,
4+
webcam_is_connected, webcam_resolution,
5+
};
6+
use pyo3::{exceptions::PyRuntimeError, prelude::*};
7+
8+
use crate::graphics::Image;
9+
10+
#[pyclass(unsendable)]
11+
pub struct Webcam {
12+
entity: Entity,
13+
}
14+
15+
#[pymethods]
16+
impl Webcam {
17+
#[new]
18+
#[pyo3(signature = (width=None, height=None, framerate=None))]
19+
pub fn new(width: Option<u32>, height: Option<u32>, framerate: Option<u32>) -> PyResult<Self> {
20+
let entity = match (width, height, framerate) {
21+
(Some(w), Some(h), Some(fps)) => webcam_create_with_format(WebcamFormat::Exact {
22+
resolution: bevy::math::UVec2::new(w, h),
23+
framerate: fps,
24+
}),
25+
(Some(w), Some(h), None) => {
26+
webcam_create_with_format(WebcamFormat::Resolution(bevy::math::UVec2::new(w, h)))
27+
}
28+
(None, None, Some(fps)) => webcam_create_with_format(WebcamFormat::FrameRate(fps)),
29+
_ => webcam_create(),
30+
}
31+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
32+
33+
Ok(Self { entity })
34+
}
35+
36+
pub fn is_connected(&self) -> PyResult<bool> {
37+
webcam_is_connected(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
38+
}
39+
40+
pub fn resolution(&self) -> PyResult<(u32, u32)> {
41+
webcam_resolution(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
42+
}
43+
44+
pub fn image(&self) -> PyResult<Image> {
45+
let entity =
46+
webcam_image(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
47+
Ok(Image::from_entity(entity))
48+
}
49+
}
50+
51+
impl Drop for Webcam {
52+
fn drop(&mut self) {
53+
let _ = webcam_destroy(self.entity);
54+
}
55+
}

0 commit comments

Comments
 (0)