Skip to content

Commit dc47dc7

Browse files
committed
feat: TextNode
1 parent de2b87d commit dc47dc7

File tree

11 files changed

+226
-115
lines changed

11 files changed

+226
-115
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ serde_json = "1.0.149"
2222
typetag = "0.2.21"
2323
dyn-clone = "1.0.20"
2424
once_cell = "1.21.3"
25+
enum_dispatch = "0.3.13"
2526

2627
# 资源处理 (图片与字体)
2728
image = { version = "0.25.9", features = ["jpeg", "png"] }
@@ -30,6 +31,7 @@ font-kit = "0.14.3"
3031
# 实用工具与随机数
3132
rand = "0.9.2"
3233
nanoid = "0.4.0"
34+
lru = "0.16.3"
3335

3436
# 日志与调试
3537
log = "0.4.29"

crates/project_graph/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ nanoid.workspace = true
4242
typetag.workspace = true
4343
dyn-clone.workspace = true
4444
once_cell.workspace = true
45+
enum_dispatch.workspace = true
46+
lru.workspace = true
4547

4648
[target.'cfg(not(any(target_os = "android", target_arch = "wasm32")))'.dependencies]
4749
font-kit.workspace = true

crates/project_graph/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ pub mod fonts;
77
pub mod settings;
88
pub mod settings_window;
99
pub mod stage;
10-
pub mod structs;
1110
pub mod themes;
1211
pub mod utils;
1312

crates/project_graph/src/stage.rs

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,27 @@
11
mod camera;
2+
mod context;
3+
mod render_context;
4+
mod structs;
25

36
use camera::Camera;
7+
use context::StageContext;
48
use eframe::egui::{self};
5-
use egui::Pos2;
6-
use nanoid::nanoid;
7-
use rand::Rng;
9+
use structs::EntityTrait;
810

9-
use crate::structs::{StageObject, TextNode};
11+
use crate::stage::render_context::RenderContext;
1012

1113
/// egui 和画布之间的桥梁
1214
/// 负责坐标系转换、事件处理等
1315
pub struct Stage {
1416
camera: Camera,
15-
items: Vec<Box<dyn StageObject>>,
17+
context: StageContext,
1618
}
1719

