# SVG Shape File Loading + SVG Editor ## Goal - SVG şekilleri diskten `.svg` dosyaları olarak yüklenip kullanılabilsin - Kullanıcı kendi SVG'lerini shapes klasörüne kopyalayıp toolbox'ta görebilsin - Built-in şekiller (`svg_templates.rs`) diskte yoksa yedek olarak kullanılsın, varsa diskteki dosyalar öncelikli - SVG şekilleri buton ikonları olarak da kullanılsın (dinamik yükleme) - SVG Editor (node tabanlı path düzenleyici) planı ## Design Decisions | Decision | Choice | |----------|--------| | Shapes directory | App data dizini (`~/.local/share/hcie/shapes/`) — ilk çalıştırmada built-in SVGs buraya kopyalanır | | Custom shape tool | `Tool::CustomShape(u32)` — `u32` index, `Copy` korunur. Serialization ile uyumlu | | Toolbox UX | Mevcut slot sistemine ek olarak dinamik "Custom" slot. Tıklayınca popup'ta diskteki tüm SVG'ler listelenir | | Icon rendering | `usvg` parse + `resvg` render → küçük RGBA pixmap → `egui::ColorImage` / `iced::image::Handle` | | SVG Editor scope (V1) | Node editor: path noktalarını sürükle, yeni nokta ekle/sil, canlı preview, SVG'yi kaydet | ## Affected Crates | # | Crate | Files | |---|-------|-------| | 1 | `hcie-protocol` (locked) | `src/tools.rs` — `Tool::CustomShape(u32)` variant | | 2 | `hcie-vector` (locked) | `src/svg_templates.rs` — `all_templates()` fn | | 3 | `hcie-engine-api` (locked) | `src/shape_catalog.rs` NEW, `src/lib.rs` — init + re-export | | 4 | `hcie-gui-egui` (open) | `tools/toolbox.rs`, `canvas/mod.rs`, `app/mod.rs` | | 5 | `hcie-iced-gui` (open) | `sidebar/mod.rs`, `app.rs`, `canvas/mod.rs` | --- ## Phase 1 — SVG File Loading ### Step 1 — `hcie-protocol/src/tools.rs` — Add `Tool::CustomShape(u32)` ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] pub enum Tool { #[default] Pen, // ... existing variants ... CustomShape(u32), } ``` Update: - `label()` — CustomShape(index) => `"Custom Shape"` - `icon()` — CustomShape(index) => `"○"` (placeholder, real icon loaded dynamically) - `is_shape_tool()` — return true - `allowed_layer_type()` — Vector - `is_vector()` — return true - `has_separator_before()` — return true (after existing shape tools) Do NOT add to `ALL` const — custom shapes are dynamic. ### Step 2 — `hcie-vector/src/svg_templates.rs` — Add `all_templates()` ```rust pub struct TemplateEntry { pub kind: &'static str, pub svg: &'static str, } pub fn all_templates() -> Vec { vec![ TemplateEntry { kind: "arrow", svg: &arrow_svg(false) }, TemplateEntry { kind: "star", svg: &star_svg(5, 0.5) }, // ... all 11 kinds ... ] } ``` Each `svg` is the actual SVG string from the respective template function. The `kind` matches the filename stem that will be written to disk. ### Step 3 — `hcie-vector/src/svg_render.rs` — Fix viewBox scaling Current code assumes `viewBox="0 0 1 1"`. Custom SVGs have arbitrary viewBox values. Fix `svg_to_points` to read the actual viewBox from `usvg::Tree::size()`: ```rust pub fn svg_to_points( svg: &str, x1: f32, y1: f32, x2: f32, y2: f32, ) -> Result>, String> { // ... parse SVG ... let tree_size = tree.size(); // (width, height) from viewBox let svg_w = tree_size.width().max(1.0); let svg_h = tree_size.height().max(1.0); let target_w = (x2 - x1).abs().max(1.0); let target_h = (y2 - y1).abs().max(1.0); let ox = x1.min(x2); let oy = y1.min(y2); // Scale: px -> ox + (px / svg_w) * target_w collect_paths(tree.root(), &mut result, ox, oy, target_w, target_h, svg_w, svg_h); } ``` This makes any viewBox work — both our normalized `0 0 1 1` templates and user SVGs with arbitrary viewBox. ### Step 4 — `hcie-engine-api/src/shape_catalog.rs` (NEW) ```rust pub struct ShapeCatalog { entries: Vec, shapes_dir: PathBuf, builtin_kinds: HashMap, // kind -> SVG string } pub struct ShapeEntry { pub index: u32, pub kind: String, // filename stem (e.g. "star", "my_shape") pub label: String, // human-readable (filename stem, capitalized) pub svg: String, // full SVG content } ``` **Methods:** - `new(shapes_dir: PathBuf, builtins: HashMap) -> Self` - Creates directory if not exists - Writes built-in SVGs to disk (one `.svg` file per template) — SKIP if file already exists - Scans `shapes_dir` for `*.svg` files - For each file: read content, create `ShapeEntry` - If a built-in file exists on disk AND in templates, disk version wins (user may have edited it) - If a file has the same name as a built-in and is MISSING from disk, fall back to builtin SVG string - Result: entries list = all found SVG files (disk) + builtins not on disk - `refresh(&mut self)` — rescans directory - `len(&self) -> usize` - `get(&self, index: u32) -> Option<&ShapeEntry>` - `all(&self) -> &[ShapeEntry]` - `shapes_dir(&self) -> &Path` - `write_to_disk(&self, path: &Path, svg: &str) -> io::Result<()>` — for SVG Editor save **Built-in SVGs written to disk:** Files written on first run to `shapes/`: - `arrow.svg`, `star.svg`, `rhombus.svg`, `cylinder.svg`, `heart.svg`, `bubble.svg`, `gear.svg`, `cross.svg`, `crescent.svg`, `bolt.svg`, `arrow4.svg` Each file contains the SVG string from the corresponding template function (with default parameters: star=5 points, arrow=non-thick). ### Step 5 — `hcie-engine-api/src/lib.rs` — Wire up ShapeCatalog Add to `Engine` struct: ```rust pub shape_catalog: ShapeCatalog, ``` Init in `Engine::new_with_options`: ```rust let shapes_dir = /* app data dir */; let builtins = hcie_vector::svg_templates::all_templates(); self.shape_catalog = ShapeCatalog::new(shapes_dir, builtins); ``` Re-export: ```rust pub use crate::shape_catalog::{ShapeCatalog, ShapeEntry}; ``` Add helper: ```rust impl Engine { pub fn refresh_shape_catalog(&mut self) { let builtins = hcie_vector::svg_templates::all_templates(); self.shape_catalog = ShapeCatalog::new( self.shape_catalog.shapes_dir().to_path_buf(), builtins, ); } } ``` ### Step 6 — `hcie-egui-app/.../tools/toolbox.rs` — Custom shape slot After the static TOOL_SLOTS rendering, add a dynamic section for custom shapes: ```rust // After all static slots: let catalog = &app.documents[app.active_doc].doc.shape_catalog; if catalog.len() > 0 { // Render a slot header "Custom" or with an SVG icon // The current custom tool selection is Tool::CustomShape(active_idx) // Show a popup with all catalog entries, each with: // - SVG icon rendered to 24x24 image // - Label text // When user clicks an entry, set active_tool = Tool::CustomShape(entry.index) } ``` Icon rendering helper: ```rust fn load_svg_icon(svg: &str, size: u32) -> Option { // 1. Parse SVG with usvg // 2. Render to tiny_skia Pixmap with resvg // 3. Convert to egui::ColorImage // 4. Cache result } ``` Cache the rendered icons so we don't re-render every frame. ### Step 7 — `hcie-egui-app/.../canvas/mod.rs` — CustomShape shape creation In the shape creation match, add: ```rust Tool::CustomShape(idx) => { let catalog = /* get shape catalog */; if let Some(entry) = catalog.get(*idx) { // Create shape from entry.svg content, NOT from template VectorShape::SvgShape { x1, y1, x2, y2, kind: entry.kind.clone(), svg: entry.svg.clone(), stroke: config.stroke_size, color: [0, 0, 0, 255], fill_color: [255, 255, 255, 255], fill: config.fill_shape, angle: 0.0, opacity: config.opacity, hardness: config.hardness, } } } ``` ### Step 8 — `hcie-iced-app/.../sidebar/mod.rs` — Custom shape slot Same pattern: render a custom shapes section after static slots. Each entry: - SVG icon rendered via `resvg` to `iced::widget::image::Handle` - Click to set `Tool::CustomShape(idx)` - Tool parameter panel updates when custom shape is selected ### Step 9 — `hcie-iced-app/.../app.rs` + `canvas/mod.rs` Handle `Tool::CustomShape(idx)` in shape creation match (same as Step 7 pattern). --- ## Phase 2 — SVG Editor (Node-based) ### Step 10 — New crate: `hcie-svg-editor` or module in `hcie-engine-api` **Location:** `hcie-engine-api/src/svg_editor.rs` (keeps it in the API crate, no new crate needed) **Public API:** ```rust /// Represents a single node in an SVG path. #[derive(Debug, Clone)] pub struct SvgNode { pub x: f32, pub y: f32, } /// Parsed editable representation of an SVG shape. #[derive(Debug, Clone)] pub struct SvgEditable { pub view_box: (f32, f32, f32, f32), // min_x, min_y, width, height pub polygons: Vec>, // one per subpath } impl SvgEditable { /// Parse an SVG string into editable nodes. pub fn from_svg(svg: &str) -> Result; /// Reconstruct SVG string from edited nodes. pub fn to_svg(&self) -> String; /// Add a node at the midpoint of edge `edge_idx` in polygon `poly_idx`. pub fn add_node(&mut self, poly_idx: usize, edge_idx: usize); /// Remove a node (only if polygon would still have ≥3 nodes for closed paths). pub fn remove_node(&mut self, poly_idx: usize, node_idx: usize); /// Move a node to new (x, y). pub fn move_node(&mut self, poly_idx: usize, node_idx: usize, x: f32, y: f32); /// Normalize all coordinates to 0..1 relative to bounding box. pub fn normalize(&mut self); } ``` **Implementation details:** `from_svg`: 1. Parse SVG with `usvg::Tree::from_data` 2. Extract viewBox from `tree.size()` 3. For each ``, extract segments via `path.data().segments()` 4. Convert MoveTo/LineTo/Close segments to `SvgNode` list 5. Reject quadratic/cubic beziers for V1 (or tessellate and mark as non-editable) `to_svg`: 1. Build `` with viewBox 2. For each polygon, emit `` ### Step 11 — `hcie-egui-app/.../panels/svg_editor.rs` (NEW) egui panel that opens when editing an `SvgShape`: - Button in the shape's property panel: "Open SVG Editor" - Modal panel or dock widget - Shows: - Preview canvas (renders the SVG at current edit state) - Node list with draggable handles on the preview - Add/Delete node buttons - On save: updates the shape's `svg` field - If shape came from a disk file: "Save to disk" button that writes to `shapes_dir` ### Step 12 — `hcie-iced-app/.../panels/svg_editor.rs` (NEW) Same SVG editor UI using iced widgets. ### Step 13 — `hcie-egui-app/.../app/mod.rs` — Wire SVG editor Add SVG editor state to `HcieApp`: ```rust pub svg_editor_state: Option, pub struct SvgEditorState { pub shape_layer_id: u64, pub shape_idx: usize, pub editable: SvgEditable, pub preview_pixels: egui::ColorImage, } ``` Property panel: when editing an `SvgShape`, add "Edit SVG" button that opens the editor. --- ## Validation ```bash # Compile all cargo check -p hcie-protocol -p hcie-vector -p hcie-engine-api -p hcie-gui-egui -p hcie-iced-gui # Test cargo test -p hcie-protocol ``` ## Manual verification 1. Run the app, verify shapes/ directory is created with SVG files 2. Verify all built-in shapes still render correctly 3. Add a new SVG file to shapes/, verify it appears in the toolbox 4. Edit an SVG file in shapes/, verify the change appears in the tool 5. Draw a custom SVG shape on canvas 6. Open SVG Editor, move nodes, save, verify shape updates ## Edge Cases - shapes/ directory doesn't exist → create it, write built-ins - SVG file has invalid syntax → skip with warning, don't crash - User deletes all SVGs → fall back to built-in templates - 100+ custom SVGs → scrollable popup, lazy icon rendering - SVG has no viewBox → use `0 0 1 1` as default - SVG has curves (C, Q) → V1 treats them as non-editable, shows "Edit not supported" warning