diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index b8e0bf19e5..b0b2009975 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -219,3 +219,13 @@ e' (/ (- (* c f) (* d e)) det) f' (/ (- (* b e) (* a f)) det)] (Matrix. a' b' c' d' e' f'))) + +(defn round + [mtx] + (-> mtx + (update :a mth/precision 4) + (update :b mth/precision 4) + (update :c mth/precision 4) + (update :d mth/precision 4) + (update :e mth/precision 4) + (update :f mth/precision 4))) diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc index 5c295acef2..32064b22df 100644 --- a/common/src/app/common/geom/shapes/constraints.cljc +++ b/common/src/app/common/geom/shapes/constraints.cljc @@ -152,6 +152,28 @@ :top :scale))) +(defn clean-modifiers + "Remove redundant modifiers" + [{:keys [displacement resize-vector resize-vector-2] :as modifiers}] + + (cond-> modifiers + ;; Displacement with value 0. We don't move in any direction + (and (some? displacement) + (mth/almost-zero? (:e displacement)) + (mth/almost-zero? (:f displacement))) + (dissoc :displacement) + + ;; Resize with value very close to 1 means no resize + (and (some? resize-vector) + (mth/almost-zero? (- 1.0 (:x resize-vector))) + (mth/almost-zero? (- 1.0 (:y resize-vector)))) + (dissoc :resize-origin :resize-vector) + + (and (some? resize-vector) + (mth/almost-zero? (- 1.0 (:x resize-vector-2))) + (mth/almost-zero? (- 1.0 (:y resize-vector-2)))) + (dissoc :resize-origin-2 :resize-vector-2))) + (defn calc-child-modifiers [parent child modifiers ignore-constraints transformed-parent-rect] (let [constraints-h @@ -192,5 +214,7 @@ (:resize-transform modifiers) (assoc :resize-transform (:resize-transform modifiers) - :resize-transform-inverse (:resize-transform-inverse modifiers))))) + :resize-transform-inverse (:resize-transform-inverse modifiers)) + :always + (clean-modifiers)))) diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index 4da32f002b..9b49869224 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -8,7 +8,9 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages.helpers :as cph])) + [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph] + [app.common.path.commands :as upc])) (defn lookup-page ([state] @@ -50,6 +52,10 @@ (filter selectable?) selected))))) +(defn lookup-selected-raw + [state] + (dm/get-in state [:workspace-local :selected])) + (defn lookup-selected ([state] (lookup-selected state nil)) @@ -94,3 +100,26 @@ (-> (:workspace-libraries state) (assoc id {:id id :data local})))) + +(defn- set-content-modifiers [state] + (fn [id shape] + (let [content-modifiers (dm/get-in state [:workspace-local :edit-path id :content-modifiers])] + (if (some? content-modifiers) + (update shape :content upc/apply-content-modifiers content-modifiers) + shape)))) + +(defn select-bool-children + [parent-id state] + (let [objects (lookup-page-objects state) + selected (lookup-selected-raw state) + modifiers (:workspace-modifiers state) + + children-ids (cph/get-children-ids objects parent-id) + selected-children (into [] (filter selected) children-ids) + + modifiers (select-keys modifiers selected-children) + children (select-keys objects children-ids)] + + (as-> children $ + (gsh/merge-modifiers $ modifiers) + (d/mapm (set-content-modifiers state) $)))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 839ff694c2..24b48c0aa5 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -9,9 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] - [app.common.path.commands :as upc] [app.main.data.workspace.state-helpers :as wsh] [app.main.store :as st] [okulary.core :as l])) @@ -193,28 +191,29 @@ (assoc :pages (:pages data))))) st/state =)) +(def workspace-data + (l/derived :workspace-data st/state)) + (def workspace-file-colors - (l/derived (fn [state] - (when-let [file (:workspace-data state)] - (->> (:colors file) - (d/mapm #(assoc %2 :file-id (:id file)))))) - st/state)) + (l/derived (fn [data] + (when data + (->> (:colors data) + (d/mapm #(assoc %2 :file-id (:id data)))))) + workspace-data + =)) (def workspace-recent-colors - (l/derived (fn [state] - (dm/get-in state [:workspace-data :recent-colors] [])) - st/state)) + (l/derived (fn [data] + (get data :recent-colors [])) + workspace-data)) (def workspace-recent-fonts - (l/derived (fn [state] - (dm/get-in state [:workspace-data :recent-fonts] [])) - st/state)) + (l/derived (fn [data] + (get data :workspace-data [])) + workspace-data)) (def workspace-file-typography - (l/derived (fn [state] - (when-let [file (:workspace-data state)] - (:typographies file))) - st/state)) + (l/derived :typographies workspace-data)) (def workspace-project (l/derived :workspace-project st/state)) @@ -313,24 +312,8 @@ workspace-modifiers-with-objects =)) -(defn- set-content-modifiers [state] - (fn [id shape] - (let [content-modifiers (dm/get-in state [:workspace-local :edit-path id :content-modifiers])] - (if (some? content-modifiers) - (update shape :content upc/apply-content-modifiers content-modifiers) - shape)))) - (defn select-bool-children [id] - (let [selector - (fn [state] - (let [objects (wsh/lookup-page-objects state) - modifiers (:workspace-modifiers state) - children (->> (cph/get-children-ids objects id) - (select-keys objects))] - (as-> children $ - (gsh/merge-modifiers $ modifiers) - (d/mapm (set-content-modifiers state) $))))] - (l/derived selector st/state =))) + (l/derived (partial wsh/select-bool-children id) st/state =)) (def selected-data (l/derived #(let [selected (wsh/lookup-selected %) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index b5cf278f2b..c3b9574019 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.filters (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.uuid :as uuid] @@ -249,6 +250,7 @@ :height filter-height :filterUnits "objectBoundingBox" :color-interpolation-filters "sRGB"} - (for [entry filters] - [:& filter-entry {:entry entry}])]))) + (for [[index entry] (d/enumerate filters)] + [:& filter-entry {:key (dm/str filter-id "-" index) + :entry entry}])]))) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 790776004b..6ec138194c 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -41,7 +41,7 @@ (defn propagate-wrapper-styles ([children wrapper-props] (if (.isArray js/Array children) - (->> children (map propagate-wrapper-styles-child)) + (->> children (map #(propagate-wrapper-styles-child % wrapper-props))) (-> children (propagate-wrapper-styles-child wrapper-props))))) (mf/defc shape-container diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index cd2a21c5b9..f6490e5fb1 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -50,7 +50,7 @@ text-align (:text-align data "start") base #js {:fontSize (str (:font-size data (:font-size txt/default-text-attrs)) "px") :lineHeight (:line-height data (:line-height txt/default-text-attrs)) - :margin "inherit"}] + :margin 0}] (cond-> base (some? line-height) (obj/set! "lineHeight" line-height) (some? text-align) (obj/set! "textAlign" text-align)))) diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index d46025e4d3..4c8054c247 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -64,6 +64,7 @@ ;; These position attributes are not really necesary but they are convenient for for the export group-props (-> #js {:transform transform + :className "text-container" :x x :y y :width width @@ -78,10 +79,11 @@ [:defs (for [[index data] (d/enumerate position-data)] (when (some? (:fill-color-gradient data)) - [:& grad/gradient {:id (str "fill-color-gradient_" (get-gradient-id index)) - :key index - :attr :fill-color-gradient - :shape data}]))]) + (let [id (dm/str "fill-color-gradient_" (get-gradient-id index))] + [:& grad/gradient {:id id + :key id + :attr :fill-color-gradient + :shape data}])))]) [:> :g group-props (for [[index data] (d/enumerate position-data)] @@ -91,7 +93,7 @@ alignment-bl (when (cfg/check-browser? :safari) "text-before-edge") dominant-bl (when-not (cfg/check-browser? :safari) "ideographic") - rtl? (= "rtl"(:direction data)) + rtl? (= "rtl" (:direction data)) props (-> #js {:key (dm/str "text-" (:id shape) "-" index) :x (if rtl? (+ (:x data) (:width data)) (:x data)) :y y @@ -110,7 +112,7 @@ (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))}) shape (assoc shape :fills (:fills data))] - [:& (mf/provider muc/render-ctx) {:value (str render-id "_" (:id shape) "_" index)} + [:& (mf/provider muc/render-ctx) {:key index :value (str render-id "_" (:id shape) "_" index)} [:& shape-custom-strokes {:shape shape :position index :render-id render-id} [:> :text props (:text data)]]]))]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index 9ec66d7d6c..14620a7626 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -81,8 +81,8 @@ (let [shape-node (dom/query base-node (str "#shape-" id)) frame? (= :frame type) - group? (= :group type) text? (= :text type) + group? (= :group type) mask? (and group? masked-group?)] (cond @@ -106,7 +106,7 @@ text? [shape-node - (dom/query shape-node ".text-shape")] + (dom/query shape-node ".text-container")] :else [shape-node]))) @@ -166,6 +166,10 @@ (str value))] (dom/set-attribute! node att (str new-value)))) +(defn override-transform-att! + [node att value] + (dom/set-attribute! node att (str value))) + (defn update-transform! [base-node shapes transforms modifiers] (doseq [{:keys [id] :as shape} shapes] @@ -177,14 +181,24 @@ (cond ;; Text shapes need special treatment because their resize only change ;; the text area, not the change size/position - (or (dom/class? node "text-shape") - (dom/class? node "frame-thumbnail")) + (dom/class? node "frame-thumbnail") (let [[transform] (transform-no-resize shape transform modifiers)] (set-transform-att! node "transform" transform)) (dom/class? node "frame-children") (set-transform-att! node "transform" (gmt/inverse transform)) + ;; We need to update the shape transform matrix when there is a resize + ;; we do it dinamicaly here + (dom/class? node "text-container") + (let [modifiers (dissoc modifiers :displacement :rotation)] + (when (not (gsh/empty-modifiers? modifiers)) + (let [mtx (-> shape + (assoc :modifiers modifiers) + (gsh/transform-shape) + (gsh/transform-matrix {:no-flip true}))] + (override-transform-att! node "transform" mtx)))) + (or (= (dom/get-tag-name node) "mask") (= (dom/get-tag-name node) "filter")) (transform-region! node modifiers) @@ -233,7 +247,13 @@ (fn [] (when (some? modifiers) (d/mapm (fn [id {modifiers :modifiers}] - (let [center (gsh/center-shape (get objects id))] + (let [shape (get objects id) + center (gsh/center-shape shape) + modifiers (cond-> modifiers + ;; For texts we only use the displacement because + ;; resize needs to recalculate the text layout + (= :text (:type shape)) + (select-keys [:displacement :rotation]))] (gsh/modifiers->transform center modifiers))) modifiers)))) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 821fca5365..7a5b662d90 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.shapes.text (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.math :as mth] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] @@ -32,24 +33,23 @@ (dwt/apply-text-modifier text-modifier))] [:> shape-container {:shape shape} - [:* - [:g.text-shape - [:& text/text-shape {:shape shape}]] + [:g.text-shape {:key (dm/str "text-" (:id shape))} + [:& text/text-shape {:shape shape}]] - (when (and (debug? :text-outline) (d/not-empty? (:position-data shape))) - (for [data (:position-data shape)] - (let [{:keys [x y width height]} data] - [:* - ;; Text fragment bounding box - [:rect {:x x - :y (- y height) - :width width - :height height - :style {:fill "none" :stroke "red"}}] + (when (and (debug? :text-outline) (d/not-empty? (:position-data shape))) + (for [[index data] (d/enumerate (:position-data shape))] + (let [{:keys [x y width height]} data] + [:g {:key (dm/str index)} + ;; Text fragment bounding box + [:rect {:x x + :y (- y height) + :width width + :height height + :style {:fill "none" :stroke "red"}}] - ;; Text baselineazo - [:line {:x1 (mth/round x) - :y1 (mth/round (- (:y data) (:height data))) - :x2 (mth/round (+ x width)) - :y2 (mth/round (- (:y data) (:height data))) - :style {:stroke "blue"}}]])))]])) + ;; Text baselineazo + [:line {:x1 (mth/round x) + :y1 (mth/round (- (:y data) (:height data))) + :x2 (mth/round (+ x width)) + :y2 (mth/round (- (:y data) (:height data))) + :style {:stroke "blue"}}]])))])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs index 2045ec6712..fea2ad1d9b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs @@ -9,6 +9,7 @@ [app.common.attrs :as attrs] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages.helpers :as cph] [app.common.text :as txt] @@ -28,6 +29,15 @@ (defn strip-position-data [shape] (dissoc shape :position-data :transform :transform-inverse)) +(defn process-shape [modifiers {:keys [id] :as shape}] + (let [modifier (get modifiers id) + modifier (d/update-when modifier :modifiers dissoc :displacement :rotation) + shape (cond-> shape + (not (gsh/empty-modifiers? modifier)) + (-> (assoc :grow-type :fixed) + (merge modifier) gsh/transform-shape))] + (strip-position-data shape))) + (defn- update-with-editor-state "Updates the shape with the current state in the editor" [shape editor-state] @@ -88,7 +98,6 @@ (fn [node] (when (some? node) (on-update shape node))))] - [:& fo/text-shape {:key (str "shape-" (:id shape)) :ref handle-update :shape shape @@ -120,7 +129,7 @@ [:* (for [{:keys [id] :as shape} changed-texts] - [:& text-container {:shape shape + [:& text-container {:shape (gsh/transform-shape shape) :on-update handle-update-shape :key (str (dm/str "text-container-" id))}])])) @@ -151,24 +160,30 @@ (defn check-props [new-props old-props] - (and (identical? (unchecked-get new-props "objects") (unchecked-get old-props "objects")) - (= (unchecked-get new-props "edition") (unchecked-get old-props "edition")))) + (and (identical? (unchecked-get new-props "objects") + (unchecked-get old-props "objects")) + (identical? (unchecked-get new-props "modifiers") + (unchecked-get old-props "modifiers")) + (= (unchecked-get new-props "edition") + (unchecked-get old-props "edition")))) (mf/defc viewport-texts {::mf/wrap-props false ::mf/wrap [#(mf/memo' % check-props)]} [props] - (let [objects (obj/get props "objects") - edition (obj/get props "edition") - - xf-texts (comp (filter (comp cph/text-shape? second)) - (map (fn [[id shape]] - [id (strip-position-data shape)]))) + (let [objects (obj/get props "objects") + edition (obj/get props "edition") + modifiers (obj/get props "modifiers") text-shapes (mf/use-memo (mf/deps objects) - #(into {} xf-texts objects)) + #(into {} (filter (comp cph/text-shape? second)) objects)) + + text-shapes + (mf/use-memo + (mf/deps text-shapes modifiers) + #(d/update-vals text-shapes (partial process-shape modifiers))) editing-shape (get text-shapes edition)] @@ -183,4 +198,6 @@ [:* (when editing-shape [:& viewport-text-editing {:shape editing-shape}]) - [:& viewport-texts-wrapper {:text-shapes (dissoc text-shapes edition)}]])) + + [:& viewport-texts-wrapper {:text-shapes (dissoc text-shapes edition) + :modifiers modifiers}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 3abcde4494..0224855ac1 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -246,6 +246,7 @@ [:& stv/viewport-texts {:key (dm/str "texts-" page-id) :page-id page-id :objects base-objects + :modifiers modifiers :edition edition}]]] [:svg.viewport-controls diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 1fe11720eb..1c22c42d18 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -166,7 +166,8 @@ (:name frame)]])) (mf/defc frame-titles - {::mf/wrap-props false} + {::mf/wrap-props false + ::mf/wrap [mf/memo]} [props] (let [objects (unchecked-get props "objects") zoom (unchecked-get props "zoom") diff --git a/frontend/src/app/util/dom/normalize_wheel.js b/frontend/src/app/util/dom/normalize_wheel.js index c9f22399f8..fc221e54ca 100644 --- a/frontend/src/app/util/dom/normalize_wheel.js +++ b/frontend/src/app/util/dom/normalize_wheel.js @@ -6,8 +6,6 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule normalizeWheel - * @typechecks */ /**