1820
impl Stage {
1921
pub fn new() -> Self {
20-
let mut rng = rand::rng();
21-
let mut items = Vec::<Box<dyn StageObject>>::new();
22-
for _ in 0..5 {
23-
let pos = Pos2::new(
24-
rng.random_range(-500.0..500.0),
25-
rng.random_range(-500.0..500.0),
26-
);
27-
28-
items.push(Box::new(TextNode {
29-
id: nanoid!(),
30-
position: pos,
31-
content: "Hello, World!".to_string(),
32-
}))
33-
}
34-
3522
Stage {
3623
camera: Camera::new(),
37-
items,
24+
context: StageContext::random(),
3825
}
3926
}
4027

@@ -52,30 +39,16 @@ impl Stage {
5239
let screen_center = rect.center();
5340
let mut visible_count = 0;
5441

55-
for item in &self.items {
56-
let screen_pos = self.camera.world_to_screen(
57-
item.as_any().downcast_ref::<TextNode>().unwrap().position,
58-
screen_center,
59-
);
60-
61-
// 简单的视锥剔除 (Frustum Culling)
62-
// 扩大一点范围以免边缘物体突然消失
63-
// if !ui.clip_rect().expand(100.0).contains(screen_pos_egui) {
64-
// continue;
65-
// }
66-
42+
for entity in self.context.entities().values() {
6743
visible_count += 1;
68-
69-
ui.push_id(item.id(), |ui| {
70-
ui.with_visual_transform(
71-
egui::emath::TSTransform {
72-
translation: screen_pos.to_vec2(),
73-
scaling: self.camera.zoom(),
74-
},
75-
|ui| {
76-
item.render(ui);
77-
},
78-
);
44+
let screen_pos = self
45+
.camera
46+
.world_to_screen(entity.position(), screen_center);
47+
48+
entity.render(&mut RenderContext {
49+
painter: painter.clone(),
50+
position: screen_pos,
51+
scale: self.camera.zoom(),
7952
});
8053
}
8154

@@ -90,22 +63,20 @@ impl Stage {
9063

9164
let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
9265
if scroll_delta.y != 0.0 {
93-
let zoom_factor = (1.0 + scroll_delta.y * 0.01).clamp(0.9, 1.1);
94-
9566
if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
67+
let zoom_factor = (1.0 + scroll_delta.y * 0.01).clamp(0.9, 1.1);
9668
let old_zoom = self.camera.zoom();
97-
let new_zoom = old_zoom * zoom_factor;
69+
self.camera.zoom_by(zoom_factor);
70+
9871
let offset = mouse_pos - rect.center();
99-
let delta = offset * (1.0 / old_zoom - 1.0 / new_zoom);
72+
let delta = offset * (self.camera.zoom() / old_zoom - 1.0);
10073
self.camera.pan_by(delta);
10174
}
102-
103-
self.camera.zoom_by(zoom_factor);
10475
}
10576

10677
// 中键拖拽平移
10778
if response.dragged_by(egui::PointerButton::Middle) {
108-
let drag_delta = ui.input(|i| i.pointer.delta()) / self.camera.zoom();
79+
let drag_delta = ui.input(|i| i.pointer.delta());
10980
self.camera.pan_by(-drag_delta);
11081
}
11182
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std::collections::HashMap;
2+
3+
use egui::Pos2;
4+
use nanoid::nanoid;
5+
6+
use crate::stage::structs::{Entity, TextNode};
7+
8+
pub struct StageContext {
9+
entities: HashMap<String, Entity>,
10+
}
11+
12+
impl StageContext {
13+
pub fn new() -> Self {
14+
StageContext {
15+
entities: HashMap::new(),
16+
}
17+
}
18+
pub fn random() -> Self {
19+
let mut entities: HashMap<String, Entity> = HashMap::new();
20+
for i in 0..1000 {
21+
let id = nanoid!();
22+
entities.insert(
23+
id.clone(),
24+
TextNode {
25+
id,
26+
content: format!("节点 {}", i),
27+
position: Pos2::new(
28+
rand::random::<f32>() * 2000.0 - 1000.0,
29+
rand::random::<f32>() * 2000.0 - 1000.0,
30+
),
31+
}
32+
.into(),
33+
);
34+
}
35+
StageContext { entities }
36+
}
37+
38+
pub fn entities(&self) -> &HashMap<String, Entity> {
39+
&self.entities
40+
}
41+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use egui::{Painter, Pos2, Stroke, vec2};
2+
3+
pub struct RenderContext {
4+
pub painter: Painter,
5+
pub position: Pos2,
6+
pub scale: f32,
7+
}
8+
9+
impl RenderContext {
10+
pub fn local_to_screen(&self, local_pos: Pos2) -> Pos2 {
11+
Pos2 {
12+
x: self.position.x + local_pos.x * self.scale,
13+
y: self.position.y + local_pos.y * self.scale,
14+
}
15+
}
16+
17+
pub fn rect(&self, local_rect: egui::Rect, color: egui::Color32) {
18+
let screen_rect = egui::Rect {
19+
min: self.local_to_screen(local_rect.min),
20+
max: self.local_to_screen(local_rect.max),
21+
};
22+
self.painter.rect_stroke(
23+
screen_rect,
24+
8.0 * self.scale,
25+
Stroke {
26+
color,
27+
width: self.scale,
28+
},
29+
egui::StrokeKind::Middle,
30+
);
31+
}
32+
33+
pub fn text(&self, position: Pos2, text: &str) {
34+
self.painter.text(
35+
self.local_to_screen(position) - vec2(0.0, 14.0 * 0.15) * self.scale,
36+
egui::Align2::LEFT_TOP,
37+
text,
38+
egui::FontId::proportional(14.0 * self.scale),
39+
egui::Color32::WHITE,
40+
);
41+
}
42+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use egui::{Color32, Pos2, Rect, pos2};
2+
use enum_dispatch::enum_dispatch;
3+
4+
use crate::{stage::render_context::RenderContext, utils::text::get_text_width};
5+
6+
#[enum_dispatch]
7+
pub trait EntityTrait {
8+
fn id(&self) -> &str;
9+
fn position(&self) -> Pos2;
10+
fn render(&self, rc: &mut RenderContext);
11+
}
12+
13+
pub struct TextNode {
14+
pub id: String,
15+
pub position: Pos2,
16+
pub content: String,
17+
}
18+
impl EntityTrait for TextNode {
19+
fn id(&self) -> &str {
20+
&self.id
21+
}
22+
fn position(&self) -> Pos2 {
23+
self.position
24+
}
25+
fn render(&self, rc: &mut RenderContext) {
26+
let padding = 8.0;
27+
28+
rc.rect(
29+
Rect {
30+
min: Pos2::ZERO,
31+
max: Pos2 {
32+
x: get_text_width(&self.content, 14.0, &rc.painter) + padding * 2.0,
33+
y: 14.0 + padding * 2.0,
34+
},
35+
},
36+
Color32::WHITE,
37+
);
38+
rc.text(pos2(padding, padding), &self.content);
39+
}
40+
}
41+
42+
#[enum_dispatch(EntityTrait)]
43+
pub enum Entity {
44+
TextNode(TextNode),
45+
}

0 commit comments

Comments
 (0)