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 GitHub
parent 9f63c6e8ea
commit eb48fbd180
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 137 additions and 43 deletions

View file

@ -166,7 +166,7 @@
<script lang="ts">
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 LayoutCol from "../layout/LayoutCol.vue";
import WorkingColors from "../widgets/WorkingColors.vue";
@ -223,12 +223,27 @@ export default defineComponent({
}
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() {
registerResponseHandler(ResponseType.UpdateCanvas, (responseData: Response) => {
const updateData = responseData as UpdateCanvas;
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) => {
const toolData = responseData as SetActiveTool;
if (toolData) this.activeTool = toolData.tool_name;

View file

@ -12,6 +12,7 @@ declare global {
export enum ResponseType {
UpdateCanvas = "UpdateCanvas",
ExportDocument = "ExportDocument",
ExpandFolder = "ExpandFolder",
CollapseFolder = "CollapseFolder",
SetActiveTool = "SetActiveTool",
@ -52,6 +53,8 @@ function parseResponse(responseType: string, data: any): Response {
return newSetActiveTool(data.SetActiveTool);
case "UpdateCanvas":
return newUpdateCanvas(data.UpdateCanvas);
case "ExportDocument":
return newExportDocument(data.ExportDocument);
default:
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 = {};
function newDocumentChanged(_: any): DocumentChanged {
return {};

View file

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

View file

@ -55,7 +55,7 @@ impl Color {
pub fn components(&self) -> (f32, f32, f32, f32) {
(self.red, self.green, self.blue, self.alpha)
}
pub fn as_hex(&self) -> String {
pub fn rgba_hex(&self) -> String {
format!(
"{:02X?}{:02X?}{:02X?}{:02X?}",
(self.r() * 255.) as u8,
@ -64,4 +64,7 @@ impl Color {
(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) {
let _ = write!(
svg,
r#"<circle cx="{}" cy="{}" r="{}" {} />"#,
r#"<circle cx="{}" cy="{}" r="{}"{} />"#,
self.shape.center.x,
self.shape.center.y,
self.shape.radius,

View file

@ -25,7 +25,7 @@ impl LayerData for Ellipse {
let _ = write!(
svg,
r#"<ellipse cx="0" cy="0" rx="{}" ry="{}" transform="translate({} {}) rotate({})" {} />"#,
r#"<ellipse cx="0" cy="0" rx="{}" ry="{}" transform="translate({} {}) rotate({})"{} />"#,
rx,
ry,
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: 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;
}
let _ = write!(svg, r#"<polyline points=""#);
for p in &self.points {
let _ = write!(svg, " {:.3} {:.3}", p.x, p.y);
let mut points = self.points.iter();
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();
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) {
let _ = write!(
svg,
r#"<rect x="{}" y="{}" width="{}" height="{}" {} />"#,
r#"<rect x="{}" y="{}" width="{}" height="{}"{} />"#,
self.shape.min_x(),
self.shape.min_y(),
self.shape.width(),

View file

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

View file

@ -1,5 +1,14 @@
use crate::color::Color;
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)]
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
@ -15,8 +24,8 @@ impl Fill {
}
pub fn render(&self) -> String {
match self.color {
Some(c) => format!("fill: #{};", c.as_hex()),
None => "fill: none;".to_string(),
Some(c) => format!(r##" fill="#{}"{}"##, c.rgb_hex(), format_opacity("fill", c.a())),
None => r#" fill="none""#.to_string(),
}
}
}
@ -33,7 +42,7 @@ impl Stroke {
Self { color, width }
}
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 {
format!(
"style=\"{}{}\"",
"{}{}",
match self.fill {
Some(fill) => fill.render(),
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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn rotate(v: &Vec2, theta: f64) -> Vec2 {

View file

@ -15,6 +15,7 @@ pub enum DocumentMessage {
ToggleLayerVisibility(Vec<LayerId>),
ToggleLayerExpansion(Vec<LayerId>),
SelectDocument(usize),
ExportDocument,
RenderDocument,
Undo,
}
@ -69,6 +70,17 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
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) => {
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()),
}
}
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> },
SetActiveTool { tool_name: String },
UpdateCanvas { document: String },
ExportDocument { document: String },
EnableTextInput,
DisableTextInput,
}

View file

@ -37,6 +37,14 @@ impl KeyMappingEntries {
fn push(&mut self, entry: MappingEntry) {
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)]
@ -52,7 +60,7 @@ macro_rules! modifiers {
let mut state = KeyStates::new();
$(
state.set(Key::$m as usize);
),*
)*
state
}};
}
@ -70,8 +78,8 @@ macro_rules! entry {
macro_rules! mapping {
//[$(<action=$action:expr; message=$key:expr; $(modifiers=[$($m:ident),* $(,)?];)?>)*] => {{
[$($entry:expr),* $(,)?] => {{
let mut up: [KeyMappingEntries; NUMBER_OF_KEYS] = Default::default();
let mut down: [KeyMappingEntries; NUMBER_OF_KEYS] = Default::default();
let mut up = KeyMappingEntries::key_array();
let mut down = KeyMappingEntries::key_array();
let mut pointer_move: KeyMappingEntries = Default::default();
$(
let arr = match $entry.trigger {
@ -98,8 +106,6 @@ impl Default for Mapping {
entry! {action=RectangleMessage::Abort, key_down=KeyEscape},
entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyCaps},
// Ellipse
entry! {action=EllipseMessage::Center, key_down=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::LockAspectRatio, key_down=KeyShift},
entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=EllipseMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyCaps},
// Shape
entry! {action=ShapeMessage::Center, key_down=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::LockAspectRatio, key_down=KeyShift},
entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=ShapeMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyCaps},
// Line
entry! {action=LineMessage::Center, key_down=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::SnapToAngle, key_down=KeyShift},
entry! {action=LineMessage::UnSnapToAngle, key_up=KeyShift},
entry! {action=LineMessage::SnapToAngle, key_down=KeyCaps},
entry! {action=LineMessage::UnSnapToAngle, key_up=KeyCaps},
// Pen
entry! {action=PenMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=PenMessage::DragStart, key_down=Lmb},
@ -147,6 +147,8 @@ impl Default for Mapping {
entry! {action=PenMessage::Confirm, key_down=KeyEnter},
// Document Actions
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
entry! {action=ToolMessage::SelectTool(ToolType::Rectangle), key_down=KeyM},
entry! {action=ToolMessage::SelectTool(ToolType::Ellipse), key_down=KeyE},

View file

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