Compare commits
2 Commits
9b8acd4002
...
740fa2f5f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
740fa2f5f9 | ||
|
|
5ca1e85209 |
@@ -30,8 +30,12 @@ cargo test -- --nocapture # Show println! output
|
|||||||
|
|
||||||
cargo check # Type check only (fast)
|
cargo check # Type check only (fast)
|
||||||
cargo doc --open # Generate and open docs
|
cargo doc --open # Generate and open docs
|
||||||
|
|
||||||
|
./scripts/bundle-macos.sh --install # Rebuild + install .app to /Applications
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**IMPORTANT:** After every release build or code change that requires testing the running app, you MUST run `./scripts/bundle-macos.sh --install` to update the macOS `.app` bundle in `/Applications`. The running `Augmented Canvas.app` uses a copied binary — a bare `cargo build --release` alone does NOT update it. Kill the running app first with `pkill -f "Augmented Canvas"`, then rebuild and relaunch with `open "/Applications/Augmented Canvas.app"`.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -187,6 +187,30 @@ pub struct BooleanOpParam {
|
|||||||
pub stroke_width: Option<f32>,
|
pub stroke_width: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
|
pub struct RenderMermaidParam {
|
||||||
|
#[schemars(description = "Session ID to target. If omitted, uses the active session.")]
|
||||||
|
pub session_id: Option<String>,
|
||||||
|
#[schemars(description = "Mermaid diagram source code (e.g., 'flowchart LR\\n A-->B')")]
|
||||||
|
pub mermaid_source: String,
|
||||||
|
#[schemars(description = "X position on canvas (default: 0)")]
|
||||||
|
pub x: Option<f32>,
|
||||||
|
#[schemars(description = "Y position on canvas (default: 0)")]
|
||||||
|
pub y: Option<f32>,
|
||||||
|
#[schemars(description = "Override width (default: natural SVG width)")]
|
||||||
|
pub width: Option<f32>,
|
||||||
|
#[schemars(description = "Override height (default: natural SVG height)")]
|
||||||
|
pub height: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
|
pub struct BatchParam {
|
||||||
|
#[schemars(
|
||||||
|
description = "JSON array of request objects. Each object must have a 'type' field and the same parameters as individual tool calls. Example: [{\"type\": \"CreateDrawingElement\", \"shape_type\": \"rectangle\", \"x\": 0, \"y\": 0, \"width\": 100, \"height\": 100}, {\"type\": \"CreateDrawingElement\", \"shape_type\": \"ellipse\", \"center_x\": 200, \"center_y\": 200, \"radius_x\": 50, \"radius_y\": 50}]"
|
||||||
|
)]
|
||||||
|
pub requests_json: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AgCanvasServer {
|
pub struct AgCanvasServer {
|
||||||
ws_url: String,
|
ws_url: String,
|
||||||
@@ -521,6 +545,55 @@ impl AgCanvasServer {
|
|||||||
self.call_agcanvas(&request).await
|
self.call_agcanvas(&request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tool(
|
||||||
|
name = "render_mermaid",
|
||||||
|
description = "Render a Mermaid diagram (flowchart, sequence, etc.) as an SVG overlay at a specific position on the canvas. The diagram appears as a visual element that can coexist with other shapes and diagrams."
|
||||||
|
)]
|
||||||
|
async fn render_mermaid(
|
||||||
|
&self,
|
||||||
|
Parameters(params): Parameters<RenderMermaidParam>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
let request = serde_json::json!({
|
||||||
|
"type": "RenderMermaid",
|
||||||
|
"session_id": params.session_id,
|
||||||
|
"mermaid_source": params.mermaid_source,
|
||||||
|
"x": params.x,
|
||||||
|
"y": params.y,
|
||||||
|
"width": params.width,
|
||||||
|
"height": params.height,
|
||||||
|
});
|
||||||
|
self.call_agcanvas(&request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(
|
||||||
|
description = "Send multiple Augmented Canvas operations in one request. Accepts a JSON array of request objects and returns one response per operation in a BatchResult."
|
||||||
|
)]
|
||||||
|
async fn batch(
|
||||||
|
&self,
|
||||||
|
Parameters(params): Parameters<BatchParam>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
let requests = match serde_json::from_str::<serde_json::Value>(¶ms.requests_json) {
|
||||||
|
Ok(serde_json::Value::Array(requests)) => requests,
|
||||||
|
Ok(_) => {
|
||||||
|
return Ok(CallToolResult::error(vec![Content::text(
|
||||||
|
"Invalid requests_json: expected a JSON array of request objects",
|
||||||
|
)]))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Ok(CallToolResult::error(vec![Content::text(format!(
|
||||||
|
"Invalid requests_json: {}",
|
||||||
|
e
|
||||||
|
))]))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = serde_json::json!({
|
||||||
|
"type": "Batch",
|
||||||
|
"requests": requests,
|
||||||
|
});
|
||||||
|
self.call_agcanvas(&request).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tool(description = "Delete a drawing element by its ID.")]
|
#[tool(description = "Delete a drawing element by its ID.")]
|
||||||
async fn delete_drawing_element(
|
async fn delete_drawing_element(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -211,6 +211,22 @@ pub enum AgentRequest {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
stroke_width: Option<f32>,
|
stroke_width: Option<f32>,
|
||||||
},
|
},
|
||||||
|
RenderMermaid {
|
||||||
|
#[serde(default)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
mermaid_source: String,
|
||||||
|
#[serde(default)]
|
||||||
|
x: Option<f32>,
|
||||||
|
#[serde(default)]
|
||||||
|
y: Option<f32>,
|
||||||
|
#[serde(default)]
|
||||||
|
width: Option<f32>,
|
||||||
|
#[serde(default)]
|
||||||
|
height: Option<f32>,
|
||||||
|
},
|
||||||
|
Batch {
|
||||||
|
requests: Vec<AgentRequest>,
|
||||||
|
},
|
||||||
|
|
||||||
Ping,
|
Ping,
|
||||||
}
|
}
|
||||||
@@ -272,6 +288,14 @@ pub enum AgentResponse {
|
|||||||
DrawingElementsCleared {
|
DrawingElementsCleared {
|
||||||
session_id: String,
|
session_id: String,
|
||||||
},
|
},
|
||||||
|
MermaidRendered {
|
||||||
|
session_id: String,
|
||||||
|
overlay_id: String,
|
||||||
|
svg_source: String,
|
||||||
|
},
|
||||||
|
BatchResult {
|
||||||
|
results: Vec<AgentResponse>,
|
||||||
|
},
|
||||||
Pong,
|
Pong,
|
||||||
Error {
|
Error {
|
||||||
message: String,
|
message: String,
|
||||||
@@ -299,6 +323,14 @@ pub enum DrawingCommand {
|
|||||||
Clear {
|
Clear {
|
||||||
session_id: String,
|
session_id: String,
|
||||||
},
|
},
|
||||||
|
RenderMermaid {
|
||||||
|
session_id: String,
|
||||||
|
overlay_id: String,
|
||||||
|
mermaid_source: String,
|
||||||
|
svg_source: String,
|
||||||
|
position: Pos2,
|
||||||
|
size: Vec2,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ use crate::drawing::{boolean, DrawingElement, Shape, ShapeStyle};
|
|||||||
use crate::session::{SessionCreator, SessionStore};
|
use crate::session::{SessionCreator, SessionStore};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::sync::{broadcast, mpsc, RwLock};
|
use tokio::sync::{broadcast, mpsc, RwLock};
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
use usvg::Tree;
|
||||||
|
|
||||||
const EVENT_CHANNEL_CAPACITY: usize = 64;
|
const EVENT_CHANNEL_CAPACITY: usize = 64;
|
||||||
|
static OVERLAY_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
pub struct AgentServer {
|
pub struct AgentServer {
|
||||||
sessions: Arc<RwLock<SessionStore>>,
|
sessions: Arc<RwLock<SessionStore>>,
|
||||||
@@ -151,6 +154,46 @@ async fn process_request(
|
|||||||
event_tx: &broadcast::Sender<GuiEvent>,
|
event_tx: &broadcast::Sender<GuiEvent>,
|
||||||
command_tx: &mpsc::UnboundedSender<DrawingCommand>,
|
command_tx: &mpsc::UnboundedSender<DrawingCommand>,
|
||||||
session_command_tx: &mpsc::UnboundedSender<SessionCommand>,
|
session_command_tx: &mpsc::UnboundedSender<SessionCommand>,
|
||||||
|
) -> AgentResponse {
|
||||||
|
match request {
|
||||||
|
AgentRequest::Batch { requests } => {
|
||||||
|
if requests
|
||||||
|
.iter()
|
||||||
|
.any(|request| matches!(request, AgentRequest::Batch { .. }))
|
||||||
|
{
|
||||||
|
return AgentResponse::Error {
|
||||||
|
message: "Nested batch requests are not supported".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results = Vec::with_capacity(requests.len());
|
||||||
|
for request in requests {
|
||||||
|
let response = process_single_request(
|
||||||
|
request,
|
||||||
|
sessions,
|
||||||
|
event_tx,
|
||||||
|
command_tx,
|
||||||
|
session_command_tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
results.push(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentResponse::BatchResult { results }
|
||||||
|
}
|
||||||
|
request => {
|
||||||
|
process_single_request(request, sessions, event_tx, command_tx, session_command_tx)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_single_request(
|
||||||
|
request: AgentRequest,
|
||||||
|
sessions: &Arc<RwLock<SessionStore>>,
|
||||||
|
event_tx: &broadcast::Sender<GuiEvent>,
|
||||||
|
command_tx: &mpsc::UnboundedSender<DrawingCommand>,
|
||||||
|
session_command_tx: &mpsc::UnboundedSender<SessionCommand>,
|
||||||
) -> AgentResponse {
|
) -> AgentResponse {
|
||||||
match request {
|
match request {
|
||||||
AgentRequest::Ping => AgentResponse::Pong,
|
AgentRequest::Ping => AgentResponse::Pong,
|
||||||
@@ -642,6 +685,81 @@ async fn process_request(
|
|||||||
element,
|
element,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AgentRequest::RenderMermaid {
|
||||||
|
session_id,
|
||||||
|
mermaid_source,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} => {
|
||||||
|
let sid = {
|
||||||
|
let store = sessions.read().await;
|
||||||
|
match store.resolve_session_id(session_id.as_deref()) {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
return AgentResponse::Error {
|
||||||
|
message: "No session found".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let svg_source = match crate::mermaid::render_mermaid_to_svg(&mermaid_source) {
|
||||||
|
Ok(svg) => svg,
|
||||||
|
Err(e) => {
|
||||||
|
return AgentResponse::Error {
|
||||||
|
message: format!("Failed to render Mermaid: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = usvg::Options::default();
|
||||||
|
let tree = match Tree::from_str(&svg_source, &options) {
|
||||||
|
Ok(tree) => tree,
|
||||||
|
Err(e) => {
|
||||||
|
return AgentResponse::Error {
|
||||||
|
message: format!("Failed to parse Mermaid SVG: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let natural_size = tree.size();
|
||||||
|
let overlay_id = format!(
|
||||||
|
"mermaid_{}",
|
||||||
|
OVERLAY_ID_COUNTER.fetch_add(1, Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
let position = egui::pos2(x.unwrap_or(0.0), y.unwrap_or(0.0));
|
||||||
|
let size = egui::vec2(
|
||||||
|
width.unwrap_or(natural_size.width()),
|
||||||
|
height.unwrap_or(natural_size.height()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if command_tx
|
||||||
|
.send(DrawingCommand::RenderMermaid {
|
||||||
|
session_id: sid.clone(),
|
||||||
|
overlay_id: overlay_id.clone(),
|
||||||
|
mermaid_source,
|
||||||
|
svg_source: svg_source.clone(),
|
||||||
|
position,
|
||||||
|
size,
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return AgentResponse::Error {
|
||||||
|
message: "Failed to enqueue Mermaid render command".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentResponse::MermaidRendered {
|
||||||
|
session_id: sid,
|
||||||
|
overlay_id,
|
||||||
|
svg_source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AgentRequest::Batch { .. } => AgentResponse::Error {
|
||||||
|
message: "Nested batch requests are not supported".to_string(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ use crate::canvas::{CanvasInteraction, CanvasState};
|
|||||||
use crate::clipboard::ClipboardManager;
|
use crate::clipboard::ClipboardManager;
|
||||||
use crate::command_palette::{CommandId, CommandPalette};
|
use crate::command_palette::{CommandId, CommandPalette};
|
||||||
use crate::drawing::{
|
use crate::drawing::{
|
||||||
draw_creation_preview, draw_elements, draw_marquee, draw_selection, find_handle_at_screen_pos,
|
canvas_to_screen, draw_creation_preview, draw_elements, draw_marquee, draw_selection,
|
||||||
screen_to_canvas, DragState, DrawingElement, Shape, ShapeStyle, Tool,
|
find_handle_at_screen_pos, screen_to_canvas, DragState, DrawingElement, Shape, ShapeStyle,
|
||||||
|
Tool,
|
||||||
};
|
};
|
||||||
use crate::history::{ChangeSource, HistoryTree, NodeId};
|
use crate::history::{ChangeSource, HistoryTree, NodeId};
|
||||||
use crate::mermaid::render_mermaid_to_svg;
|
use crate::mermaid::render_mermaid_to_svg;
|
||||||
use crate::persistence::{self, SavedSession, SavedWorkspace};
|
use crate::persistence::{self, SavedSession, SavedWorkspace};
|
||||||
use crate::session::{Session, SessionCreator, SessionStore};
|
use crate::session::{MermaidOverlay, Session, SessionCreator, SessionStore};
|
||||||
use crate::svg::{parse_svg, SvgRenderer};
|
use crate::svg::{parse_svg, SvgRenderer};
|
||||||
use egui::{Color32, ColorImage, TextureOptions};
|
use egui::{Color32, ColorImage, TextureOptions};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -290,6 +291,43 @@ impl AgCanvasApp {
|
|||||||
self.status_message = Some((message, std::time::Instant::now()));
|
self.status_message = Some((message, std::time::Instant::now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_mermaid_overlays_to_texture(&mut self, ctx: &egui::Context) {
|
||||||
|
let ppp = ctx.pixels_per_point();
|
||||||
|
let scale = self.active_session().canvas_state.zoom.max(1.0) * ppp;
|
||||||
|
let session = self.active_session_mut();
|
||||||
|
let session_id = session.id.clone();
|
||||||
|
|
||||||
|
for overlay in &mut session.mermaid_overlays {
|
||||||
|
if overlay.texture.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(pixmap) = overlay.renderer.render(scale) {
|
||||||
|
let size = [pixmap.width() as usize, pixmap.height() as usize];
|
||||||
|
let pixels: Vec<Color32> = pixmap
|
||||||
|
.pixels()
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
Color32::from_rgba_premultiplied(p.red(), p.green(), p.blue(), p.alpha())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let image = ColorImage { size, pixels };
|
||||||
|
overlay.texture = Some(ctx.load_texture(
|
||||||
|
format!(
|
||||||
|
"mermaid-{}-{}-{}-{}",
|
||||||
|
session_id,
|
||||||
|
overlay.id,
|
||||||
|
overlay.mermaid_source.len(),
|
||||||
|
overlay.svg_source.len()
|
||||||
|
),
|
||||||
|
image,
|
||||||
|
TextureOptions::LINEAR,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_mermaid_render(&mut self, ctx: &egui::Context) {
|
fn handle_mermaid_render(&mut self, ctx: &egui::Context) {
|
||||||
let source = self.mermaid_input.trim().to_string();
|
let source = self.mermaid_input.trim().to_string();
|
||||||
if source.is_empty() {
|
if source.is_empty() {
|
||||||
@@ -347,7 +385,9 @@ impl AgCanvasApp {
|
|||||||
DrawingCommand::Delete { session_id, id } => {
|
DrawingCommand::Delete { session_id, id } => {
|
||||||
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
|
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
|
||||||
session.drawing_elements.retain(|e| e.id != id);
|
session.drawing_elements.retain(|e| e.id != id);
|
||||||
session.selected_element_ids.retain(|selected_id| selected_id != &id);
|
session
|
||||||
|
.selected_element_ids
|
||||||
|
.retain(|selected_id| selected_id != &id);
|
||||||
session.record_edit(
|
session.record_edit(
|
||||||
"Agent: Delete Element",
|
"Agent: Delete Element",
|
||||||
ChangeSource::Agent { name: None },
|
ChangeSource::Agent { name: None },
|
||||||
@@ -362,6 +402,38 @@ impl AgCanvasApp {
|
|||||||
.record_edit("Agent: Clear Canvas", ChangeSource::Agent { name: None });
|
.record_edit("Agent: Clear Canvas", ChangeSource::Agent { name: None });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DrawingCommand::RenderMermaid {
|
||||||
|
session_id,
|
||||||
|
overlay_id,
|
||||||
|
mermaid_source,
|
||||||
|
svg_source,
|
||||||
|
position,
|
||||||
|
size,
|
||||||
|
} => {
|
||||||
|
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
|
||||||
|
match parse_svg(&svg_source) {
|
||||||
|
Ok((_, usvg_tree)) => {
|
||||||
|
let overlay = MermaidOverlay {
|
||||||
|
id: overlay_id,
|
||||||
|
mermaid_source,
|
||||||
|
svg_source,
|
||||||
|
renderer: SvgRenderer::new(usvg_tree),
|
||||||
|
texture: None,
|
||||||
|
position,
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
session.mermaid_overlays.push(overlay);
|
||||||
|
session.record_edit(
|
||||||
|
"Agent: Render Mermaid",
|
||||||
|
ChangeSource::Agent { name: None },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to parse Mermaid overlay SVG: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,6 +620,7 @@ impl AgCanvasApp {
|
|||||||
CommandId::PasteSvg => self.handle_paste(ctx),
|
CommandId::PasteSvg => self.handle_paste(ctx),
|
||||||
CommandId::PasteMermaid => self.show_mermaid_dialog = true,
|
CommandId::PasteMermaid => self.show_mermaid_dialog = true,
|
||||||
CommandId::ToolSelect => self.active_session_mut().active_tool = Tool::Select,
|
CommandId::ToolSelect => self.active_session_mut().active_tool = Tool::Select,
|
||||||
|
CommandId::ToolPan => self.active_session_mut().active_tool = Tool::Pan,
|
||||||
CommandId::ToolRectangle => self.active_session_mut().active_tool = Tool::Rectangle,
|
CommandId::ToolRectangle => self.active_session_mut().active_tool = Tool::Rectangle,
|
||||||
CommandId::ToolEllipse => self.active_session_mut().active_tool = Tool::Ellipse,
|
CommandId::ToolEllipse => self.active_session_mut().active_tool = Tool::Ellipse,
|
||||||
CommandId::ToolLine => self.active_session_mut().active_tool = Tool::Line,
|
CommandId::ToolLine => self.active_session_mut().active_tool = Tool::Line,
|
||||||
@@ -579,6 +652,11 @@ impl AgCanvasApp {
|
|||||||
Tool::Select => {
|
Tool::Select => {
|
||||||
handle_select_tool(session, response, canvas_center, offset, zoom, pointer_pos);
|
handle_select_tool(session, response, canvas_center, offset, zoom, pointer_pos);
|
||||||
}
|
}
|
||||||
|
Tool::Pan => {
|
||||||
|
if response.dragged_by(egui::PointerButton::Primary) {
|
||||||
|
session.canvas_state.pan(response.drag_delta());
|
||||||
|
}
|
||||||
|
}
|
||||||
Tool::Rectangle | Tool::Ellipse | Tool::Line | Tool::Arrow => {
|
Tool::Rectangle | Tool::Ellipse | Tool::Line | Tool::Arrow => {
|
||||||
handle_shape_tool(session, response, canvas_center, offset, zoom, pointer_pos);
|
handle_shape_tool(session, response, canvas_center, offset, zoom, pointer_pos);
|
||||||
}
|
}
|
||||||
@@ -894,6 +972,9 @@ impl eframe::App for AgCanvasApp {
|
|||||||
if i.key_pressed(egui::Key::V) {
|
if i.key_pressed(egui::Key::V) {
|
||||||
tool_switch = Some(Tool::Select);
|
tool_switch = Some(Tool::Select);
|
||||||
}
|
}
|
||||||
|
if i.key_pressed(egui::Key::H) {
|
||||||
|
tool_switch = Some(Tool::Pan);
|
||||||
|
}
|
||||||
if i.key_pressed(egui::Key::R) {
|
if i.key_pressed(egui::Key::R) {
|
||||||
tool_switch = Some(Tool::Rectangle);
|
tool_switch = Some(Tool::Rectangle);
|
||||||
}
|
}
|
||||||
@@ -1021,6 +1102,14 @@ impl eframe::App for AgCanvasApp {
|
|||||||
{
|
{
|
||||||
self.render_svg_to_texture(ctx);
|
self.render_svg_to_texture(ctx);
|
||||||
}
|
}
|
||||||
|
if self
|
||||||
|
.active_session()
|
||||||
|
.mermaid_overlays
|
||||||
|
.iter()
|
||||||
|
.any(|overlay| overlay.texture.is_none())
|
||||||
|
{
|
||||||
|
self.render_mermaid_overlays_to_texture(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||||
egui::menu::bar(ui, |ui| {
|
egui::menu::bar(ui, |ui| {
|
||||||
@@ -1110,10 +1199,17 @@ impl eframe::App for AgCanvasApp {
|
|||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
ui.label(format!("Agent: ws://127.0.0.1:{}", AGENT_PORT));
|
ui.label(format!("Agent: ws://127.0.0.1:{}", AGENT_PORT));
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label(format!(
|
let zoom_label = format!(
|
||||||
"Zoom: {:.0}%",
|
"Zoom: {:.0}%",
|
||||||
self.active_session().canvas_state.zoom * 100.0
|
self.active_session().canvas_state.zoom * 100.0
|
||||||
));
|
);
|
||||||
|
if ui
|
||||||
|
.add(egui::Button::new(zoom_label).frame(false))
|
||||||
|
.on_hover_text("Click to reset zoom")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.active_session_mut().canvas_state.reset();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1186,6 +1282,7 @@ impl eframe::App for AgCanvasApp {
|
|||||||
let active_tool = self.active_session().active_tool;
|
let active_tool = self.active_session().active_tool;
|
||||||
let tools = [
|
let tools = [
|
||||||
Tool::Select,
|
Tool::Select,
|
||||||
|
Tool::Pan,
|
||||||
Tool::Rectangle,
|
Tool::Rectangle,
|
||||||
Tool::Ellipse,
|
Tool::Ellipse,
|
||||||
Tool::Line,
|
Tool::Line,
|
||||||
@@ -1420,7 +1517,9 @@ impl eframe::App for AgCanvasApp {
|
|||||||
let canvas_center = response.rect.center();
|
let canvas_center = response.rect.center();
|
||||||
|
|
||||||
if response.dragged_by(egui::PointerButton::Middle) {
|
if response.dragged_by(egui::PointerButton::Middle) {
|
||||||
self.active_session_mut().canvas_state.pan(response.drag_delta());
|
self.active_session_mut()
|
||||||
|
.canvas_state
|
||||||
|
.pan(response.drag_delta());
|
||||||
}
|
}
|
||||||
if response.hovered() {
|
if response.hovered() {
|
||||||
let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y);
|
let scroll_delta = ui.input(|i| i.smooth_scroll_delta.y);
|
||||||
@@ -1441,7 +1540,9 @@ impl eframe::App for AgCanvasApp {
|
|||||||
&& response.dragged_by(egui::PointerButton::Primary)
|
&& response.dragged_by(egui::PointerButton::Primary)
|
||||||
&& ui.input(|i| i.modifiers.command)
|
&& ui.input(|i| i.modifiers.command)
|
||||||
{
|
{
|
||||||
self.active_session_mut().canvas_state.pan(response.drag_delta());
|
self.active_session_mut()
|
||||||
|
.canvas_state
|
||||||
|
.pan(response.drag_delta());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.show_mermaid_dialog && !self.show_text_input {
|
if !self.show_mermaid_dialog && !self.show_text_input {
|
||||||
@@ -1476,6 +1577,21 @@ impl eframe::App for AgCanvasApp {
|
|||||||
let offset = self.active_session().canvas_state.offset;
|
let offset = self.active_session().canvas_state.offset;
|
||||||
let zoom = self.active_session().canvas_state.zoom;
|
let zoom = self.active_session().canvas_state.zoom;
|
||||||
|
|
||||||
|
for overlay in &self.active_session().mermaid_overlays {
|
||||||
|
if let Some(texture) = &overlay.texture {
|
||||||
|
let screen_pos =
|
||||||
|
canvas_to_screen(overlay.position, canvas_center, offset, zoom);
|
||||||
|
let screen_size = overlay.size * zoom;
|
||||||
|
let rect = egui::Rect::from_min_size(screen_pos, screen_size);
|
||||||
|
painter.image(
|
||||||
|
texture.id(),
|
||||||
|
rect,
|
||||||
|
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||||
|
Color32::WHITE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
draw_elements(
|
draw_elements(
|
||||||
&painter,
|
&painter,
|
||||||
&self.active_session().drawing_elements,
|
&self.active_session().drawing_elements,
|
||||||
@@ -1488,7 +1604,8 @@ impl eframe::App for AgCanvasApp {
|
|||||||
draw_selection(&painter, selected_el, canvas_center, offset, zoom);
|
draw_selection(&painter, selected_el, canvas_center, offset, zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let DragState::MarqueeSelecting { start, current } = &self.active_session().drag_state
|
if let DragState::MarqueeSelecting { start, current } =
|
||||||
|
&self.active_session().drag_state
|
||||||
{
|
{
|
||||||
draw_marquee(&painter, *start, *current, canvas_center, offset, zoom);
|
draw_marquee(&painter, *start, *current, canvas_center, offset, zoom);
|
||||||
}
|
}
|
||||||
@@ -1504,6 +1621,7 @@ impl eframe::App for AgCanvasApp {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if self.active_session().svg_texture.is_none()
|
if self.active_session().svg_texture.is_none()
|
||||||
|
&& self.active_session().mermaid_overlays.is_empty()
|
||||||
&& self.active_session().drawing_elements.is_empty()
|
&& self.active_session().drawing_elements.is_empty()
|
||||||
{
|
{
|
||||||
painter.text(
|
painter.text(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub enum CommandId {
|
|||||||
PasteSvg,
|
PasteSvg,
|
||||||
PasteMermaid,
|
PasteMermaid,
|
||||||
ToolSelect,
|
ToolSelect,
|
||||||
|
ToolPan,
|
||||||
ToolRectangle,
|
ToolRectangle,
|
||||||
ToolEllipse,
|
ToolEllipse,
|
||||||
ToolLine,
|
ToolLine,
|
||||||
@@ -68,6 +69,7 @@ pub fn all_commands() -> Vec<PaletteCommand> {
|
|||||||
"Canvas",
|
"Canvas",
|
||||||
),
|
),
|
||||||
PaletteCommand::new(CommandId::ToolSelect, "Select Tool", Some("V"), "Tool"),
|
PaletteCommand::new(CommandId::ToolSelect, "Select Tool", Some("V"), "Tool"),
|
||||||
|
PaletteCommand::new(CommandId::ToolPan, "Pan Tool", Some("H"), "Tool"),
|
||||||
PaletteCommand::new(
|
PaletteCommand::new(
|
||||||
CommandId::ToolRectangle,
|
CommandId::ToolRectangle,
|
||||||
"Rectangle Tool",
|
"Rectangle Tool",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ mod tool;
|
|||||||
pub use boolean::BooleanOpType;
|
pub use boolean::BooleanOpType;
|
||||||
pub use element::{DrawingElement, Shape, ShapeStyle};
|
pub use element::{DrawingElement, Shape, ShapeStyle};
|
||||||
pub use render::{
|
pub use render::{
|
||||||
draw_creation_preview, draw_elements, draw_marquee, draw_selection, find_handle_at_screen_pos,
|
canvas_to_screen, draw_creation_preview, draw_elements, draw_marquee, draw_selection,
|
||||||
screen_to_canvas,
|
find_handle_at_screen_pos, screen_to_canvas,
|
||||||
};
|
};
|
||||||
pub use tool::{DragState, Tool};
|
pub use tool::{DragState, Tool};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub enum Tool {
|
pub enum Tool {
|
||||||
#[default]
|
#[default]
|
||||||
Select,
|
Select,
|
||||||
|
Pan,
|
||||||
Rectangle,
|
Rectangle,
|
||||||
Ellipse,
|
Ellipse,
|
||||||
Line,
|
Line,
|
||||||
@@ -16,6 +17,7 @@ impl Tool {
|
|||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Tool::Select => "Select",
|
Tool::Select => "Select",
|
||||||
|
Tool::Pan => "Pan",
|
||||||
Tool::Rectangle => "Rect",
|
Tool::Rectangle => "Rect",
|
||||||
Tool::Ellipse => "Ellipse",
|
Tool::Ellipse => "Ellipse",
|
||||||
Tool::Line => "Line",
|
Tool::Line => "Line",
|
||||||
@@ -27,6 +29,7 @@ impl Tool {
|
|||||||
pub fn shortcut(&self) -> Option<char> {
|
pub fn shortcut(&self) -> Option<char> {
|
||||||
match self {
|
match self {
|
||||||
Tool::Select => Some('V'),
|
Tool::Select => Some('V'),
|
||||||
|
Tool::Pan => Some('H'),
|
||||||
Tool::Rectangle => Some('R'),
|
Tool::Rectangle => Some('R'),
|
||||||
Tool::Ellipse => Some('E'),
|
Tool::Ellipse => Some('E'),
|
||||||
Tool::Line => Some('L'),
|
Tool::Line => Some('L'),
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ pub struct SessionInfo {
|
|||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct MermaidOverlay {
|
||||||
|
pub id: String,
|
||||||
|
pub mermaid_source: String,
|
||||||
|
pub svg_source: String,
|
||||||
|
pub renderer: SvgRenderer,
|
||||||
|
pub texture: Option<TextureHandle>,
|
||||||
|
pub position: egui::Pos2,
|
||||||
|
pub size: egui::Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -88,6 +98,7 @@ pub struct Session {
|
|||||||
pub description_text: String,
|
pub description_text: String,
|
||||||
|
|
||||||
pub drawing_elements: Vec<DrawingElement>,
|
pub drawing_elements: Vec<DrawingElement>,
|
||||||
|
pub mermaid_overlays: Vec<MermaidOverlay>,
|
||||||
pub selected_element_ids: Vec<String>,
|
pub selected_element_ids: Vec<String>,
|
||||||
pub active_tool: Tool,
|
pub active_tool: Tool,
|
||||||
pub drag_state: DragState,
|
pub drag_state: DragState,
|
||||||
@@ -110,6 +121,7 @@ impl Session {
|
|||||||
svg_source: None,
|
svg_source: None,
|
||||||
description_text: String::new(),
|
description_text: String::new(),
|
||||||
drawing_elements: Vec::new(),
|
drawing_elements: Vec::new(),
|
||||||
|
mermaid_overlays: Vec::new(),
|
||||||
selected_element_ids: Vec::new(),
|
selected_element_ids: Vec::new(),
|
||||||
active_tool: Tool::default(),
|
active_tool: Tool::default(),
|
||||||
drag_state: DragState::default(),
|
drag_state: DragState::default(),
|
||||||
@@ -145,6 +157,7 @@ impl Session {
|
|||||||
self.svg_source = None;
|
self.svg_source = None;
|
||||||
self.description_text.clear();
|
self.description_text.clear();
|
||||||
self.drawing_elements.clear();
|
self.drawing_elements.clear();
|
||||||
|
self.mermaid_overlays.clear();
|
||||||
self.selected_element_ids.clear();
|
self.selected_element_ids.clear();
|
||||||
self.drag_state = DragState::default();
|
self.drag_state = DragState::default();
|
||||||
self.canvas_state.reset();
|
self.canvas_state.reset();
|
||||||
@@ -193,6 +206,7 @@ impl Session {
|
|||||||
self.svg_renderer = None;
|
self.svg_renderer = None;
|
||||||
self.svg_texture = None;
|
self.svg_texture = None;
|
||||||
self.description_text.clear();
|
self.description_text.clear();
|
||||||
|
self.mermaid_overlays.clear();
|
||||||
self.selected_element_ids.clear();
|
self.selected_element_ids.clear();
|
||||||
self.drag_state = DragState::default();
|
self.drag_state = DragState::default();
|
||||||
}
|
}
|
||||||
@@ -205,6 +219,7 @@ impl Session {
|
|||||||
self.svg_renderer = None;
|
self.svg_renderer = None;
|
||||||
self.svg_texture = None;
|
self.svg_texture = None;
|
||||||
self.description_text.clear();
|
self.description_text.clear();
|
||||||
|
self.mermaid_overlays.clear();
|
||||||
self.selected_element_ids.clear();
|
self.selected_element_ids.clear();
|
||||||
self.drag_state = DragState::default();
|
self.drag_state = DragState::default();
|
||||||
true
|
true
|
||||||
@@ -221,6 +236,7 @@ impl Session {
|
|||||||
self.svg_renderer = None;
|
self.svg_renderer = None;
|
||||||
self.svg_texture = None;
|
self.svg_texture = None;
|
||||||
self.description_text.clear();
|
self.description_text.clear();
|
||||||
|
self.mermaid_overlays.clear();
|
||||||
self.selected_element_ids.clear();
|
self.selected_element_ids.clear();
|
||||||
self.drag_state = DragState::default();
|
self.drag_state = DragState::default();
|
||||||
true
|
true
|
||||||
|
|||||||
Reference in New Issue
Block a user