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 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
|
||||
|
||||
```
|
||||
|
||||
@@ -187,6 +187,30 @@ pub struct BooleanOpParam {
|
||||
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)]
|
||||
pub struct AgCanvasServer {
|
||||
ws_url: String,
|
||||
@@ -521,6 +545,55 @@ impl AgCanvasServer {
|
||||
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.")]
|
||||
async fn delete_drawing_element(
|
||||
&self,
|
||||
|
||||
@@ -211,6 +211,22 @@ pub enum AgentRequest {
|
||||
#[serde(default)]
|
||||
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,
|
||||
}
|
||||
@@ -272,6 +288,14 @@ pub enum AgentResponse {
|
||||
DrawingElementsCleared {
|
||||
session_id: String,
|
||||
},
|
||||
MermaidRendered {
|
||||
session_id: String,
|
||||
overlay_id: String,
|
||||
svg_source: String,
|
||||
},
|
||||
BatchResult {
|
||||
results: Vec<AgentResponse>,
|
||||
},
|
||||
Pong,
|
||||
Error {
|
||||
message: String,
|
||||
@@ -299,6 +323,14 @@ pub enum DrawingCommand {
|
||||
Clear {
|
||||
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 anyhow::Result;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{broadcast, mpsc, RwLock};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use usvg::Tree;
|
||||
|
||||
const EVENT_CHANNEL_CAPACITY: usize = 64;
|
||||
static OVERLAY_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub struct AgentServer {
|
||||
sessions: Arc<RwLock<SessionStore>>,
|
||||
@@ -151,6 +154,46 @@ async fn process_request(
|
||||
event_tx: &broadcast::Sender<GuiEvent>,
|
||||
command_tx: &mpsc::UnboundedSender<DrawingCommand>,
|
||||
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 {
|
||||
match request {
|
||||
AgentRequest::Ping => AgentResponse::Pong,
|
||||
@@ -642,6 +685,81 @@ async fn process_request(
|
||||
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::command_palette::{CommandId, CommandPalette};
|
||||
use crate::drawing::{
|
||||
draw_creation_preview, draw_elements, draw_marquee, draw_selection, find_handle_at_screen_pos,
|
||||
screen_to_canvas, DragState, DrawingElement, Shape, ShapeStyle, Tool,
|
||||
canvas_to_screen, draw_creation_preview, draw_elements, draw_marquee, draw_selection,
|
||||
find_handle_at_screen_pos, screen_to_canvas, DragState, DrawingElement, Shape, ShapeStyle,
|
||||
Tool,
|
||||
};
|
||||
use crate::history::{ChangeSource, HistoryTree, NodeId};
|
||||
use crate::mermaid::render_mermaid_to_svg;
|
||||
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 egui::{Color32, ColorImage, TextureOptions};
|
||||
use std::path::Path;
|
||||
@@ -290,6 +291,43 @@ impl AgCanvasApp {
|
||||
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) {
|
||||
let source = self.mermaid_input.trim().to_string();
|
||||
if source.is_empty() {
|
||||
@@ -347,7 +385,9 @@ impl AgCanvasApp {
|
||||
DrawingCommand::Delete { session_id, id } => {
|
||||
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_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(
|
||||
"Agent: Delete Element",
|
||||
ChangeSource::Agent { name: None },
|
||||
@@ -362,6 +402,38 @@ impl AgCanvasApp {
|
||||
.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::PasteMermaid => self.show_mermaid_dialog = true,
|
||||
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::ToolEllipse => self.active_session_mut().active_tool = Tool::Ellipse,
|
||||
CommandId::ToolLine => self.active_session_mut().active_tool = Tool::Line,
|
||||
@@ -579,6 +652,11 @@ impl AgCanvasApp {
|
||||
Tool::Select => {
|
||||
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 => {
|
||||
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) {
|
||||
tool_switch = Some(Tool::Select);
|
||||
}
|
||||
if i.key_pressed(egui::Key::H) {
|
||||
tool_switch = Some(Tool::Pan);
|
||||
}
|
||||
if i.key_pressed(egui::Key::R) {
|
||||
tool_switch = Some(Tool::Rectangle);
|
||||
}
|
||||
@@ -1021,6 +1102,14 @@ impl eframe::App for AgCanvasApp {
|
||||
{
|
||||
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::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.label(format!("Agent: ws://127.0.0.1:{}", AGENT_PORT));
|
||||
ui.separator();
|
||||
ui.label(format!(
|
||||
let zoom_label = format!(
|
||||
"Zoom: {:.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 tools = [
|
||||
Tool::Select,
|
||||
Tool::Pan,
|
||||
Tool::Rectangle,
|
||||
Tool::Ellipse,
|
||||
Tool::Line,
|
||||
@@ -1420,7 +1517,9 @@ impl eframe::App for AgCanvasApp {
|
||||
let canvas_center = response.rect.center();
|
||||
|
||||
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() {
|
||||
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)
|
||||
&& 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 {
|
||||
@@ -1476,6 +1577,21 @@ impl eframe::App for AgCanvasApp {
|
||||
let offset = self.active_session().canvas_state.offset;
|
||||
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(
|
||||
&painter,
|
||||
&self.active_session().drawing_elements,
|
||||
@@ -1488,7 +1604,8 @@ impl eframe::App for AgCanvasApp {
|
||||
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);
|
||||
}
|
||||
@@ -1504,6 +1621,7 @@ impl eframe::App for AgCanvasApp {
|
||||
);
|
||||
|
||||
if self.active_session().svg_texture.is_none()
|
||||
&& self.active_session().mermaid_overlays.is_empty()
|
||||
&& self.active_session().drawing_elements.is_empty()
|
||||
{
|
||||
painter.text(
|
||||
|
||||
@@ -11,6 +11,7 @@ pub enum CommandId {
|
||||
PasteSvg,
|
||||
PasteMermaid,
|
||||
ToolSelect,
|
||||
ToolPan,
|
||||
ToolRectangle,
|
||||
ToolEllipse,
|
||||
ToolLine,
|
||||
@@ -68,6 +69,7 @@ pub fn all_commands() -> Vec<PaletteCommand> {
|
||||
"Canvas",
|
||||
),
|
||||
PaletteCommand::new(CommandId::ToolSelect, "Select Tool", Some("V"), "Tool"),
|
||||
PaletteCommand::new(CommandId::ToolPan, "Pan Tool", Some("H"), "Tool"),
|
||||
PaletteCommand::new(
|
||||
CommandId::ToolRectangle,
|
||||
"Rectangle Tool",
|
||||
|
||||
@@ -6,7 +6,7 @@ mod tool;
|
||||
pub use boolean::BooleanOpType;
|
||||
pub use element::{DrawingElement, Shape, ShapeStyle};
|
||||
pub use render::{
|
||||
draw_creation_preview, draw_elements, draw_marquee, draw_selection, find_handle_at_screen_pos,
|
||||
screen_to_canvas,
|
||||
canvas_to_screen, draw_creation_preview, draw_elements, draw_marquee, draw_selection,
|
||||
find_handle_at_screen_pos, screen_to_canvas,
|
||||
};
|
||||
pub use tool::{DragState, Tool};
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub enum Tool {
|
||||
#[default]
|
||||
Select,
|
||||
Pan,
|
||||
Rectangle,
|
||||
Ellipse,
|
||||
Line,
|
||||
@@ -16,6 +17,7 @@ impl Tool {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Tool::Select => "Select",
|
||||
Tool::Pan => "Pan",
|
||||
Tool::Rectangle => "Rect",
|
||||
Tool::Ellipse => "Ellipse",
|
||||
Tool::Line => "Line",
|
||||
@@ -27,6 +29,7 @@ impl Tool {
|
||||
pub fn shortcut(&self) -> Option<char> {
|
||||
match self {
|
||||
Tool::Select => Some('V'),
|
||||
Tool::Pan => Some('H'),
|
||||
Tool::Rectangle => Some('R'),
|
||||
Tool::Ellipse => Some('E'),
|
||||
Tool::Line => Some('L'),
|
||||
|
||||
@@ -77,6 +77,16 @@ pub struct SessionInfo {
|
||||
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 id: String,
|
||||
pub name: String,
|
||||
@@ -88,6 +98,7 @@ pub struct Session {
|
||||
pub description_text: String,
|
||||
|
||||
pub drawing_elements: Vec<DrawingElement>,
|
||||
pub mermaid_overlays: Vec<MermaidOverlay>,
|
||||
pub selected_element_ids: Vec<String>,
|
||||
pub active_tool: Tool,
|
||||
pub drag_state: DragState,
|
||||
@@ -110,6 +121,7 @@ impl Session {
|
||||
svg_source: None,
|
||||
description_text: String::new(),
|
||||
drawing_elements: Vec::new(),
|
||||
mermaid_overlays: Vec::new(),
|
||||
selected_element_ids: Vec::new(),
|
||||
active_tool: Tool::default(),
|
||||
drag_state: DragState::default(),
|
||||
@@ -145,6 +157,7 @@ impl Session {
|
||||
self.svg_source = None;
|
||||
self.description_text.clear();
|
||||
self.drawing_elements.clear();
|
||||
self.mermaid_overlays.clear();
|
||||
self.selected_element_ids.clear();
|
||||
self.drag_state = DragState::default();
|
||||
self.canvas_state.reset();
|
||||
@@ -193,6 +206,7 @@ impl Session {
|
||||
self.svg_renderer = None;
|
||||
self.svg_texture = None;
|
||||
self.description_text.clear();
|
||||
self.mermaid_overlays.clear();
|
||||
self.selected_element_ids.clear();
|
||||
self.drag_state = DragState::default();
|
||||
}
|
||||
@@ -205,6 +219,7 @@ impl Session {
|
||||
self.svg_renderer = None;
|
||||
self.svg_texture = None;
|
||||
self.description_text.clear();
|
||||
self.mermaid_overlays.clear();
|
||||
self.selected_element_ids.clear();
|
||||
self.drag_state = DragState::default();
|
||||
true
|
||||
@@ -221,6 +236,7 @@ impl Session {
|
||||
self.svg_renderer = None;
|
||||
self.svg_texture = None;
|
||||
self.description_text.clear();
|
||||
self.mermaid_overlays.clear();
|
||||
self.selected_element_ids.clear();
|
||||
self.drag_state = DragState::default();
|
||||
true
|
||||
|
||||
Reference in New Issue
Block a user