mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
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:
parent
9f63c6e8ea
commit
eb48fbd180
16 changed files with 137 additions and 43 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue