Skip to main content

draw_wasm/
lib.rs

1//! WASM bindings for the draw renderer.
2//!
3//! Exposes a `DrawEngine` that holds document state, viewport, selection,
4//! and renderer — callable from JavaScript via wasm-bindgen.
5//!
6//! Complex types cross the boundary as JSON strings to keep the interface simple.
7
8#[cfg(target_arch = "wasm32")]
9use wasm_bindgen::prelude::*;
10
11use draw_core::history::{Action, History};
12use draw_core::point::{Bounds, ViewState};
13use draw_core::render::{RenderConfig, Renderer};
14use draw_core::{Document, Element};
15
16// ── DrawEngine ──────────────────────────────────────────────────────────
17
18/// The main WASM-facing engine. Holds all state needed for rendering and
19/// interaction: document, renderer, viewport, selection, and undo history.
20#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
21pub struct DrawEngine {
22    document: Document,
23    renderer: Renderer,
24    viewport: ViewState,
25    selected_ids: Vec<String>,
26    selection_box: Option<Bounds>,
27    history: History,
28    pixel_ratio: f32,
29}
30
31// Methods exposed to JS via wasm_bindgen (wasm32 only) AND available natively.
32// We use a single `impl` block with conditional attributes so native tests
33// can call the same functions without wasm-bindgen.
34
35#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
36impl DrawEngine {
37    // ── Constructor ─────────────────────────────────────────────────
38
39    /// Create a new engine with the given canvas dimensions and device pixel ratio.
40    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
41    pub fn new(width: u32, height: u32, pixel_ratio: f32) -> Self {
42        let config = RenderConfig {
43            width,
44            height,
45            pixel_ratio,
46            ..RenderConfig::default()
47        };
48        Self {
49            document: Document::new("untitled".to_string()),
50            renderer: Renderer::new(config),
51            viewport: ViewState::default(),
52            selected_ids: Vec::new(),
53            selection_box: None,
54            history: History::new(),
55            pixel_ratio,
56        }
57    }
58
59    // ── Canvas size ─────────────────────────────────────────────────
60
61    /// Update canvas dimensions (e.g. on window resize).
62    pub fn set_size(&mut self, width: u32, height: u32) {
63        let config = RenderConfig {
64            width,
65            height,
66            pixel_ratio: self.pixel_ratio,
67            ..RenderConfig::default()
68        };
69        self.renderer = Renderer::new(config);
70    }
71
72    // ── Document serialization ──────────────────────────────────────
73
74    /// Load a document from a JSON string.
75    pub fn load_document(&mut self, json: &str) -> bool {
76        match serde_json::from_str::<Document>(json) {
77            Ok(doc) => {
78                self.document = doc;
79                self.history = History::new();
80                self.selected_ids.clear();
81                self.selection_box = None;
82                true
83            }
84            Err(_) => false,
85        }
86    }
87
88    /// Serialize the current document to JSON.
89    pub fn save_document(&self) -> String {
90        serde_json::to_string(&self.document).unwrap_or_default()
91    }
92
93    // ── Rendering ───────────────────────────────────────────────────
94
95    /// Render the current state and return RGBA pixel data.
96    pub fn render(&self) -> Vec<u8> {
97        let sel_refs: Vec<&str> = self.selected_ids.iter().map(|s| s.as_str()).collect();
98        let pixmap = self.renderer.render(
99            &self.document,
100            &self.viewport,
101            &sel_refs,
102            self.selection_box,
103        );
104        pixmap.data().to_vec()
105    }
106
107    /// Return the width of the rendered pixmap in physical pixels.
108    pub fn render_width(&self) -> u32 {
109        (self.renderer.config().width as f32 * self.pixel_ratio) as u32
110    }
111
112    /// Return the height of the rendered pixmap in physical pixels.
113    pub fn render_height(&self) -> u32 {
114        (self.renderer.config().height as f32 * self.pixel_ratio) as u32
115    }
116
117    /// Get text overlay data for browser-native text rendering.
118    /// Returns JSON array of text elements with screen-space positions:
119    /// `[{"x":..,"y":..,"text":"..","fontSize":..,"fontFamily":"..","align":"..","color":"..","opacity":..,"width":..,"height":..}]`
120    pub fn get_text_overlays(&self) -> String {
121        let zoom = self.viewport.zoom;
122        let sx = self.viewport.scroll_x;
123        let sy = self.viewport.scroll_y;
124        let pr = self.pixel_ratio as f64;
125
126        let mut overlays = Vec::new();
127        for el in &self.document.elements {
128            if let Element::Text(t) = el {
129                let screen_x = (t.x * zoom + sx) * pr;
130                let screen_y = (t.y * zoom + sy) * pr;
131                let font_size = t.font.size * zoom * pr;
132                let align = match t.font.align {
133                    draw_core::style::TextAlign::Left => "left",
134                    draw_core::style::TextAlign::Center => "center",
135                    draw_core::style::TextAlign::Right => "right",
136                };
137                // Approximate text bounds for width/height
138                let lines: Vec<&str> = t.text.split('\n').collect();
139                let max_chars = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
140                let width = (max_chars as f64 * t.font.size * 0.6) * zoom * pr;
141                let height = (lines.len() as f64 * t.font.size * 1.2) * zoom * pr;
142
143                overlays.push(format!(
144                    r#"{{"x":{},"y":{},"text":"{}","fontSize":{},"fontFamily":"{}","align":"{}","color":"{}","opacity":{},"width":{},"height":{}}}"#,
145                    screen_x, screen_y,
146                    t.text.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n"),
147                    font_size, t.font.family, align, t.stroke.color, t.opacity,
148                    width, height
149                ));
150            }
151        }
152        format!("[{}]", overlays.join(","))
153    }
154
155    // ── Hit testing ─────────────────────────────────────────────────
156
157    /// Hit test at screen coordinates. Returns element ID or empty string.
158    pub fn hit_test(&self, screen_x: f32, screen_y: f32) -> String {
159        self.renderer
160            .hit_test(&self.document, &self.viewport, screen_x, screen_y)
161            .unwrap_or_default()
162    }
163
164    /// Hit test for resize handles. Returns JSON `{"id":"...","handle":"NorthWest"}`
165    /// or empty string if no handle hit.
166    pub fn hit_test_handle(&self, screen_x: f32, screen_y: f32) -> String {
167        match self
168            .renderer
169            .hit_test_handle(&self.document, &self.viewport, screen_x, screen_y)
170        {
171            Some((id, handle)) => {
172                let handle_str = match handle {
173                    draw_core::HandlePosition::NorthWest => "NorthWest",
174                    draw_core::HandlePosition::NorthEast => "NorthEast",
175                    draw_core::HandlePosition::SouthWest => "SouthWest",
176                    draw_core::HandlePosition::SouthEast => "SouthEast",
177                };
178                format!(r#"{{"id":"{}","handle":"{}"}}"#, id, handle_str)
179            }
180            None => String::new(),
181        }
182    }
183
184    /// Get element IDs within a world-coordinate rectangle. Returns JSON array.
185    pub fn elements_in_rect(&self, x: f64, y: f64, w: f64, h: f64) -> String {
186        let rect = Bounds::new(x, y, w, h);
187        let ids = self
188            .renderer
189            .elements_in_rect(&self.document, &self.viewport, rect);
190        serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string())
191    }
192
193    // ── Viewport ────────────────────────────────────────────────────
194
195    /// Set the viewport (scroll and zoom).
196    pub fn set_viewport(&mut self, scroll_x: f64, scroll_y: f64, zoom: f64) {
197        self.viewport = ViewState {
198            scroll_x,
199            scroll_y,
200            zoom,
201        };
202    }
203
204    /// Convert screen coordinates to world coordinates. Returns JSON `{"x":...,"y":...}`.
205    pub fn screen_to_world(&self, sx: f64, sy: f64) -> String {
206        let wx = (sx - self.viewport.scroll_x) / self.viewport.zoom;
207        let wy = (sy - self.viewport.scroll_y) / self.viewport.zoom;
208        format!(r#"{{"x":{},"y":{}}}"#, wx, wy)
209    }
210
211    pub fn scroll_x(&self) -> f64 {
212        self.viewport.scroll_x
213    }
214
215    pub fn scroll_y(&self) -> f64 {
216        self.viewport.scroll_y
217    }
218
219    pub fn zoom(&self) -> f64 {
220        self.viewport.zoom
221    }
222
223    // ── Selection ───────────────────────────────────────────────────
224
225    /// Set the selected element IDs (JSON array of strings).
226    pub fn set_selection(&mut self, ids_json: &str) {
227        if let Ok(ids) = serde_json::from_str::<Vec<String>>(ids_json) {
228            self.selected_ids = ids;
229        }
230    }
231
232    /// Get the current selection as a JSON array of strings.
233    pub fn get_selection(&self) -> String {
234        serde_json::to_string(&self.selected_ids).unwrap_or_else(|_| "[]".to_string())
235    }
236
237    /// Set the rubber-band selection box (screen coordinates).
238    pub fn set_selection_box(&mut self, x: f64, y: f64, w: f64, h: f64) {
239        self.selection_box = Some(Bounds::new(x, y, w, h));
240    }
241
242    /// Clear the rubber-band selection box.
243    pub fn clear_selection_box(&mut self) {
244        self.selection_box = None;
245    }
246
247    // ── Document mutation ───────────────────────────────────────────
248
249    /// Add an element from JSON. Returns the element ID, or empty string on failure.
250    pub fn add_element(&mut self, json: &str) -> String {
251        match serde_json::from_str::<Element>(json) {
252            Ok(el) => {
253                let id = el.id().to_string();
254                self.history.push(Action::AddElement(Box::new(el.clone())));
255                self.document.add_element(el);
256                id
257            }
258            Err(_) => String::new(),
259        }
260    }
261
262    /// Remove an element by ID. Returns true if the element existed.
263    pub fn remove_element(&mut self, id: &str) -> bool {
264        if let Some(el) = self.document.remove_element(id) {
265            self.history
266                .push(Action::RemoveElement(id.to_string(), Box::new(el)));
267            self.selected_ids.retain(|s| s != id);
268            true
269        } else {
270            false
271        }
272    }
273
274    /// Move an element to absolute position (x, y).
275    pub fn move_element(&mut self, id: &str, x: f64, y: f64) {
276        if let Some(el) = self.document.get_element(id) {
277            let bounds = el.bounds();
278            let dx = x - bounds.x;
279            let dy = y - bounds.y;
280            self.history.push(Action::MoveElement {
281                id: id.to_string(),
282                dx,
283                dy,
284            });
285        }
286        // Apply the move by setting position on the actual element
287        if let Some(el) = self.document.get_element_mut(id) {
288            match el {
289                Element::Rectangle(e) | Element::Ellipse(e) | Element::Diamond(e) => {
290                    e.x = x;
291                    e.y = y;
292                }
293                Element::Line(e) | Element::Arrow(e) => {
294                    e.x = x;
295                    e.y = y;
296                }
297                Element::FreeDraw(e) => {
298                    e.x = x;
299                    e.y = y;
300                }
301                Element::Text(e) => {
302                    e.x = x;
303                    e.y = y;
304                }
305            }
306        }
307    }
308
309    /// Resize an element to the given bounds.
310    pub fn resize_element(&mut self, id: &str, x: f64, y: f64, w: f64, h: f64) {
311        if let Some(el) = self.document.get_element(id) {
312            let b = el.bounds();
313            self.history.push(Action::ResizeElement {
314                id: id.to_string(),
315                old_x: b.x,
316                old_y: b.y,
317                old_width: b.width,
318                old_height: b.height,
319                new_x: x,
320                new_y: y,
321                new_width: w,
322                new_height: h,
323            });
324        }
325        if let Some(el) = self.document.get_element_mut(id) {
326            match el {
327                Element::Rectangle(e) | Element::Ellipse(e) | Element::Diamond(e) => {
328                    e.x = x;
329                    e.y = y;
330                    e.width = w;
331                    e.height = h;
332                }
333                _ => {
334                    // Lines/FreeDraw/Text don't have width/height in the same way;
335                    // for now, just reposition.
336                    match el {
337                        Element::Line(e) | Element::Arrow(e) => {
338                            e.x = x;
339                            e.y = y;
340                        }
341                        Element::FreeDraw(e) => {
342                            e.x = x;
343                            e.y = y;
344                        }
345                        Element::Text(e) => {
346                            e.x = x;
347                            e.y = y;
348                        }
349                        _ => {}
350                    }
351                }
352            }
353        }
354    }
355
356    /// Update an element's style from JSON. The JSON should contain style fields
357    /// (stroke, fill, font, opacity, etc.) to merge into the element.
358    pub fn update_element_style(&mut self, id: &str, style_json: &str) -> bool {
359        // Snapshot "before" for undo
360        let before = self.document.get_element(id).cloned();
361        let before = match before {
362            Some(b) => b,
363            None => return false,
364        };
365
366        // Parse the style update as a generic JSON value
367        let updates: serde_json::Value = match serde_json::from_str(style_json) {
368            Ok(v) => v,
369            Err(_) => return false,
370        };
371
372        // Serialize element, merge updates, deserialize back
373        let mut elem_val = match serde_json::to_value(&before) {
374            Ok(v) => v,
375            Err(_) => return false,
376        };
377
378        if let (Some(obj), Some(upd)) = (elem_val.as_object_mut(), updates.as_object()) {
379            for (k, v) in upd {
380                obj.insert(k.clone(), v.clone());
381            }
382        } else {
383            return false;
384        }
385
386        let updated: Element = match serde_json::from_value(elem_val) {
387            Ok(e) => e,
388            Err(_) => return false,
389        };
390
391        self.history.push(Action::UpdateElement {
392            id: id.to_string(),
393            before: Box::new(before),
394            after: Box::new(updated.clone()),
395        });
396
397        // Replace in document
398        if let Some(el) = self.document.get_element_mut(id) {
399            *el = updated;
400        }
401
402        true
403    }
404
405    /// Get an element as JSON, or empty string if not found.
406    pub fn get_element(&self, id: &str) -> String {
407        match self.document.get_element(id) {
408            Some(el) => serde_json::to_string(el).unwrap_or_default(),
409            None => String::new(),
410        }
411    }
412
413    // ── History ─────────────────────────────────────────────────────
414
415    /// Undo the last action. Returns true if something was undone.
416    pub fn undo(&mut self) -> bool {
417        if let Some(action) = self.history.pop_undo() {
418            self.apply_undo(action);
419            true
420        } else {
421            false
422        }
423    }
424
425    /// Redo the last undone action. Returns true if something was redone.
426    pub fn redo(&mut self) -> bool {
427        if let Some(action) = self.history.pop_redo() {
428            self.apply_redo(action);
429            true
430        } else {
431            false
432        }
433    }
434
435    /// Push a raw action for undo support. The action_json must match the Action
436    /// enum serialization format.
437    pub fn push_action(&mut self, action_json: &str) -> bool {
438        // Actions are not (de)serializable from JSON in the current core API,
439        // so we expose a simple interface: callers should use the mutation
440        // methods above which automatically track history.
441        // This method exists as a placeholder for future extensibility.
442        let _ = action_json;
443        false
444    }
445
446    pub fn can_undo(&self) -> bool {
447        self.history.can_undo()
448    }
449
450    pub fn can_redo(&self) -> bool {
451        self.history.can_redo()
452    }
453
454    // ── Selection helpers ───────────────────────────────────────────
455
456    /// Select all elements (skipping bound text — they follow parent shapes).
457    pub fn select_all(&mut self) {
458        self.selected_ids = self
459            .document
460            .elements
461            .iter()
462            .filter(|el| el.group_id().is_none())
463            .map(|el| el.id().to_string())
464            .collect();
465    }
466
467    /// Clear the selection.
468    pub fn clear_selection(&mut self) {
469        self.selected_ids.clear();
470    }
471
472    /// Add an element to the selection.
473    pub fn add_to_selection(&mut self, id: &str) {
474        if !self.selected_ids.iter().any(|s| s == id) {
475            self.selected_ids.push(id.to_string());
476        }
477    }
478
479    /// Remove an element from the selection.
480    pub fn remove_from_selection(&mut self, id: &str) {
481        self.selected_ids.retain(|s| s != id);
482    }
483
484    /// Check if an element is selected.
485    pub fn is_selected(&self, id: &str) -> bool {
486        self.selected_ids.iter().any(|s| s == id)
487    }
488
489    // ── Bulk operations ────────────────────────────────────────────
490
491    /// Remove multiple elements by ID (JSON array). Pushes a batch undo action.
492    pub fn remove_elements(&mut self, ids_json: &str) {
493        if let Ok(ids) = serde_json::from_str::<Vec<String>>(ids_json) {
494            for id in &ids {
495                if let Some(el) = self.document.remove_element(id) {
496                    self.history
497                        .push(Action::RemoveElement(id.to_string(), Box::new(el)));
498                    self.selected_ids.retain(|s| s != id);
499                }
500            }
501        }
502    }
503
504    /// Get all element IDs as a JSON array.
505    pub fn get_all_element_ids(&self) -> String {
506        let ids: Vec<&str> = self.document.elements.iter().map(|el| el.id()).collect();
507        serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string())
508    }
509
510    /// Get elements with a specific group_id (for bound text). Returns JSON array of IDs.
511    pub fn get_elements_by_group(&self, group_id: &str) -> String {
512        let ids: Vec<&str> = self
513            .document
514            .elements
515            .iter()
516            .filter(|el| el.group_id() == Some(group_id))
517            .map(|el| el.id())
518            .collect();
519        serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string())
520    }
521
522    // ── Z-ordering ─────────────────────────────────────────────────
523
524    /// Move an element to the front (top of draw order).
525    pub fn reorder_to_front(&mut self, id: &str) {
526        if let Some(idx) = self.document.elements.iter().position(|e| e.id() == id) {
527            let el = self.document.elements.remove(idx);
528            self.document.elements.push(el);
529        }
530    }
531
532    /// Move an element to the back (bottom of draw order).
533    pub fn reorder_to_back(&mut self, id: &str) {
534        if let Some(idx) = self.document.elements.iter().position(|e| e.id() == id) {
535            let el = self.document.elements.remove(idx);
536            self.document.elements.insert(0, el);
537        }
538    }
539
540    /// Move an element one position forward in draw order.
541    pub fn reorder_forward(&mut self, id: &str) {
542        if let Some(idx) = self.document.elements.iter().position(|e| e.id() == id)
543            && idx < self.document.elements.len() - 1
544        {
545            self.document.elements.swap(idx, idx + 1);
546        }
547    }
548
549    /// Move an element one position backward in draw order.
550    pub fn reorder_backward(&mut self, id: &str) {
551        if let Some(idx) = self.document.elements.iter().position(|e| e.id() == id)
552            && idx > 0
553        {
554            self.document.elements.swap(idx, idx - 1);
555        }
556    }
557
558    // ── Grid toggle ────────────────────────────────────────────────
559
560    /// Show or hide the grid.
561    pub fn set_show_grid(&mut self, show: bool) {
562        let mut config = self.renderer.config().clone();
563        config.show_grid = show;
564        self.renderer = Renderer::new(config);
565    }
566
567    // ── Document serialization (extended) ──────────────────────────
568
569    /// Get the full document JSON with updated modified_at for saving.
570    pub fn get_document_json_for_save(&self) -> String {
571        // Clone and update modified_at
572        let mut doc = self.document.clone();
573        doc.modified_at = chrono::Utc::now().to_rfc3339();
574        serde_json::to_string(&doc).unwrap_or_default()
575    }
576
577    // ── Document metadata ──────────────────────────────────────────
578
579    pub fn set_document_id(&mut self, id: &str) {
580        self.document.id = id.to_string();
581    }
582
583    pub fn document_id(&self) -> String {
584        self.document.id.clone()
585    }
586
587    pub fn set_created_at(&mut self, ts: &str) {
588        self.document.created_at = ts.to_string();
589    }
590
591    // ── Document info ───────────────────────────────────────────────
592
593    pub fn document_name(&self) -> String {
594        self.document.name.clone()
595    }
596
597    pub fn set_document_name(&mut self, name: &str) {
598        self.document.name = name.to_string();
599    }
600
601    pub fn element_count(&self) -> usize {
602        self.document.elements.len()
603    }
604}
605
606// ── Private helpers (not exposed to JS) ─────────────────────────────────
607
608impl DrawEngine {
609    fn apply_undo(&mut self, action: Action) {
610        match action {
611            Action::AddElement(el) => {
612                self.document.remove_element(el.id());
613            }
614            Action::RemoveElement(_, el) => {
615                self.document.add_element(*el);
616            }
617            Action::MoveElement { id, dx, dy } => {
618                if let Some(el) = self.document.get_element_mut(&id) {
619                    match el {
620                        Element::Rectangle(e) | Element::Ellipse(e) | Element::Diamond(e) => {
621                            e.x -= dx;
622                            e.y -= dy;
623                        }
624                        Element::Line(e) | Element::Arrow(e) => {
625                            e.x -= dx;
626                            e.y -= dy;
627                        }
628                        Element::FreeDraw(e) => {
629                            e.x -= dx;
630                            e.y -= dy;
631                        }
632                        Element::Text(e) => {
633                            e.x -= dx;
634                            e.y -= dy;
635                        }
636                    }
637                }
638            }
639            Action::ResizeElement {
640                id,
641                old_x,
642                old_y,
643                old_width,
644                old_height,
645                ..
646            } => {
647                if let Some(Element::Rectangle(e) | Element::Ellipse(e) | Element::Diamond(e)) =
648                    self.document.get_element_mut(&id)
649                {
650                    e.x = old_x;
651                    e.y = old_y;
652                    e.width = old_width;
653                    e.height = old_height;
654                }
655            }
656            Action::UpdateElement { id, before, .. } => {
657                if let Some(el) = self.document.get_element_mut(&id) {
658                    *el = *before;
659                }
660            }
661            Action::Batch(actions) => {
662                for a in actions.into_iter().rev() {
663                    self.apply_undo(a);
664                }
665            }
666        }
667    }
668
669    fn apply_redo(&mut self, action: Action) {
670        match action {
671            Action::AddElement(el) => {
672                self.document.add_element(*el);
673            }
674            Action::RemoveElement(id, _) => {
675                self.document.remove_element(&id);
676            }
677            Action::MoveElement { id, dx, dy } => {
678                if let Some(el) = self.document.get_element_mut(&id) {
679                    match el {
680                        Element::Rectangle(e) | Element::Ellipse(e) | Element::Diamond(e) => {
681                            e.x += dx;
682                            e.y += dy;
683                        }
684                        Element::Line(e) | Element::Arrow(e) => {
685                            e.x += dx;
686                            e.y += dy;
687                        }
688                        Element::FreeDraw(e) => {
689                            e.x += dx;
690                            e.y += dy;
691                        }
692                        Element::Text(e) => {
693                            e.x += dx;
694                            e.y += dy;
695                        }
696                    }
697                }
698            }
699            Action::ResizeElement {
700                id,
701                new_x,
702                new_y,
703                new_width,
704                new_height,
705                ..
706            } => {
707                if let Some(Element::Rectangle(e) | Element::Ellipse(e) | Element::Diamond(e)) =
708                    self.document.get_element_mut(&id)
709                {
710                    e.x = new_x;
711                    e.y = new_y;
712                    e.width = new_width;
713                    e.height = new_height;
714                }
715            }
716            Action::UpdateElement { id, after, .. } => {
717                if let Some(el) = self.document.get_element_mut(&id) {
718                    *el = *after;
719                }
720            }
721            Action::Batch(actions) => {
722                for a in actions {
723                    self.apply_redo(a);
724                }
725            }
726        }
727    }
728}
729
730// ── Native tests ────────────────────────────────────────────────────────
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735
736    #[test]
737    fn test_new_engine() {
738        let engine = DrawEngine::new(800, 600, 2.0);
739        assert_eq!(engine.render_width(), 1600);
740        assert_eq!(engine.render_height(), 1200);
741        assert_eq!(engine.element_count(), 0);
742        assert_eq!(engine.document_name(), "untitled");
743    }
744
745    #[test]
746    fn test_set_size() {
747        let mut engine = DrawEngine::new(800, 600, 1.0);
748        engine.set_size(1920, 1080);
749        assert_eq!(engine.render_width(), 1920);
750        assert_eq!(engine.render_height(), 1080);
751    }
752
753    #[test]
754    fn test_load_save_document() {
755        let mut engine = DrawEngine::new(800, 600, 1.0);
756
757        let doc = Document::new("test doc".to_string());
758        let json = serde_json::to_string(&doc).unwrap();
759
760        assert!(engine.load_document(&json));
761        assert_eq!(engine.document_name(), "test doc");
762
763        let saved = engine.save_document();
764        assert!(saved.contains("test doc"));
765
766        // Invalid JSON should return false
767        assert!(!engine.load_document("not json"));
768    }
769
770    #[test]
771    fn test_render_returns_pixel_data() {
772        let engine = DrawEngine::new(100, 100, 1.0);
773        let data = engine.render();
774        // 100x100 pixels * 4 bytes (RGBA)
775        assert_eq!(data.len(), 100 * 100 * 4);
776    }
777
778    #[test]
779    fn test_viewport() {
780        let mut engine = DrawEngine::new(800, 600, 1.0);
781        engine.set_viewport(100.0, 200.0, 2.0);
782        assert_eq!(engine.scroll_x(), 100.0);
783        assert_eq!(engine.scroll_y(), 200.0);
784        assert_eq!(engine.zoom(), 2.0);
785    }
786
787    #[test]
788    fn test_screen_to_world() {
789        let mut engine = DrawEngine::new(800, 600, 1.0);
790        engine.set_viewport(100.0, 50.0, 2.0);
791
792        let result = engine.screen_to_world(300.0, 250.0);
793        // wx = (300 - 100) / 2 = 100
794        // wy = (250 - 50) / 2 = 100
795        assert!(result.contains("100"));
796    }
797
798    #[test]
799    fn test_add_and_remove_element() {
800        let mut engine = DrawEngine::new(800, 600, 1.0);
801
802        let json = r#"{"type":"Rectangle","id":"r1","x":10,"y":20,"width":100,"height":50}"#;
803        let id = engine.add_element(json);
804        assert_eq!(id, "r1");
805        assert_eq!(engine.element_count(), 1);
806
807        // Get element back
808        let el_json = engine.get_element("r1");
809        assert!(el_json.contains("r1"));
810
811        // Remove
812        assert!(engine.remove_element("r1"));
813        assert_eq!(engine.element_count(), 0);
814        assert!(!engine.remove_element("r1")); // already gone
815    }
816
817    #[test]
818    fn test_move_element() {
819        let mut engine = DrawEngine::new(800, 600, 1.0);
820        engine
821            .add_element(r#"{"type":"Rectangle","id":"r1","x":10,"y":20,"width":100,"height":50}"#);
822
823        engine.move_element("r1", 50.0, 60.0);
824        let el_json = engine.get_element("r1");
825        assert!(el_json.contains("50"));
826    }
827
828    #[test]
829    fn test_resize_element() {
830        let mut engine = DrawEngine::new(800, 600, 1.0);
831        engine
832            .add_element(r#"{"type":"Rectangle","id":"r1","x":0,"y":0,"width":100,"height":100}"#);
833
834        engine.resize_element("r1", 10.0, 10.0, 200.0, 150.0);
835        let el_json = engine.get_element("r1");
836        assert!(el_json.contains("200"));
837        assert!(el_json.contains("150"));
838    }
839
840    #[test]
841    fn test_selection() {
842        let mut engine = DrawEngine::new(800, 600, 1.0);
843        engine.set_selection(r#"["r1","r2"]"#);
844        let sel = engine.get_selection();
845        assert!(sel.contains("r1"));
846        assert!(sel.contains("r2"));
847    }
848
849    #[test]
850    fn test_selection_box() {
851        let mut engine = DrawEngine::new(800, 600, 1.0);
852        engine.set_selection_box(10.0, 20.0, 100.0, 50.0);
853        // Just verify it doesn't panic; the box is used during render
854        let data = engine.render();
855        assert!(!data.is_empty());
856
857        engine.clear_selection_box();
858        let data = engine.render();
859        assert!(!data.is_empty());
860    }
861
862    #[test]
863    fn test_undo_redo_add() {
864        let mut engine = DrawEngine::new(800, 600, 1.0);
865        engine
866            .add_element(r#"{"type":"Rectangle","id":"r1","x":0,"y":0,"width":100,"height":100}"#);
867        assert_eq!(engine.element_count(), 1);
868        assert!(engine.can_undo());
869
870        assert!(engine.undo());
871        assert_eq!(engine.element_count(), 0);
872        assert!(engine.can_redo());
873
874        assert!(engine.redo());
875        assert_eq!(engine.element_count(), 1);
876    }
877
878    #[test]
879    fn test_undo_redo_remove() {
880        let mut engine = DrawEngine::new(800, 600, 1.0);
881        engine
882            .add_element(r#"{"type":"Rectangle","id":"r1","x":0,"y":0,"width":100,"height":100}"#);
883        engine.remove_element("r1");
884        assert_eq!(engine.element_count(), 0);
885
886        assert!(engine.undo()); // undo remove
887        assert_eq!(engine.element_count(), 1);
888
889        assert!(engine.redo()); // redo remove
890        assert_eq!(engine.element_count(), 0);
891    }
892
893    #[test]
894    fn test_undo_move() {
895        let mut engine = DrawEngine::new(800, 600, 1.0);
896        engine.add_element(
897            r#"{"type":"Rectangle","id":"r1","x":10,"y":20,"width":100,"height":100}"#,
898        );
899        engine.move_element("r1", 50.0, 60.0);
900
901        // Undo the move
902        engine.undo();
903        let el_json = engine.get_element("r1");
904        // Should be back at (10, 20)
905        assert!(el_json.contains("\"x\":10"));
906        assert!(el_json.contains("\"y\":20"));
907    }
908
909    #[test]
910    fn test_update_element_style() {
911        let mut engine = DrawEngine::new(800, 600, 1.0);
912        engine
913            .add_element(r#"{"type":"Rectangle","id":"r1","x":0,"y":0,"width":100,"height":100}"#);
914
915        let ok = engine.update_element_style("r1", r#"{"opacity": 0.5}"#);
916        assert!(ok);
917
918        let el_json = engine.get_element("r1");
919        assert!(el_json.contains("0.5"));
920
921        // Undo should restore opacity
922        engine.undo();
923        let el_json = engine.get_element("r1");
924        assert!(el_json.contains("1.0") || el_json.contains("1"));
925    }
926
927    #[test]
928    fn test_hit_test_empty() {
929        let engine = DrawEngine::new(800, 600, 1.0);
930        let result = engine.hit_test(400.0, 300.0);
931        assert!(result.is_empty());
932    }
933
934    #[test]
935    fn test_elements_in_rect_empty() {
936        let engine = DrawEngine::new(800, 600, 1.0);
937        let result = engine.elements_in_rect(0.0, 0.0, 100.0, 100.0);
938        assert_eq!(result, "[]");
939    }
940
941    #[test]
942    fn test_document_name() {
943        let mut engine = DrawEngine::new(800, 600, 1.0);
944        engine.set_document_name("my drawing");
945        assert_eq!(engine.document_name(), "my drawing");
946    }
947
948    #[test]
949    fn test_push_action_placeholder() {
950        let mut engine = DrawEngine::new(800, 600, 1.0);
951        assert!(!engine.push_action("{}"));
952    }
953}