From fcc928230464cdbd55b23d709f537945faf84668 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 30 Oct 2025 12:20:45 +0100 Subject: [PATCH] :sparkles: Fix problems with SVGraw and modifiers --- .../app/main/data/workspace/modifiers.cljs | 38 ++++++++++++------- .../app/main/data/workspace/transforms.cljs | 38 ++++++++++--------- frontend/src/app/render_wasm/shape.cljs | 5 ++- render-wasm/src/main.rs | 8 +++- render-wasm/src/render.rs | 9 ++++- render-wasm/src/shapes.rs | 10 ++++- render-wasm/src/shapes/modifiers.rs | 18 ++++----- render-wasm/src/shapes/shape_to_path.rs | 8 ++-- render-wasm/src/state.rs | 9 ++++- render-wasm/src/state/shapes_pool.rs | 14 ++++--- 10 files changed, 101 insertions(+), 56 deletions(-) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 4e175d0009..3d3831062d 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -645,12 +645,16 @@ #_:clj-kondo/ignore (defn apply-wasm-modifiers - [modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis undo-group] - :or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil undo-group nil} + [modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis undo-transation?] + :or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil undo-transation? true} :as params}] (ptk/reify ::apply-wasm-modifiesr ptk/WatchEvent (watch [_ state _] + (wasm.api/clean-modifiers) + (let [structure-entries (parse-structure-modifiers modif-tree)] + (wasm.api/set-structure-modifiers structure-entries)) + (let [objects (dsh/lookup-page-objects state) ignore-tree @@ -670,8 +674,6 @@ snap-pixel? (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid)) - _ (wasm.api/clean-geometry-modifiers) - transforms (into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)) @@ -696,16 +698,26 @@ (mapcat (partial cfh/get-parents-with-self objects)) (filter cfh/bool-shape?) (map :id)) - ids)] - (rx/of - (clear-local-transform) - (ptk/event ::dwg/move-frame-guides {:ids ids :transforms transforms}) - (ptk/event ::dwcm/move-frame-comment-threads transforms) - (dwsh/update-shapes ids update-shape options) + ids) - ;; The update to the bool path needs to be in a different operation because it - ;; needs to have the updated children info - (dwsh/update-shapes bool-ids path/update-bool-shape (assoc options :with-objects? true))))))) + undo-id (js/Symbol)] + (rx/concat + (if undo-transation? + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/empty)) + (rx/of + (clear-local-transform) + (ptk/event ::dwg/move-frame-guides {:ids ids :transforms transforms}) + (ptk/event ::dwcm/move-frame-comment-threads transforms) + (dwsh/update-shapes ids update-shape options) + + ;; The update to the bool path needs to be in a different operation because it + ;; needs to have the updated children info + (dwsh/update-shapes bool-ids path/update-bool-shape (assoc options :with-objects? true))) + + (if undo-transation? + (rx/of (dwu/commit-undo-transaction undo-id)) + (rx/empty))))))) (def ^:private xf-rotation-shape diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 0fdc6665e8..13fa6c3356 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -146,7 +146,7 @@ (defn start-resize "Enter mouse resize mode, until mouse button is released." [handler ids shape] - (letfn [(resize [shape initial layout [point lock? center? point-snap]] + (letfn [(resize [shape initial layout objects [point lock? center? point-snap]] (let [selrect (dm/get-prop shape :selrect) width (dm/get-prop selrect :width) height (dm/get-prop selrect :height) @@ -243,10 +243,14 @@ :always (ctm/resize scalev resize-origin shape-transform shape-transform-inverse) - ^boolean change-width? + (and (ctl/any-layout-immediate-child? objects shape) + (not= (:layout-item-h-sizing shape) :fix) + ^boolean change-width?) (ctm/change-property :layout-item-h-sizing :fix) - ^boolean change-height? + (and (ctl/any-layout-immediate-child? objects shape) + (not= (:layout-item-v-sizing shape) :fix) + ^boolean change-height?) (ctm/change-property :layout-item-v-sizing :fix) ;; Set grow-type if it should change @@ -298,7 +302,7 @@ (fn [[point _ _ :as current]] (->> (snap/closest-snap-point page-id shapes objects layout zoom focus point) (rx/map #(conj current %))))) - (rx/map #(resize shape initial-position layout %)) + (rx/map #(resize shape initial-position layout objects %)) (rx/share)) modifiers-stream @@ -332,8 +336,8 @@ (rx/take-until stopper)))] (rx/concat - ;; This initial stream waits for some pixels to be move before making the resize - ;; if you make a click in the border will not make a resize + ;; This initial stream waits for some pixels to be move before making the resize + ;; if you make a click in the border will not make a resize (->> ms/mouse-position (rx/map #(gpt/to-vec initial-position %)) (rx/map #(gpt/length %)) @@ -745,12 +749,6 @@ (fn [[modifiers snap-ignore-axis]] (dwm/set-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis)))) - (->> modifiers-stream - (rx/last) - (rx/map - (fn [[modifiers snap-ignore-axis]] - (dwm/apply-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis)))) - (->> move-stream (rx/with-latest-from ms/mouse-position-alt) (rx/filter (fn [[_ alt?]] alt?)) @@ -765,14 +763,18 @@ ;; Last event will write the modifiers creating the changes (->> move-stream (rx/last) + (rx/with-latest-from modifiers-stream) (rx/mapcat - (fn [[_ target-frame drop-index drop-cell]] + (fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]] (let [undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - ;; (dwm/apply-modifiers {:undo-transation? false}) - (move-shapes-to-frame ids target-frame drop-index drop-cell) - (finish-transform) - (dwu/commit-undo-transaction undo-id))))))) + (rx/of + (dwu/start-undo-transaction undo-id) + (dwm/apply-wasm-modifiers modifiers + :snap-ignore-axis snap-ignore-axis + :undo-transation? false) + (move-shapes-to-frame ids target-frame drop-index drop-cell) + (finish-transform) + (dwu/commit-undo-transaction undo-id))))))) (rx/merge (->> modifiers-stream diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 7e0ffcc7a2..922240dd29 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -135,7 +135,10 @@ (when (or (= v :path) (= v :bool)) (api/set-shape-path-content (:content shape)))) :bool-type (api/set-shape-bool-type v) - :selrect (api/set-shape-selrect v) + :selrect (do + (api/set-shape-selrect v) + (when (= (:type shape) :svg-raw) + (api/set-shape-svg-raw-content (api/get-static-markup shape)))) :show-content (if (= (:type shape) :frame) (api/set-shape-clip-content (not v)) (api/set-shape-clip-content false)) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 675a6e3ad0..694c0c9901 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -290,15 +290,21 @@ pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) { fn set_children_set(entries: IndexSet) { let mut deleted = IndexSet::new(); + let mut parent_id = None; with_current_shape_mut!(state, |shape: &mut Shape| { + parent_id = Some(shape.id); (_, deleted) = shape.compute_children_differences(&entries); shape.children = entries.clone(); }); with_state_mut!(state, { + let Some(parent_id) = parent_id else { + return; + }; + for id in deleted { - state.delete_shape(id); + state.delete_shape_children(parent_id, id); } }); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 058454b421..78e032caaf 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -535,7 +535,12 @@ impl RenderState { match &shape.shape_type { Type::SVGRaw(sr) => { + if let Some(svg_transform) = shape.svg_transform() { + matrix.pre_concat(&svg_transform); + } + self.surfaces.canvas(fills_surface_id).concat(&matrix); + if let Some(svg) = shape.svg.as_ref() { svg.render(self.surfaces.canvas(fills_surface_id)) } else { @@ -1574,7 +1579,7 @@ impl RenderState { while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { if shape_id != Uuid::nil() { - self.update_tile_for(&shape, tree); + self.update_tile_for(shape, tree); } else { // We only need to rebuild tiles from the first level. let children = shape.children_ids(false); @@ -1595,7 +1600,7 @@ impl RenderState { while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { if shape_id != Uuid::nil() { - self.update_tile_for(&shape, tree); + self.update_tile_for(shape, tree); } let children = shape.children_ids(false); diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index ebe2c97beb..6a9d0bddb5 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -182,6 +182,7 @@ pub struct Shape { pub layout_item: Option, pub extrect: OnceCell, pub bounds: OnceCell, + pub svg_transform: Option, } // Returns all ancestor shapes of this shape, traversing up the parent hierarchy @@ -263,6 +264,7 @@ impl Shape { layout_item: None, extrect: OnceCell::new(), bounds: OnceCell::new(), + svg_transform: None, } } @@ -393,6 +395,10 @@ impl Shape { self.hidden = value; } + pub fn svg_transform(&self) -> Option { + self.svg_transform + } + // FIXME: These arguments could be grouped or simplified #[allow(clippy::too_many_arguments)] pub fn set_flex_layout_child_data( @@ -876,7 +882,7 @@ impl Shape { } Type::Text(text_content) => { // FIXME: we need to recalculate the text bounds here because the shape's selrect - let text_bounds = text_content.calculate_bounds(&shape); + let text_bounds = text_content.calculate_bounds(shape); text_bounds.to_rect() } _ => shape.bounds().to_rect(), @@ -1160,6 +1166,8 @@ impl Shape { } } else if let Type::Text(text) = &mut self.shape_type { text.transform(transform); + } else if let Type::SVGRaw(_) = &mut self.shape_type { + self.svg_transform = Some(*transform); } } diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index df5839e888..5bfd4781ca 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -95,7 +95,7 @@ fn calculate_group_bounds( let mut result = Vec::::new(); for child_id in shape.children_ids_iter(true) { - let Some(child) = shapes.get(&child_id) else { + let Some(child) = shapes.get(child_id) else { continue; }; @@ -109,7 +109,7 @@ fn calculate_bool_bounds( shape: &Shape, shapes: ShapesPoolRef, bounds: &HashMap, - modifiers: &HashMap + modifiers: &HashMap, ) -> Option { let shape_bounds = bounds.find(shape); let children_ids = shape.children_ids(true); @@ -258,7 +258,7 @@ fn propagate_reflow( bounds: &mut HashMap, layout_reflows: &mut Vec, reflown: &mut HashSet, - modifiers: &HashMap + modifiers: &HashMap, ) { let Some(shape) = state.shapes.get(id) else { return; @@ -267,7 +267,7 @@ fn propagate_reflow( let shapes = &state.shapes; let mut reflow_parent = false; - if reflown.contains(&id) { + if reflown.contains(id) { return; } @@ -403,7 +403,7 @@ pub fn propagate_modifiers( &mut bounds, &mut layout_reflows, &mut reflown, - &mut modifiers, + &modifiers, ), } } @@ -429,13 +429,14 @@ mod tests { use crate::math::{Matrix, Point}; use crate::shapes::*; + use crate::state::ShapesPool; #[test] fn test_propagate_shape() { let parent_id = Uuid::new_v4(); let shapes = { - let mut shapes = ShapesPoolRef::new(); + let mut shapes = ShapesPool::new(); shapes.initialize(10); let child_id = Uuid::new_v4(); @@ -468,7 +469,6 @@ mod tests { transform, &HashMap::new(), &HashMap::new(), - &HashMap::new(), ); assert_eq!(result.len(), 1); @@ -478,7 +478,7 @@ mod tests { fn test_group_bounds() { let parent_id = Uuid::new_v4(); let shapes = { - let mut shapes = ShapesPoolRef::new(); + let mut shapes = ShapesPool::new(); shapes.initialize(10); let child1_id = Uuid::new_v4(); @@ -500,7 +500,7 @@ mod tests { let parent = shapes.get(&parent_id).unwrap(); let bounds = - calculate_group_bounds(parent, &shapes, &HashMap::new(), &HashMap::new()).unwrap(); + calculate_group_bounds(parent, &shapes, &HashMap::new()).unwrap(); assert_eq!(bounds.width(), 3.0); assert_eq!(bounds.height(), 3.0); diff --git a/render-wasm/src/shapes/shape_to_path.rs b/render-wasm/src/shapes/shape_to_path.rs index d641a2dc86..db241fe44b 100644 --- a/render-wasm/src/shapes/shape_to_path.rs +++ b/render-wasm/src/shapes/shape_to_path.rs @@ -175,7 +175,7 @@ impl ToPath for Shape { match &self.shape_type { Type::Frame(ref frame) => { let children = self.children_ids(true); - let mut result = Path::new(rect_segments(&self, frame.corners)); + let mut result = Path::new(rect_segments(self, frame.corners)); for id in children { let Some(shape) = shapes.get(&id) else { continue; @@ -202,11 +202,11 @@ impl ToPath for Shape { Type::Bool(bool_data) => bool_data.path.clone(), - Type::Rect(ref rect) => Path::new(rect_segments(&self, rect.corners)), + Type::Rect(ref rect) => Path::new(rect_segments(self, rect.corners)), Type::Path(path_data) => path_data.clone(), - Type::Circle => Path::new(circle_segments(&self)), + Type::Circle => Path::new(circle_segments(self)), Type::SVGRaw(_) => Path::default(), @@ -217,7 +217,7 @@ impl ToPath for Shape { result = join_paths(result, Path::from_skia_path(path)); } - Path::new(transform_segments(result.segments().clone(), &self)) + Path::new(transform_segments(result.segments().clone(), self)) } } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 85998b1bb3..54059ba70d 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -94,9 +94,14 @@ impl<'a> State<'a> { self.current_id = Some(id); } - pub fn delete_shape(&mut self, id: Uuid) { + pub fn delete_shape_children(&mut self, parent_id: Uuid, id: Uuid) { // We don't really do a self.shapes.remove so that redo/undo keep working - if let Some(shape) = self.shapes.get(&id) { + let Some(shape) = self.shapes.get(&id) else { + return; + }; + + // Only remove the children when is being deleted from the owner + if shape.parent_id.is_none() || shape.parent_id == Some(parent_id) { let tiles::TileRect(rsx, rsy, rex, rey) = self.render_state.get_tiles_for_shape(shape, &self.shapes); for x in rsx..=rex { diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index d286d6955c..9c0b8ff450 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -217,7 +217,7 @@ impl<'a> ShapesPoolImpl<'a> { Some(cell.get_or_init(|| { let shape = &*shape_ptr; shape.transformed( - &self, + self, (*modifiers_ptr).get(&id_ref), (*structure_ptr).get(&id_ref), ) @@ -264,7 +264,7 @@ impl<'a> ShapesPoolImpl<'a> { } self.modifiers = modifiers_with_refs; - let all_ids = shapes::all_with_ancestors(&ids, &self, true); + let all_ids = shapes::all_with_ancestors(&ids, self, true); for uuid in all_ids { if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); @@ -287,7 +287,7 @@ impl<'a> ShapesPoolImpl<'a> { } self.structure = structure_with_refs; - let all_ids = shapes::all_with_ancestors(&ids, &self, true); + let all_ids = shapes::all_with_ancestors(&ids, self, true); for uuid in all_ids { if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); @@ -317,7 +317,9 @@ impl<'a> ShapesPoolImpl<'a> { } pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl<'a> { - let Some(shape) = self.get(id) else { panic!("Subtree not found"); }; + let Some(shape) = self.get(id) else { + panic!("Subtree not found"); + }; // TODO: Maybe create all_children_iter let all_children = shape.all_children(self, true, true); @@ -327,7 +329,9 @@ impl<'a> ShapesPoolImpl<'a> { let mut shapes_uuid_to_idx = HashMap::default(); for id in all_children.iter() { - let Some(shape) = self.get(id) else { panic!("Not found"); }; + let Some(shape) = self.get(id) else { + panic!("Not found"); + }; shapes.push(shape.clone()); let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };