This commit is contained in:
Matthieu Jolimaitre 2025-07-30 21:54:57 +02:00
commit 5aa6d2be89
14 changed files with 5093 additions and 0 deletions

16
.cargo/config.toml Normal file
View file

@ -0,0 +1,16 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = [ # alignment
"-C",
"link-arg=-fuse-ld=lld",
"-Zshare-generics=y",
]
[unstable]
codegen-backend = true
[profile.dev]
codegen-backend = "cranelift"
[profile.dev.package."*"]
codegen-backend = "llvm"

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

4698
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

72
Cargo.toml Normal file
View file

@ -0,0 +1,72 @@
[package]
name = "noders"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { version = "0.16.1", default-features = false, features = [
# "android-game-activity",
# "android_shared_stdcxx",
# "animation",
"async_executor",
"bevy_asset",
# "bevy_audio",
"bevy_color",
"bevy_core_pipeline",
# "bevy_gilrs",
"bevy_gizmos",
# "bevy_gltf",
# "bevy_input_focus",
# "bevy_log",
# "bevy_mesh_picking_backend",
"bevy_pbr",
# "bevy_picking",
"bevy_render",
"bevy_scene",
"bevy_sprite",
# "bevy_sprite_picking_backend",
"bevy_state",
"bevy_text",
# "bevy_ui",
# "bevy_ui_picking_backend",
"bevy_window",
"bevy_winit",
# "custom_cursor",
# "default_font",
"hdr",
"multi_threaded",
"png",
# "smaa_luts",
"std",
"sysinfo_plugin",
# "tonemapping_luts",
# "vorbis",
"webgl2",
"x11",
"dynamic_linking",
] }
bevy_pancam = "0.18.0"
bevy_rapier2d = { version = "0.30.0", default-features = false, features = [
"async-collider",
# "debug-render-2d",
"dim2",
# "picking-backend",
# "to-bevy-mesh",
"parallel",
"simd-stable",
] }
itertools = "0.14.0"
log = { version = "*", features = [
"max_level_debug",
"release_max_level_warn",
] }
rand = "0.9.2"
rand_xorshift = "0.4.0"
[[bin]]
name = "noders"
path = "src/noders.rs"
[profile.release]
lto = true

16
examples/simple.rs Normal file
View file

@ -0,0 +1,16 @@
use noders::{display::display, graph::Graph};
fn main() {
let mut graph = Graph::empty();
let a = graph.node("A");
let b = graph.node("B");
let c = graph.node("C");
let d = graph.node("D");
graph.link(a, b);
graph.link(a, c);
graph.link(a, d);
display(graph);
}

37
examples/tree.rs Normal file
View file

@ -0,0 +1,37 @@
use noders::{
display::display,
graph::{Graph, NodeId},
};
use rand::{SeedableRng, prelude::*};
use rand_xorshift::XorShiftRng;
fn main() {
let mut rng = XorShiftRng::seed_from_u64(111111111112111122);
let mut graph = Graph::empty();
let root = graph.node("ROOT");
branch(&mut graph, &mut rng, root, vec![], 7, 5);
dbg!(graph.nodes.len());
display(graph);
}
pub fn branch(
graph: &mut Graph,
rng: &mut XorShiftRng,
parent: NodeId,
path: Vec<usize>,
max_children: usize,
max_depth: usize,
) {
if max_depth == 0 {
return;
}
let child_count = rng.random_range(0..max_children);
for index in 1..child_count {
let mut path = path.clone();
path.push(index);
let label = path.iter().map(ToString::to_string).collect::<Vec<_>>().join(".");
let child = graph.node(label);
graph.link(child, parent);
branch(graph, rng, child, path, max_children, max_depth - 1);
}
}

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

3
rustfmt.toml Normal file
View file

@ -0,0 +1,3 @@
hard_tabs = true
max_width = 120
use_small_heuristics = "Max"

131
src/core/display.rs Normal file
View file

@ -0,0 +1,131 @@
use std::collections::HashMap;
use super::graph::Graph;
use bevy::{color::palettes, prelude::*};
use bevy_pancam::{PanCam, PanCamPlugin};
use bevy_rapier2d::{prelude::*, rapier::prelude::RigidBodyVelocity};
use itertools::Itertools;
pub fn display(graph: Graph) {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
// .add_plugins(RapierDebugRenderPlugin::default())
.add_plugins(PanCamPlugin)
.add_systems(Startup, disable_gravity)
.add_systems(Startup, setup)
.add_systems(Startup, move |mut c: Commands| {
let mut node_ents = HashMap::new();
let node_size = vec2(100., 50.);
let placement = placement::circle(graph.nodes.len(), node_size.length() * 1.5);
let placement = placement::Grid::square(graph.nodes.len(), node_size * 2.);
let placement = placement.into_iter();
for (node, pos) in graph.nodes.values().zip(placement) {
let entity = spawn_node(&mut c, node.label.clone(), node_size, pos);
node_ents.insert(node.id, entity);
}
for link in graph.links.values() {
let a = node_ents.get(&link.from_node_id);
let b = node_ents.get(&link.into_node_id);
let Some((&a, &b)) = a.zip(b) else {
continue;
};
let joint = SpringJoint::new(200., 1_000_000., 0.01);
let joint = ImpulseJoint::new(a, joint);
c.entity(b).with_child(joint);
spawn_link(&mut c, a, b);
}
for (&a, &b) in node_ents.values().tuple_combinations() {
let joint = SpringJoint::new(1_000., 1_000., 0.01);
let joint = ImpulseJoint::new(a, joint);
c.entity(b).with_child(joint);
}
})
.add_systems(Update, (draw_link, draw_node));
app.run();
}
fn spawn_node(c: &mut Commands, label: String, size: Vec2, pos: Vec2) -> Entity {
c.spawn((
Damping { linear_damping: 0.5, angular_damping: 0.5 },
Transform::default().with_translation(vec3(pos.x, pos.y, 0.)),
RigidBody::Dynamic,
Collider::cuboid(size.x / 2., size.y / 2.),
Sensor,
ExternalImpulse::default(),
Velocity::default(),
LockedAxes::ROTATION_LOCKED,
Sleeping::disabled(),
NodeComp { label, size },
))
.id()
}
#[derive(Debug, Clone, Component, Reflect)]
struct NodeComp {
label: String,
size: Vec2,
}
fn spawn_link(c: &mut Commands, a: Entity, b: Entity) {
c.spawn(LinkComp { a, b });
}
#[derive(Debug, Clone, Component, Reflect)]
struct LinkComp {
a: Entity,
b: Entity,
}
const LINK_DISTANCE: f32 = 150.;
const LINK_STIFFNESS: f32 = 100.;
fn sim_link(links: Query<&LinkComp>, mut nodes: Query<(&Transform, &mut ExternalImpulse), With<NodeComp>>) {
for link in links.iter() {
let [(transform_a, mut impulse_a), (transform_b, mut impulse_b)] =
nodes.get_many_mut([link.a, link.b]).unwrap();
let a_to_b = transform_b.translation.xy() - transform_a.translation.xy();
// negative when stretched inward.
let stretch = a_to_b.length() - LINK_DISTANCE;
let strength = stretch * LINK_STIFFNESS;
impulse_a.impulse = a_to_b.normalize_or_zero() * strength;
impulse_b.impulse = a_to_b.normalize_or_zero() * -strength;
}
}
fn repulsion(mut nodes: Query<(&Transform, &mut ExternalImpulse), With<NodeComp>>) {
let mut combinations = nodes.iter_combinations_mut::<2>();
while let Some([(transform_a, mut impulse_a), (transform_b, mut impulse_b)]) = combinations.fetch_next() {
let a_to_b = transform_b.translation.xy() - transform_a.translation.xy();
// // negative when stretched inward.
// let stretch = a_to_b.length() - LINK_DISTANCE;
// let strength = stretch * LINK_STIFFNESS;
// impulse_a.impulse = a_to_b.normalize_or_zero() * strength;
// impulse_b.impulse = a_to_b.normalize_or_zero() * -strength;
}
}
fn draw_link(mut gizmos: Gizmos, links: Query<&LinkComp>, nodes: Query<&Transform, With<NodeComp>>) {
for link in links {
let a = nodes.get(link.a).unwrap();
let b = nodes.get(link.b).unwrap();
gizmos.line_2d(a.translation.xy(), b.translation.xy(), palettes::basic::WHITE);
}
}
fn draw_node(mut gizmos: Gizmos, nodes: Query<(&Transform, &NodeComp)>) {
for node in nodes {
gizmos.rect_2d(Isometry2d::from_translation(node.0.translation.xy()), node.1.size, palettes::basic::WHITE);
}
}
fn setup(mut c: Commands) {
c.spawn((Camera2d, PanCam::default()));
}
fn disable_gravity(mut rapier_config: Query<&mut RapierConfiguration>) {
if let Ok(mut config) = rapier_config.single_mut() {
config.gravity = Vec2::ZERO;
}
}
mod placement;

View file

@ -0,0 +1,53 @@
use std::f32::consts::PI;
use bevy::math::{Vec2, vec2};
pub struct Grid {
column_count: usize,
cell_size: Vec2,
}
impl Grid {
pub fn square(slot_count: usize, cell_size: Vec2) -> Self {
let column_count = ((slot_count as f32).sqrt() + 1.) as usize;
Self { column_count, cell_size }
}
}
impl<'g> IntoIterator for &'g Grid {
type IntoIter = GridIter<'g>;
type Item = Vec2;
fn into_iter(self) -> Self::IntoIter {
let grid = self;
let i = 0;
GridIter { grid, i }
}
}
pub struct GridIter<'g> {
grid: &'g Grid,
i: usize,
}
impl<'g> Iterator for GridIter<'g> {
type Item = Vec2;
fn next(&mut self) -> Option<Self::Item> {
let x = self.i % self.grid.column_count;
let y = self.i / self.grid.column_count;
let value = vec2(x as _, y as _) * self.grid.cell_size;
self.i += 1;
Some(value)
}
}
pub fn circle(items: usize, item_width: f32) -> impl Iterator<Item = Vec2> {
let perimeter = items as f32 * item_width;
let radius = perimeter / (2. * PI);
(0..items).map(move |i| {
let frac = i as f32 / items as f32;
let angle = frac * 2. * PI;
vec2(angle.cos(), angle.sin()) * radius
})
}

56
src/core/graph.rs Normal file
View file

@ -0,0 +1,56 @@
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct Graph {
next_id: usize,
pub nodes: HashMap<NodeId, Node>,
pub links: HashMap<LinkId, Link>,
}
impl Graph {
pub fn empty() -> Self {
Self::default()
}
pub fn node(&mut self, label: impl ToString) -> NodeId {
let id = self.id(NodeId);
let label = label.to_string();
let node = Node { id, label };
self.nodes.insert(id, node);
id
}
pub fn link(&mut self, from_node_id: NodeId, into_node_id: NodeId) -> Option<LinkId> {
self.nodes.get(&from_node_id)?;
self.nodes.get(&into_node_id)?;
let id = self.id(LinkId);
let link = Link { from_node_id, into_node_id, id };
self.links.insert(id, link);
Some(id)
}
fn id<T>(&mut self, cons: impl Fn(usize) -> T) -> T {
let id = cons(self.next_id);
self.next_id += 1;
id
}
}
#[derive(Debug, Clone)]
pub struct Node {
pub id: NodeId,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct Link {
pub id: LinkId,
pub from_node_id: NodeId,
pub into_node_id: NodeId,
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct NodeId(usize);
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct LinkId(usize);

2
src/core/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod display;
pub mod graph;

3
src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod core;
pub use core::*;

3
src/noders.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
println!("Henlo.");
}