Export current document as SVG when pressing Ctrl+Shift+S (#160)

* Export current document when pressing Ctrl+Shift+S

* Use a blob for download

* Add Ctrl + E shortcut, match on lower case

* Don't mount element in DOM

* Polish some keybindings

* Add initialization for MappingEntries

* Implement svg coloring

* Add newline after svg tag

* Add spaces to svg style format

* Fix more svg formatting

* Add space before />

* Remove duplicate whitespace
This commit is contained in:
TrueDoctor 2021-05-28 20:43:51 +02:00 committed by Keavon Chambers
parent 805116b031
commit c0e60a21d3
16 changed files with 137 additions and 43 deletions

View File

@ -166,7 +166,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool } from "../../response-handler"; import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, ExportDocument } from "../../response-handler";
import LayoutRow from "../layout/LayoutRow.vue"; import LayoutRow from "../layout/LayoutRow.vue";
import LayoutCol from "../layout/LayoutCol.vue"; import LayoutCol from "../layout/LayoutCol.vue";
import WorkingColors from "../widgets/WorkingColors.vue"; import WorkingColors from "../widgets/WorkingColors.vue";
@ -223,12 +223,27 @@ export default defineComponent({
} }
todo(toolIndex); todo(toolIndex);
}, },
download(filename: string, svgData: string) {
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
const svgUrl = URL.createObjectURL(svgBlob);
const element = document.createElement("a");
element.href = svgUrl;
element.setAttribute("download", filename);
element.style.display = "none";
element.click();
},
}, },
mounted() { mounted() {
registerResponseHandler(ResponseType.UpdateCanvas, (responseData: Response) => { registerResponseHandler(ResponseType.UpdateCanvas, (responseData: Response) => {
const updateData = responseData as UpdateCanvas; const updateData = responseData as UpdateCanvas;
if (updateData) this.viewportSvg = updateData.document; if (updateData) this.viewportSvg = updateData.document;
}); });
registerResponseHandler(ResponseType.ExportDocument, (responseData: Response) => {
const updateData = responseData as ExportDocument;
if (updateData) this.download("canvas.svg", updateData.document);
});
registerResponseHandler(ResponseType.SetActiveTool, (responseData: Response) => { registerResponseHandler(ResponseType.SetActiveTool, (responseData: Response) => {
const toolData = responseData as SetActiveTool; const toolData = responseData as SetActiveTool;
if (toolData) this.activeTool = toolData.tool_name; if (toolData) this.activeTool = toolData.tool_name;

View File

@ -12,6 +12,7 @@ declare global {
export enum ResponseType { export enum ResponseType {
UpdateCanvas = "UpdateCanvas", UpdateCanvas = "UpdateCanvas",
ExportDocument = "ExportDocument",
ExpandFolder = "ExpandFolder", ExpandFolder = "ExpandFolder",
CollapseFolder = "CollapseFolder", CollapseFolder = "CollapseFolder",
SetActiveTool = "SetActiveTool", SetActiveTool = "SetActiveTool",
@ -52,6 +53,8 @@ function parseResponse(responseType: string, data: any): Response {
return newSetActiveTool(data.SetActiveTool); return newSetActiveTool(data.SetActiveTool);
case "UpdateCanvas": case "UpdateCanvas":
return newUpdateCanvas(data.UpdateCanvas); return newUpdateCanvas(data.UpdateCanvas);
case "ExportDocument":
return newExportDocument(data.ExportDocument);
default: default:
throw new Error(`Unrecognized origin/responseType pair: ${origin}, ${responseType}`); throw new Error(`Unrecognized origin/responseType pair: ${origin}, ${responseType}`);
} }
@ -77,6 +80,15 @@ function newUpdateCanvas(input: any): UpdateCanvas {
}; };
} }
export interface ExportDocument {
document: string;
}
function newExportDocument(input: any): UpdateCanvas {
return {
document: input.document,
};
}
export type DocumentChanged = {}; export type DocumentChanged = {};
function newDocumentChanged(_: any): DocumentChanged { function newDocumentChanged(_: any): DocumentChanged {
return {}; return {};

View File

@ -77,16 +77,33 @@ pub fn translate_append_mode(name: &str) -> Option<SelectAppendMode> {
pub fn translate_key(name: &str) -> Key { pub fn translate_key(name: &str) -> Key {
log::trace!("pressed key: {}", name); log::trace!("pressed key: {}", name);
use Key::*; use Key::*;
match name { match name.to_lowercase().as_str() {
"a" => KeyA,
"b" => KeyB,
"c" => KeyC,
"d" => KeyD,
"e" => KeyE, "e" => KeyE,
"v" => KeyV, "f" => KeyF,
"g" => KeyG,
"h" => KeyH,
"i" => KeyI,
"j" => KeyJ,
"k" => KeyK,
"l" => KeyL, "l" => KeyL,
"p" => KeyP,
"r" => KeyR,
"m" => KeyM, "m" => KeyM,
"n" => KeyN,
"o" => KeyO,
"p" => KeyP,
"q" => KeyQ,
"r" => KeyR,
"s" => KeyS,
"t" => KeyT,
"u" => KeyU,
"v" => KeyV,
"w" => KeyW,
"x" => KeyX, "x" => KeyX,
"z" => KeyZ,
"y" => KeyY, "y" => KeyY,
"z" => KeyZ,
"0" => Key0, "0" => Key0,
"1" => Key1, "1" => Key1,
"2" => Key2, "2" => Key2,
@ -97,12 +114,13 @@ pub fn translate_key(name: &str) -> Key {
"7" => Key7, "7" => Key7,
"8" => Key8, "8" => Key8,
"9" => Key9, "9" => Key9,
"Enter" => KeyEnter, "enter" => KeyEnter,
"Shift" => KeyShift, "shift" => KeyShift,
"CapsLock" => KeyCaps, // When using linux + chrome + the neo keyboard layout, the shift key is recognized as caps
"Control" => KeyControl, "capslock" => KeyShift,
"Alt" => KeyAlt, "control" => KeyControl,
"Escape" => KeyEscape, "alt" => KeyAlt,
"escape" => KeyEscape,
_ => UnknownKey, _ => UnknownKey,
} }
} }

View File

@ -55,7 +55,7 @@ impl Color {
pub fn components(&self) -> (f32, f32, f32, f32) { pub fn components(&self) -> (f32, f32, f32, f32) {
(self.red, self.green, self.blue, self.alpha) (self.red, self.green, self.blue, self.alpha)
} }
pub fn as_hex(&self) -> String { pub fn rgba_hex(&self) -> String {
format!( format!(
"{:02X?}{:02X?}{:02X?}{:02X?}", "{:02X?}{:02X?}{:02X?}{:02X?}",
(self.r() * 255.) as u8, (self.r() * 255.) as u8,
@ -64,4 +64,7 @@ impl Color {
(self.a() * 255.) as u8, (self.a() * 255.) as u8,
) )
} }
pub fn rgb_hex(&self) -> String {
format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8,)
}
} }

View File

@ -22,7 +22,7 @@ impl LayerData for Circle {
fn render(&mut self, svg: &mut String) { fn render(&mut self, svg: &mut String) {
let _ = write!( let _ = write!(
svg, svg,
r#"<circle cx="{}" cy="{}" r="{}" {} />"#, r#"<circle cx="{}" cy="{}" r="{}"{} />"#,
self.shape.center.x, self.shape.center.x,
self.shape.center.y, self.shape.center.y,
self.shape.radius, self.shape.radius,

View File

@ -25,7 +25,7 @@ impl LayerData for Ellipse {
let _ = write!( let _ = write!(
svg, svg,
r#"<ellipse cx="0" cy="0" rx="{}" ry="{}" transform="translate({} {}) rotate({})" {} />"#, r#"<ellipse cx="0" cy="0" rx="{}" ry="{}" transform="translate({} {}) rotate({})"{} />"#,
rx, rx,
ry, ry,
cx, cx,

View File

@ -23,6 +23,6 @@ impl LayerData for Line {
let kurbo::Point { x: x1, y: y1 } = self.shape.p0; let kurbo::Point { x: x1, y: y1 } = self.shape.p0;
let kurbo::Point { x: x2, y: y2 } = self.shape.p1; let kurbo::Point { x: x2, y: y2 } = self.shape.p1;
let _ = write!(svg, r#"<line x1="{}" y1="{}" x2="{}" y2="{}" {} />"#, x1, y1, x2, y2, self.style.render(),); let _ = write!(svg, r#"<line x1="{}" y1="{}" x2="{}" y2="{}"{} />"#, x1, y1, x2, y2, self.style.render(),);
} }
} }

View File

@ -24,10 +24,13 @@ impl LayerData for PolyLine {
return; return;
} }
let _ = write!(svg, r#"<polyline points=""#); let _ = write!(svg, r#"<polyline points=""#);
for p in &self.points { let mut points = self.points.iter();
let _ = write!(svg, " {:.3} {:.3}", p.x, p.y); let first = points.next().unwrap();
let _ = write!(svg, "{:.3} {:.3}", first.x, first.y);
for point in points {
let _ = write!(svg, " {:.3} {:.3}", point.x, point.y);
} }
let _ = write!(svg, r#"" {}/>"#, self.style.render()); let _ = write!(svg, r#""{} />"#, self.style.render());
} }
} }
@ -41,5 +44,5 @@ fn polyline_should_render() {
let mut svg = String::new(); let mut svg = String::new();
polyline.render(&mut svg); polyline.render(&mut svg);
assert_eq!(r#"<polyline points=" 3.000 4.124 1.000 5.540" style="stroke: #00FF00FF;stroke-width:0.4;"/>"#, svg); assert_eq!(r##"<polyline points="3.000 4.124 1.000 5.540" stroke="#00FF00" stroke-width="0.4" />"##, svg);
} }

View File

@ -22,7 +22,7 @@ impl LayerData for Rect {
fn render(&mut self, svg: &mut String) { fn render(&mut self, svg: &mut String) {
let _ = write!( let _ = write!(
svg, svg,
r#"<rect x="{}" y="{}" width="{}" height="{}" {} />"#, r#"<rect x="{}" y="{}" width="{}" height="{}"{} />"#,
self.shape.min_x(), self.shape.min_x(),
self.shape.min_y(), self.shape.min_y(),
self.shape.width(), self.shape.width(),

View File

@ -26,7 +26,7 @@ impl LayerData for Shape {
fn render(&mut self, svg: &mut String) { fn render(&mut self, svg: &mut String) {
let _ = write!( let _ = write!(
svg, svg,
r#"<polygon points="{}" transform="translate({} {}) scale({} {})" {} />"#, r#"<polygon points="{}" transform="translate({} {}) scale({} {})"{} />"#,
self.shape, self.shape,
self.bounding_rect.origin().x, self.bounding_rect.origin().x,
self.bounding_rect.origin().y, self.bounding_rect.origin().y,

View File

@ -1,5 +1,14 @@
use crate::color::Color; use crate::color::Color;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const OPACITY_PERCISION: usize = 3;
fn format_opacity(name: &str, opacity: f32) -> String {
if (opacity - 1.).abs() > 10f32.powi(-(OPACITY_PERCISION as i32)) {
format!(r#" {}-opacity="{:.percision$}""#, name, opacity, percision = OPACITY_PERCISION)
} else {
String::new()
}
}
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
@ -15,8 +24,8 @@ impl Fill {
} }
pub fn render(&self) -> String { pub fn render(&self) -> String {
match self.color { match self.color {
Some(c) => format!("fill: #{};", c.as_hex()), Some(c) => format!(r##" fill="#{}"{}"##, c.rgb_hex(), format_opacity("fill", c.a())),
None => "fill: none;".to_string(), None => r#" fill="none""#.to_string(),
} }
} }
} }
@ -33,7 +42,7 @@ impl Stroke {
Self { color, width } Self { color, width }
} }
pub fn render(&self) -> String { pub fn render(&self) -> String {
format!("stroke: #{};stroke-width:{};", self.color.as_hex(), self.width) format!(r##" stroke="#{}"{} stroke-width="{}""##, self.color.rgb_hex(), format_opacity("stroke", self.color.a()), self.width)
} }
} }
@ -49,7 +58,7 @@ impl PathStyle {
} }
pub fn render(&self) -> String { pub fn render(&self) -> String {
format!( format!(
"style=\"{}{}\"", "{}{}",
match self.fill { match self.fill {
Some(fill) => fill.render(), Some(fill) => fill.render(),
None => String::new(), None => String::new(),

View File

@ -39,6 +39,9 @@ impl ShapePoints {
} }
} }
// TODO: The display impl and iter impl share large amounts of code and should be refactored. (Display should use the Iterator)
// TODO: Once that is done, the trailing space from the display impl should be removed
// Also consider implementing index
impl std::fmt::Display for ShapePoints { impl std::fmt::Display for ShapePoints {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn rotate(v: &Vec2, theta: f64) -> Vec2 { fn rotate(v: &Vec2, theta: f64) -> Vec2 {

View File

@ -15,6 +15,7 @@ pub enum DocumentMessage {
ToggleLayerVisibility(Vec<LayerId>), ToggleLayerVisibility(Vec<LayerId>),
ToggleLayerExpansion(Vec<LayerId>), ToggleLayerExpansion(Vec<LayerId>),
SelectDocument(usize), SelectDocument(usize),
ExportDocument,
RenderDocument, RenderDocument,
Undo, Undo,
} }
@ -69,6 +70,17 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
assert!(id < self.documents.len(), "Tried to select a document that was not initialized"); assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
self.active_document = id; self.active_document = id;
} }
ExportDocument => responses.push_back(
FrontendMessage::ExportDocument {
//TODO: Add canvas size instead of using 1080p per default
document: format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080">{}{}</svg>"#,
"\n",
self.active_document_mut().document.render_root(),
),
}
.into(),
),
ToggleLayerVisibility(path) => { ToggleLayerVisibility(path) => {
responses.push_back(DocumentOperation::ToggleVisibility { path }.into()); responses.push_back(DocumentOperation::ToggleVisibility { path }.into());
} }
@ -96,5 +108,5 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()), message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()),
} }
} }
advertise_actions!(DocumentMessageDiscriminant; Undo, RenderDocument); advertise_actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument);
} }

View File

@ -11,6 +11,7 @@ pub enum FrontendMessage {
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> }, ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
SetActiveTool { tool_name: String }, SetActiveTool { tool_name: String },
UpdateCanvas { document: String }, UpdateCanvas { document: String },
ExportDocument { document: String },
EnableTextInput, EnableTextInput,
DisableTextInput, DisableTextInput,
} }

View File

@ -37,6 +37,14 @@ impl KeyMappingEntries {
fn push(&mut self, entry: MappingEntry) { fn push(&mut self, entry: MappingEntry) {
self.0.push(entry) self.0.push(entry)
} }
fn key_array() -> [Self; NUMBER_OF_KEYS] {
let mut array: [KeyMappingEntries; NUMBER_OF_KEYS] = unsafe { std::mem::zeroed() };
for key in array.iter_mut() {
*key = KeyMappingEntries::default();
}
array
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -52,7 +60,7 @@ macro_rules! modifiers {
let mut state = KeyStates::new(); let mut state = KeyStates::new();
$( $(
state.set(Key::$m as usize); state.set(Key::$m as usize);
),* )*
state state
}}; }};
} }
@ -70,8 +78,8 @@ macro_rules! entry {
macro_rules! mapping { macro_rules! mapping {
//[$(<action=$action:expr; message=$key:expr; $(modifiers=[$($m:ident),* $(,)?];)?>)*] => {{ //[$(<action=$action:expr; message=$key:expr; $(modifiers=[$($m:ident),* $(,)?];)?>)*] => {{
[$($entry:expr),* $(,)?] => {{ [$($entry:expr),* $(,)?] => {{
let mut up: [KeyMappingEntries; NUMBER_OF_KEYS] = Default::default(); let mut up = KeyMappingEntries::key_array();
let mut down: [KeyMappingEntries; NUMBER_OF_KEYS] = Default::default(); let mut down = KeyMappingEntries::key_array();
let mut pointer_move: KeyMappingEntries = Default::default(); let mut pointer_move: KeyMappingEntries = Default::default();
$( $(
let arr = match $entry.trigger { let arr = match $entry.trigger {
@ -98,8 +106,6 @@ impl Default for Mapping {
entry! {action=RectangleMessage::Abort, key_down=KeyEscape}, entry! {action=RectangleMessage::Abort, key_down=KeyEscape},
entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyShift}, entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyShift}, entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyCaps},
// Ellipse // Ellipse
entry! {action=EllipseMessage::Center, key_down=KeyAlt}, entry! {action=EllipseMessage::Center, key_down=KeyAlt},
entry! {action=EllipseMessage::UnCenter, key_up=KeyAlt}, entry! {action=EllipseMessage::UnCenter, key_up=KeyAlt},
@ -110,8 +116,6 @@ impl Default for Mapping {
entry! {action=EllipseMessage::Abort, key_down=KeyEscape}, entry! {action=EllipseMessage::Abort, key_down=KeyEscape},
entry! {action=EllipseMessage::LockAspectRatio, key_down=KeyShift}, entry! {action=EllipseMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyShift}, entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=EllipseMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyCaps},
// Shape // Shape
entry! {action=ShapeMessage::Center, key_down=KeyAlt}, entry! {action=ShapeMessage::Center, key_down=KeyAlt},
entry! {action=ShapeMessage::UnCenter, key_up=KeyAlt}, entry! {action=ShapeMessage::UnCenter, key_up=KeyAlt},
@ -122,8 +126,6 @@ impl Default for Mapping {
entry! {action=ShapeMessage::Abort, key_down=KeyEscape}, entry! {action=ShapeMessage::Abort, key_down=KeyEscape},
entry! {action=ShapeMessage::LockAspectRatio, key_down=KeyShift}, entry! {action=ShapeMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyShift}, entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=ShapeMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyCaps},
// Line // Line
entry! {action=LineMessage::Center, key_down=KeyAlt}, entry! {action=LineMessage::Center, key_down=KeyAlt},
entry! {action=LineMessage::UnCenter, key_up=KeyAlt}, entry! {action=LineMessage::UnCenter, key_up=KeyAlt},
@ -136,8 +138,6 @@ impl Default for Mapping {
entry! {action=LineMessage::UnlockAngle, key_up=KeyControl}, entry! {action=LineMessage::UnlockAngle, key_up=KeyControl},
entry! {action=LineMessage::SnapToAngle, key_down=KeyShift}, entry! {action=LineMessage::SnapToAngle, key_down=KeyShift},
entry! {action=LineMessage::UnSnapToAngle, key_up=KeyShift}, entry! {action=LineMessage::UnSnapToAngle, key_up=KeyShift},
entry! {action=LineMessage::SnapToAngle, key_down=KeyCaps},
entry! {action=LineMessage::UnSnapToAngle, key_up=KeyCaps},
// Pen // Pen
entry! {action=PenMessage::MouseMove, message=InputMapperMessage::PointerMove}, entry! {action=PenMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=PenMessage::DragStart, key_down=Lmb}, entry! {action=PenMessage::DragStart, key_down=Lmb},
@ -147,6 +147,8 @@ impl Default for Mapping {
entry! {action=PenMessage::Confirm, key_down=KeyEnter}, entry! {action=PenMessage::Confirm, key_down=KeyEnter},
// Document Actions // Document Actions
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]}, entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
// Tool Actions // Tool Actions
entry! {action=ToolMessage::SelectTool(ToolType::Rectangle), key_down=KeyM}, entry! {action=ToolMessage::SelectTool(ToolType::Rectangle), key_down=KeyM},
entry! {action=ToolMessage::SelectTool(ToolType::Ellipse), key_down=KeyE}, entry! {action=ToolMessage::SelectTool(ToolType::Ellipse), key_down=KeyE},

View File

@ -16,16 +16,32 @@ pub enum Key {
Mmb, Mmb,
// Keyboard keys // Keyboard keys
KeyR, KeyA,
KeyM, KeyB,
KeyC,
KeyD,
KeyE, KeyE,
KeyF,
KeyG,
KeyH,
KeyI,
KeyJ,
KeyK,
KeyL, KeyL,
KeyM,
KeyN,
KeyO,
KeyP, KeyP,
KeyQ,
KeyR,
KeyS,
KeyT,
KeyU,
KeyV, KeyV,
KeyW,
KeyX, KeyX,
KeyZ,
KeyY, KeyY,
KeyEnter, KeyZ,
Key0, Key0,
Key1, Key1,
Key2, Key2,
@ -36,8 +52,8 @@ pub enum Key {
Key7, Key7,
Key8, Key8,
Key9, Key9,
KeyEnter,
KeyShift, KeyShift,
KeyCaps,
KeyControl, KeyControl,
KeyAlt, KeyAlt,
KeyEscape, KeyEscape,