Implement outline view mode (#401)

* Created wasm binding to action's of the radio buttons which control the view mode
Added entry to DocumentMessage Enum

* draw in wireframe mode by changing parameters on each shape
added functions/changed behavior to do as above
not working yet
   - newly added shapes should be drawn in wireframe
   - setting fill to "none" on a path does not only draw an outline
      - maybe the stroke width is 0?

* Wire frame view mostly functional for ellipses
   - Need to implement for all shapes
   - BUG: shapes don't immediatley update upon changing view-mode

* Fixed: active document now updates after view mode swap

* The Pros:
   - wire frame mode effects all shapes correctly

The Cons:
   - wire frame mode effects everything, including things that maybe shouldn't be, like select boxes and pen lines

* wire frame view no longer effects overlay layers

* Fixed: While in wireframe view the pen tool will draw regular thickness lines.

* some commenting

* Fixed potential bug:
   In layer/file system with a Folder layer with a sub-layer that is also
   a Folder cache_dirty must be set in order for all shapes to update properly

* refactored code to use ViewMode enum names throughout

* Changed: All wireframe lines are blank
cargo fmt

* Wireframe thickness doesn't change as a result of zooming
   - Added DocumentMessage::ReRenderDocument, which marks layers as dirty and renders with the updated render-string
   - All "zoom" messages in the movement_handler send a re-render message
   - while in wireframe view, the "render-transform" of all shapes includes the root layer transform

Added getter/setter methods for graphene::Document::view_mode

* cargo fmt

* wireframe now has proper thickness after "Zoom Canvas to Fit all" action

* Refactored
   - Changed FrontendMessage::UpdateCanvas to RenderDocument message to allow for lazy evaluation
   - Created DocumentOperation::SetViewMode to be more consistent with existing code
   - removed log statement
   - Added constants for empty fill and thin-black stroke

* cargo fmt

* Removed ReRenderDocument message

* cargo fmt

* Fixes as suggested by TrueDoctor

* clean up merge
cargo fmt

* Refactor:
   moved view_mode to DocumentMessageHandler

* Polishing

* changed those two comments

* Remove unknown todo comment

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
caleb 2021-12-24 16:04:58 -07:00 committed by Keavon Chambers
parent d2b0411295
commit 1594b9c61d
14 changed files with 158 additions and 68 deletions

View File

@ -2,4 +2,4 @@
## Mask mode
At any time while in the viewport, <kbd>Tab</kbd> may be pressed to enter mask mode. The underlying canvas seen before entering this mode is still shown, but masks are drawn as marching ants (or other optional overlays) above the main document content. While in this mode, an island layer group is provided as the destination for drawing new mask layers using the regular set of tools. The Layer Panel also still shows the underlying main document, which lets the user select layers as contextual inputs for tools that are aware of input layers, like the Fill Tool. Rather than showing the full-color shapes over the main document canvas, they are overlaid in wireframe view mode and surrounded by a marching ants marquee outline. The mask group may be isolated (meaning it becomes the render output to the viewport, and a breadcrumb trail is shown leading from the document to the isolated layer/group) which makes the viewport output show the mask in grayscale and has the Layer Panel host the contents of the mask group. While in mask mode, the working colors are temporarily replaced with a grayscale pair. Certain tools, such as the Freehand Tool and Pen Tool, may default to a "closed" form in mask mode by turning off stroke and setting fill to white in order to provide functionality akin to the lasso or polygonal lasso selection tools. <kbd>Tab</kbd> may be hit again to exit mask mode, but the marching ants still show up. However now, all tools used and commands performed will take into account the working mask. <kbd>Ctrl</kbd><kbd>D</kbd> will discard the working mask.
At any time while in the viewport, <kbd>Tab</kbd> may be pressed to enter mask mode. The underlying canvas seen before entering this mode is still shown, but masks are drawn as marching ants (or other optional overlays) above the main document content. While in this mode, an island layer group is provided as the destination for drawing new mask layers using the regular set of tools. The Layer Panel also still shows the underlying main document, which lets the user select layers as contextual inputs for tools that are aware of input layers, like the Fill Tool. Rather than showing the full-color shapes over the main document canvas, they are overlaid in outline view mode and surrounded by a marching ants marquee outline. The mask group may be isolated (meaning it becomes the render output to the viewport, and a breadcrumb trail is shown leading from the document to the isolated layer/group) which makes the viewport output show the mask in grayscale and has the Layer Panel host the contents of the mask group. While in mask mode, the working colors are temporarily replaced with a grayscale pair. Certain tools, such as the Freehand Tool and Pen Tool, may default to a "closed" form in mask mode by turning off stroke and setting fill to white in order to provide functionality akin to the lasso or polygonal lasso selection tools. <kbd>Tab</kbd> may be hit again to exit mask mode, but the marching ants still show up. However now, all tools used and commands performed will take into account the working mask. <kbd>Ctrl</kbd><kbd>D</kbd> will discard the working mask.

View File

@ -16,8 +16,8 @@ use kurbo::PathSeg;
use log::warn;
use serde::{Deserialize, Serialize};
use graphene::layers::BlendMode;
use graphene::{document::Document as GrapheneDocument, layers::LayerDataType, DocumentError, LayerId};
use graphene::layers::{style::ViewMode, BlendMode, LayerDataType};
use graphene::{document::Document as GrapheneDocument, DocumentError, LayerId};
use graphene::{DocumentResponse, Operation as DocumentOperation};
type DocumentSave = (GrapheneDocument, HashMap<Vec<LayerId>, LayerData>);
@ -68,6 +68,7 @@ pub struct DocumentMessageHandler {
movement_handler: MovementMessageHandler,
transform_layer_handler: TransformLayerMessageHandler,
pub snapping_enabled: bool,
pub view_mode: ViewMode,
}
impl Default for DocumentMessageHandler {
@ -83,6 +84,7 @@ impl Default for DocumentMessageHandler {
movement_handler: MovementMessageHandler::default(),
transform_layer_handler: TransformLayerMessageHandler::default(),
snapping_enabled: true,
view_mode: ViewMode::default(),
}
}
}
@ -122,6 +124,9 @@ pub enum DocumentMessage {
ExportDocument,
SaveDocument,
RenderDocument,
DirtyRenderDocument,
DirtyRenderDocumentInOutlineView,
SetViewMode(ViewMode),
Undo,
Redo,
DocumentHistoryBackward,
@ -162,6 +167,7 @@ impl DocumentMessageHandler {
movement_handler: MovementMessageHandler::default(),
transform_layer_handler: TransformLayerMessageHandler::default(),
snapping_enabled: true,
view_mode: ViewMode::default(),
};
document.graphene_document.root.transform = document.layerdata(&[]).calculate_offset_transform(ipp.viewport_bounds.size() / 2.);
document
@ -479,7 +485,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
size.x,
size.y,
"\n",
self.graphene_document.render_root()
self.graphene_document.render_root(self.view_mode)
),
name,
}
@ -570,6 +576,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_front(DocumentOperation::DeleteLayer { path }.into());
}
}
SetViewMode(mode) => {
self.view_mode = mode;
responses.push_front(DocumentMessage::DirtyRenderDocument.into());
}
DuplicateSelectedLayers => {
self.backup(responses);
for path in self.selected_layers_sorted() {
@ -659,7 +669,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(FolderChanged(vec![]).into());
}
FolderChanged(path) => {
let _ = self.graphene_document.render_root();
let _ = self.graphene_document.render_root(self.view_mode);
responses.extend([LayerChanged(path).into(), DocumentStructureChanged.into()]);
}
DocumentStructureChanged => {
@ -672,7 +682,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
(!overlay).then(|| FrontendMessage::UpdateLayer { data: entry }.into())
}));
}
DispatchOperation(op) => match self.graphene_document.handle_operation(&op) {
Ok(Some(document_responses)) => {
for response in document_responses {
@ -702,7 +711,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
RenderDocument => {
responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.graphene_document.render_root(),
document: self.graphene_document.render_root(self.view_mode),
}
.into(),
);
@ -720,8 +729,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
let scrollbar_size = viewport_size / bounds_length;
let log = root_layerdata.scale.log2();
let ruler_inverval = if log < 0. { 100. * 2_f64.powf(-log.ceil()) } else { 100. / 2_f64.powf(log.ceil()) };
let ruler_spacing = ruler_inverval * root_layerdata.scale;
let ruler_interval = if log < 0. { 100. * 2_f64.powf(-log.ceil()) } else { 100. / 2_f64.powf(log.ceil()) };
let ruler_spacing = ruler_interval * root_layerdata.scale;
let ruler_origin = self.graphene_document.root.transform.transform_point2(DVec2::ZERO);
@ -738,12 +747,22 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
FrontendMessage::UpdateRulers {
origin: ruler_origin.into(),
spacing: ruler_spacing,
interval: ruler_inverval,
interval: ruler_interval,
}
.into(),
);
}
DirtyRenderDocument => {
// Mark all non-overlay caches as dirty
GrapheneDocument::visit_all_shapes(&mut self.graphene_document.root, &mut |_| {});
responses.push_back(DocumentMessage::RenderDocument.into());
}
DirtyRenderDocumentInOutlineView => {
if self.view_mode == ViewMode::Outline {
responses.push_front(DocumentMessage::DirtyRenderDocument.into());
}
}
NudgeSelectedLayers(x, y) => {
self.backup(responses);
for path in self.selected_layers().map(|path| path.to_vec()) {

View File

@ -1,10 +1,7 @@
use crate::consts::VIEWPORT_ROTATE_SNAP_INTERVAL;
use glam::{DAffine2, DVec2};
use graphene::layers::{BlendMode, LayerDataType};
use graphene::{
layers::{Layer, LayerData as DocumentLayerData},
LayerId,
};
use graphene::layers::{style::ViewMode, BlendMode, Layer, LayerData as DocumentLayerData, LayerDataType};
use graphene::LayerId;
use serde::{
ser::{SerializeSeq, SerializeStruct},
Deserialize, Serialize,
@ -64,7 +61,7 @@ pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &La
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
let mut thumbnail = String::new();
layer.data.clone().render(&mut thumbnail, &mut vec![transform]);
layer.data.clone().render(&mut thumbnail, &mut vec![transform], ViewMode::Normal);
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
format!(

View File

@ -1,16 +1,15 @@
pub use super::layer_panel::*;
use super::LayerData;
pub use crate::document::layer_panel::*;
use crate::document::{DocumentMessage, LayerData};
use crate::message_prelude::*;
use crate::{
consts::{VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_LEVELS, VIEWPORT_ZOOM_MOUSE_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_WHEEL_RATE},
input::{mouse::ViewportBounds, mouse::ViewportPosition, InputPreprocessor},
};
use glam::DVec2;
use graphene::document::Document;
use graphene::layers::style::ViewMode;
use graphene::Operation as DocumentOperation;
use glam::DVec2;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
@ -132,21 +131,27 @@ impl MessageHandler<MovementMessage, (&mut LayerData, &Document, &InputPreproces
layerdata.scale = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
responses.push_back(ToolMessage::SelectedLayersChanged.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses);
}
IncreaseCanvasZoom => {
// TODO: Eliminate redundant code by making this call SetCanvasZoom
layerdata.scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > layerdata.scale).unwrap_or(&layerdata.scale);
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
responses.push_back(ToolMessage::SelectedLayersChanged.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses);
}
DecreaseCanvasZoom => {
// TODO: Eliminate redundant code by making this call SetCanvasZoom
layerdata.scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < layerdata.scale).unwrap_or(&layerdata.scale);
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
responses.push_back(ToolMessage::SelectedLayersChanged.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses);
}
WheelCanvasZoom => {
// TODO: Eliminate redundant code by making this call SetCanvasZoom
let scroll = ipp.mouse.scroll_delta.scroll_delta();
let mouse = ipp.mouse.position;
let viewport_bounds = ipp.viewport_bounds.size();
@ -165,6 +170,7 @@ impl MessageHandler<MovementMessage, (&mut LayerData, &Document, &InputPreproces
layerdata.translation += transformed_delta;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
responses.push_back(ToolMessage::SelectedLayersChanged.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses);
}
WheelCanvasTranslate { use_y_as_x } => {
@ -199,6 +205,7 @@ impl MessageHandler<MovementMessage, (&mut LayerData, &Document, &InputPreproces
layerdata.scale *= new_scale;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
responses.push_back(ToolMessage::SelectedLayersChanged.into());
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses);
}
}

View File

@ -264,6 +264,9 @@ export default defineComponent({
setSnap(newStatus: boolean) {
this.editor.instance.set_snapping(newStatus);
},
setViewMode(newViewMode: string) {
this.editor.instance.set_view_mode(newViewMode);
},
viewportResize() {
const canvas = this.$refs.canvas as HTMLElement;
// Get the width and height rounded up to the nearest even number because resizing is centered and dividing an odd number by 2 for centering causes antialiasing
@ -358,8 +361,8 @@ export default defineComponent({
],
];
const viewModeEntries: RadioEntries = [
{ value: "normal", icon: "ViewModeNormal", tooltip: "View Mode: Normal" },
{ value: "outline", icon: "ViewModeOutline", tooltip: "View Mode: Outline", action: () => this.dialog.comingSoon(319) },
{ value: "normal", icon: "ViewModeNormal", tooltip: "View Mode: Normal", action: () => this.setViewMode("Normal") },
{ value: "outline", icon: "ViewModeOutline", tooltip: "View Mode: Outline", action: () => this.setViewMode("Outline") },
{ value: "pixels", icon: "ViewModePixels", tooltip: "View Mode: Pixels", action: () => this.dialog.comingSoon(320) },
];

View File

@ -5,16 +5,15 @@
use std::cell::Cell;
use crate::helpers::Error;
use crate::type_translators::{translate_blend_mode, translate_key, translate_tool_type};
use crate::type_translators::{translate_blend_mode, translate_key, translate_tool_type, translate_view_mode};
use crate::{EDITOR_HAS_CRASHED, EDITOR_INSTANCES};
use editor::consts::FILE_SAVE_SUFFIX;
use editor::input::input_preprocessor::ModifierKeys;
use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::message_prelude::*;
use editor::misc::EditorError;
use editor::tool::{tool_options::ToolOptions, tools, ToolType};
use editor::Color;
use editor::LayerId;
use editor::{message_prelude::*, Editor};
use editor::{Color, Editor, LayerId};
use wasm_bindgen::prelude::*;
// To avoid wasm-bindgen from checking mutable reference issues using WasmRefCell
@ -379,6 +378,15 @@ impl JsEditorHandle {
self.dispatch(message);
}
/// Set the view mode to change the way layers are drawn in the viewport
pub fn set_view_mode(&self, new_mode: String) -> Result<(), JsValue> {
match translate_view_mode(new_mode.as_str()) {
Some(view_mode) => self.dispatch(DocumentMessage::SetViewMode(view_mode)),
None => return Err(Error::new("Invalid view mode").into()),
};
Ok(())
}
/// Sets the zoom to the value
pub fn set_canvas_zoom(&self, new_zoom: f64) {
let message = MovementMessage::SetCanvasZoom(new_zoom);

View File

@ -1,7 +1,7 @@
use crate::helpers::match_string_to_enum;
use editor::input::keyboard::Key;
use editor::tool::ToolType;
use graphene::layers::BlendMode;
use graphene::layers::{style::ViewMode, BlendMode};
pub fn translate_tool_type(name: &str) -> Option<ToolType> {
use ToolType::*;
@ -126,3 +126,12 @@ pub fn translate_key(name: &str) -> Key {
_ => UnknownKey,
}
}
pub fn translate_view_mode(name: &str) -> Option<ViewMode> {
Some(match name {
"Normal" => ViewMode::Normal,
"Outline" => ViewMode::Outline,
"Pixels" => ViewMode::Pixels,
_ => return None,
})
}

5
graphene/src/consts.rs Normal file
View File

@ -0,0 +1,5 @@
use crate::color::Color;
// RENDERING
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
pub const LAYER_OUTLINE_STROKE_WIDTH: f32 = 1.;

View File

@ -8,7 +8,7 @@ use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use crate::{
layers::{self, Folder, Layer, LayerData, LayerDataType, Shape},
layers::{self, style::ViewMode, Folder, Layer, LayerData, LayerDataType, Shape},
DocumentError, DocumentResponse, LayerId, Operation, Quad,
};
@ -36,8 +36,8 @@ impl Document {
}
/// Wrapper around render, that returns the whole document as a Response.
pub fn render_root(&mut self) -> String {
self.root.render(&mut vec![]);
pub fn render_root(&mut self, mode: ViewMode) -> String {
self.root.render(&mut vec![], mode);
self.root.cache.clone()
}
@ -203,6 +203,28 @@ impl Document {
Ok(())
}
/// Visit each layer recursively, applies modify_shape to each non-overlay Shape
pub fn visit_all_shapes<F: FnMut(&mut Shape)>(layer: &mut Layer, modify_shape: &mut F) -> bool {
match layer.data {
LayerDataType::Shape(ref mut shape) => {
if !layer.overlay {
modify_shape(shape);
// This layer should be updated on next render pass
layer.cache_dirty = true;
}
}
LayerDataType::Folder(ref mut folder) => {
for sub_layer in folder.layers_mut() {
if Document::visit_all_shapes(sub_layer, modify_shape) {
layer.cache_dirty = true;
}
}
}
}
layer.cache_dirty
}
/// Adds a new layer to the folder specified by `path`.
/// Passing a negative `insert_index` indexes relative to the end.
/// -1 is equivalent to adding the layer to the top.

View File

@ -1,6 +1,6 @@
use glam::DVec2;
use crate::{DocumentError, LayerId, Quad};
use crate::{layers::style::ViewMode, DocumentError, LayerId, Quad};
use super::{Layer, LayerData, LayerDataType};
@ -15,9 +15,9 @@ pub struct Folder {
}
impl LayerData for Folder {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>) {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode) {
for layer in &mut self.layers {
let _ = writeln!(svg, "{}", layer.render(transforms));
let _ = writeln!(svg, "{}", layer.render(transforms, view_mode));
}
}

View File

@ -1,4 +1,5 @@
pub mod style;
use style::ViewMode;
use glam::DAffine2;
use glam::{DMat2, DVec2};
@ -18,7 +19,7 @@ use serde::{Deserialize, Serialize};
use std::fmt::Write;
pub trait LayerData {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>);
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode);
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>);
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>;
}
@ -46,12 +47,14 @@ impl LayerDataType {
}
impl LayerData for LayerDataType {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>) {
self.inner_mut().render(svg, transforms)
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode) {
self.inner_mut().render(svg, transforms, view_mode)
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
self.inner().intersects_quad(quad, path, intersections)
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
self.inner().bounding_box(transform)
}
@ -102,14 +105,14 @@ impl Layer {
}
}
pub fn render(&mut self, transforms: &mut Vec<DAffine2>) -> &str {
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) -> &str {
if !self.visible {
return "";
}
if self.cache_dirty {
transforms.push(self.transform);
self.thumbnail_cache.clear();
self.data.render(&mut self.thumbnail_cache, transforms);
self.data.render(&mut self.thumbnail_cache, transforms, if self.overlay { ViewMode::Normal } else { view_mode });
self.cache.clear();
let _ = writeln!(self.cache, r#"<g transform="matrix("#);

View File

@ -1,18 +1,18 @@
use glam::DAffine2;
use glam::DMat2;
use glam::DVec2;
use kurbo::Affine;
use kurbo::BezPath;
use kurbo::Shape as KurboShape;
use crate::intersection::intersect_quad_bez_path;
use crate::layers::{
style,
style::{PathStyle, ViewMode},
LayerData,
};
use crate::LayerId;
use crate::Quad;
use kurbo::BezPath;
use super::style;
use super::style::PathStyle;
use super::LayerData;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
@ -30,9 +30,9 @@ pub struct Shape {
}
impl LayerData for Shape {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<DAffine2>) {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
let mut path = self.path.clone();
let transform = self.transform(transforms);
let transform = self.transform(transforms, view_mode);
let inverse = transform.inverse();
if !inverse.is_finite() {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
@ -45,7 +45,7 @@ impl LayerData for Shape {
let _ = svg.write_str(&(entry.to_string() + if i != 5 { "," } else { "" }));
});
let _ = svg.write_str(r#")">"#);
let _ = write!(svg, r#"<path d="{}" {} />"#, path.to_svg(), self.style.render());
let _ = write!(svg, r#"<path d="{}" {} />"#, path.to_svg(), self.style.render(view_mode));
let _ = svg.write_str("</g>");
}
@ -69,10 +69,11 @@ impl LayerData for Shape {
}
impl Shape {
pub fn transform(&self, transforms: &[DAffine2]) -> DAffine2 {
let start = match self.render_index {
-1 => 0,
x => (transforms.len() as i32 - x).max(0) as usize,
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
let start = match (mode, self.render_index) {
(ViewMode::Outline, _) => 0,
(_, -1) => 0,
(_, x) => (transforms.len() as i32 - x).max(0) as usize,
};
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
}
@ -82,7 +83,7 @@ impl Shape {
path: bez_path,
style,
render_index: 1,
solid: solid,
solid,
}
}

View File

@ -1,15 +1,29 @@
use crate::color::Color;
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WIDTH};
use serde::{Deserialize, Serialize};
const OPACITY_PRECISION: usize = 3;
fn format_opacity(name: &str, opacity: f32) -> String {
if (opacity - 1.).abs() > 10f32.powi(-(OPACITY_PRECISION as i32)) {
if (opacity - 1.).abs() > 10_f32.powi(-(OPACITY_PRECISION as i32)) {
format!(r#" {}-opacity="{:.precision$}""#, name, opacity, precision = OPACITY_PRECISION)
} else {
String::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
pub enum ViewMode {
Normal,
Outline,
Pixels,
}
impl Default for ViewMode {
fn default() -> Self {
ViewMode::Normal
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Fill {
@ -22,7 +36,7 @@ impl Fill {
pub fn color(&self) -> Option<Color> {
self.color
}
pub fn none() -> Self {
pub const fn none() -> Self {
Self { color: None }
}
pub fn render(&self) -> String {
@ -41,7 +55,7 @@ pub struct Stroke {
}
impl Stroke {
pub fn new(color: Color, width: f32) -> Self {
pub const fn new(color: Color, width: f32) -> Self {
Self { color, width }
}
pub fn color(&self) -> Color {
@ -83,17 +97,18 @@ impl PathStyle {
pub fn clear_stroke(&mut self) {
self.stroke = None;
}
pub fn render(&self) -> String {
format!(
"{}{}",
match self.fill {
Some(fill) => fill.render(),
None => String::new(),
},
match self.stroke {
Some(stroke) => stroke.render(),
None => String::new(),
},
)
pub fn render(&self, view_mode: ViewMode) -> String {
let fill_attribute = match (view_mode, self.fill) {
(ViewMode::Outline, _) => Fill::none().render(),
(_, Some(fill)) => fill.render(),
(_, None) => String::new(),
};
let stroke_attribute = match (view_mode, self.stroke) {
(ViewMode::Outline, _) => Stroke::new(LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WIDTH).render(),
(_, Some(stroke)) => stroke.render(),
(_, None) => String::new(),
};
format!("{}{}", fill_attribute, stroke_attribute)
}
}

View File

@ -1,4 +1,5 @@
pub mod color;
pub mod consts;
pub mod document;
pub mod intersection;
pub mod layers;