diff --git a/common/app/common/attrs.cljc b/common/app/common/attrs.cljc new file mode 100644 index 0000000000..9008c9ae1b --- /dev/null +++ b/common/app/common/attrs.cljc @@ -0,0 +1,71 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.common.attrs) + +(defn get-attrs-multi + [shapes attrs] + ;; Extract some attributes of a list of shapes. + ;; For each attribute, if the value is the same in all shapes, + ;; wll take this value. If there is any shape that is different, + ;; the value of the attribute will be the keyword :multiple. + ;; + ;; If some shape has the value nil in any attribute, it's + ;; considered a different value. If the shape does not contain + ;; the attribute, it's ignored in the final result. + ;; + ;; Example: + ;; (def shapes [{:stroke-color "#ff0000" + ;; :stroke-width 3 + ;; :fill-color "#0000ff" + ;; :x 1000 :y 2000 :rx nil} + ;; {:stroke-width "#ff0000" + ;; :stroke-width 5 + ;; :x 1500 :y 2000}]) + ;; + ;; (get-attrs-multi shapes [:stroke-color + ;; :stroke-width + ;; :fill-color + ;; :rx + ;; :ry]) + ;; >>> {:stroke-color "#ff0000" + ;; :stroke-width :multiple + ;; :fill-color "#0000ff" + ;; :rx nil + ;; :ry nil} + ;; + (let [defined-shapes (filter some? shapes) + + combine-value (fn [v1 v2] (cond + (= v1 v2) v1 + (= v1 :undefined) v2 + (= v2 :undefined) v1 + :else :multiple)) + + combine-values (fn [attrs shape values] + (map #(combine-value (get shape % :undefined) + (get values % :undefined)) attrs)) + + select-attrs (fn [shape attrs] + (zipmap attrs (map #(get shape % :undefined) attrs))) + + reducer (fn [result shape] + (zipmap attrs (combine-values attrs shape result))) + + combined (reduce reducer + (select-attrs (first defined-shapes) attrs) + (rest defined-shapes)) + + cleanup-value (fn [value] + (if (= value :undefined) nil value)) + + cleanup (fn [result] + (zipmap attrs (map #(cleanup-value (get result %)) attrs)))] + + (cleanup combined))) diff --git a/common/app/common/geom/align.cljc b/common/app/common/geom/align.cljc new file mode 100644 index 0000000000..64089b70c6 --- /dev/null +++ b/common/app/common/geom/align.cljc @@ -0,0 +1,141 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.common.geom.align + (:require + [clojure.spec.alpha :as s] + [app.common.spec :as us] + [app.common.geom.shapes :as gsh] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.math :as mth] + [app.common.data :as d])) + +;; --- Alignment + +(s/def ::align-axis #{:hleft :hcenter :hright :vtop :vcenter :vbottom}) + +(declare calc-align-pos) + +(defn align-to-rect + "Move the shape so that it is aligned with the given rectangle + in the given axis. Take account the form of the shape and the + possible rotation. What is aligned is the rectangle that wraps + the shape with the given rectangle. If the shape is a group, + move also all of its recursive children." + [shape rect axis objects] + (let [wrapper-rect (gsh/selection-rect [shape]) + align-pos (calc-align-pos wrapper-rect rect axis) + delta {:x (- (:x align-pos) (:x wrapper-rect)) + :y (- (:y align-pos) (:y wrapper-rect))}] + (gsh/recursive-move shape delta objects))) + +(defn calc-align-pos + [wrapper-rect rect axis] + (case axis + :hleft (let [left (:x rect)] + {:x left + :y (:y wrapper-rect)}) + + :hcenter (let [center (+ (:x rect) (/ (:width rect) 2))] + {:x (- center (/ (:width wrapper-rect) 2)) + :y (:y wrapper-rect)}) + + :hright (let [right (+ (:x rect) (:width rect))] + {:x (- right (:width wrapper-rect)) + :y (:y wrapper-rect)}) + + :vtop (let [top (:y rect)] + {:x (:x wrapper-rect) + :y top}) + + :vcenter (let [center (+ (:y rect) (/ (:height rect) 2))] + {:x (:x wrapper-rect) + :y (- center (/ (:height wrapper-rect) 2))}) + + :vbottom (let [bottom (+ (:y rect) (:height rect))] + {:x (:x wrapper-rect) + :y (- bottom (:height wrapper-rect))}))) + +;; --- Distribute + +(s/def ::dist-axis #{:horizontal :vertical}) + +(defn distribute-space + "Distribute equally the space between shapes in the given axis. If + there is no space enough, it does nothing. It takes into account + the form of the shape and the rotation, what is distributed is + the wrapping recangles of the shapes. If any shape is a group, + move also all of its recursive children." + [shapes axis objects] + (let [coord (if (= axis :horizontal) :x :y) + other-coord (if (= axis :horizontal) :y :x) + size (if (= axis :horizontal) :width :height) + ; The rectangle that wraps the whole selection + wrapper-rect (gsh/selection-rect shapes) + ; Sort shapes by the center point in the given axis + sorted-shapes (sort-by #(coord (gsh/center %)) shapes) + ; Each shape wrapped in its own rectangle + wrapped-shapes (map #(gsh/selection-rect [%]) sorted-shapes) + ; The total space between shapes + space (reduce - (size wrapper-rect) (map size wrapped-shapes))] + + (if (<= space 0) + shapes + (let [unit-space (/ space (- (count wrapped-shapes) 1)) + ; Calculate the distance we need to move each shape. + ; The new position of each one is the position of the + ; previous one plus its size plus the unit space. + deltas (loop [shapes' wrapped-shapes + start-pos (coord wrapper-rect) + deltas []] + + (let [first-shape (first shapes') + delta (- start-pos (coord first-shape)) + new-pos (+ start-pos (size first-shape) unit-space)] + + (if (= (count shapes') 1) + (conj deltas delta) + (recur (rest shapes') + new-pos + (conj deltas delta)))))] + + (mapcat #(gsh/recursive-move %1 {coord %2 other-coord 0} objects) + sorted-shapes deltas))))) + +;; Adjusto to viewport + +(defn adjust-to-viewport + ([viewport srect] (adjust-to-viewport viewport srect nil)) + ([viewport srect {:keys [padding] :or {padding 0}}] + (let [gprop (/ (:width viewport) (:height viewport)) + srect (-> srect + (update :x #(- % padding)) + (update :y #(- % padding)) + (update :width #(+ % padding padding)) + (update :height #(+ % padding padding))) + width (:width srect) + height (:height srect) + lprop (/ width height)] + (cond + (> gprop lprop) + (let [width' (* (/ width lprop) gprop) + padding (/ (- width' width) 2)] + (-> srect + (update :x #(- % padding)) + (assoc :width width'))) + + (< gprop lprop) + (let [height' (/ (* height lprop) gprop) + padding (/ (- height' height) 2)] + (-> srect + (update :y #(- % padding)) + (assoc :height height'))) + + :else srect)))) diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index b191ed5bf9..0bb89d5cfd 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -13,6 +13,9 @@ [app.common.spec :as us] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.transforms :as gtr] + [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] [app.common.data :as d])) @@ -90,39 +93,6 @@ dy (if y (- (-chk y) (-chk (:y shape))) 0)] (move shape (gpt/point dx dy)))) -;; --- Center - -(declare center-rect) -(declare center-path) - -(defn center - "Calculate the center of the shape." - [shape] - (case (:type shape) - :curve (center-path shape) - :path (center-path shape) - (center-rect shape))) - -(defn- center-rect - [{:keys [x y width height] :as shape}] - (gpt/point (+ x (/ width 2)) (+ y (/ height 2)))) - -(defn- center-path - [{:keys [segments] :as shape}] - (let [minx (apply min (map :x segments)) - miny (apply min (map :y segments)) - maxx (apply max (map :x segments)) - maxy (apply max (map :y segments))] - (gpt/point (/ (+ minx maxx) 2) (/ (+ miny maxy) 2)))) - -(defn center->rect - "Creates a rect given a center and a width and height" - [center width height] - {:x (- (:x center) (/ width 2)) - :y (- (:y center) (/ height 2)) - :width width - :height height}) - ;; --- Proportions (declare assign-proportions-path) @@ -219,9 +189,6 @@ :image (setup-image shape props) (setup-rect shape props))) -(declare shape->points) -(declare points->selrect) - (defn- setup-rect "A specialized function for setup rect-like shapes." [shape {:keys [x y width height]}] @@ -230,8 +197,8 @@ :y y :width width :height height) - (assoc $ :points (shape->points $)) - (assoc $ :selrect (points->selrect (:points $))))) + (assoc $ :points (gtr/shape->points $)) + (assoc $ :selrect (gpr/points->selrect (:points $))))) (defn- setup-image [{:keys [metadata] :as shape} {:keys [x y width height] :as props}] @@ -241,91 +208,6 @@ (:height metadata)) :proportion-lock true))) -;; --- Coerce to Rect-like shape. - -(declare path->rect-shape) -(declare group->rect-shape) -(declare rect->rect-shape) - -;; TODO: completly remove - -(defn shape->rect-shape - "Coerce shape to rect like shape." - - [{:keys [type] :as shape}] - (case type - (:curve :path) (path->rect-shape shape) - (rect->rect-shape shape))) - -;; -- Points - -(declare transform-shape-point) - -(defn shape->points [shape] - (let [points (case (:type shape) - (:curve :path) (:segments shape) - (let [{:keys [x y width height]} shape] - [(gpt/point x y) - (gpt/point (+ x width) y) - (gpt/point (+ x width) (+ y height)) - (gpt/point x (+ y height))]))] - (->> points - (map #(transform-shape-point % shape (:transform shape (gmt/matrix)))) - (map gpt/round) - (vec)))) - -(defn points->selrect [points] - (let [minx (transduce (map :x) min ##Inf points) - miny (transduce (map :y) min ##Inf points) - maxx (transduce (map :x) max ##-Inf points) - maxy (transduce (map :y) max ##-Inf points)] - {:x1 minx - :y1 miny - :x2 maxx - :y2 maxy - :x minx - :y miny - :width (- maxx minx) - :height (- maxy miny) - :type :rect})) - -;; Shape->PATH - -(declare rect->path) - -(defn shape->path - [shape] - (case (:type shape) - (:curve :path) shape - (rect->path shape))) - -(defn rect->path - [{:keys [x y width height] :as shape}] - - (let [points [(gpt/point x y) - (gpt/point (+ x width) y) - (gpt/point (+ x width) (+ y height)) - (gpt/point x (+ y height)) - (gpt/point x y)]] - (-> shape - (assoc :type :path) - (assoc :segments points)))) - -;; --- SHAPE -> RECT - -(defn- rect->rect-shape - [{:keys [x y width height] :as shape}] - (assoc shape - :x1 x - :y1 y - :x2 (+ x width) - :y2 (+ y height))) - -(defn- path->rect-shape - [{:keys [segments] :as shape}] - (merge shape - {:type :rect} - (:selrect shape))) ;; --- Resolve Shape @@ -347,51 +229,6 @@ (translate-from-frame shape pobj) (recur (get objects (:parent pobj)))))) -;; --- Transform Shape - -(declare transform-rect) -(declare transform-path) - -(defn transform - "Apply the matrix transformation to shape." - [{:keys [type] :as shape} xfmt] - (if (gmt/matrix? xfmt) - (case type - :path (transform-path shape xfmt) - :curve (transform-path shape xfmt) - (transform-rect shape xfmt)) - shape)) - -(defn center-transform [shape matrix] - (let [shape-center (center shape)] - (-> shape - (transform - (-> (gmt/matrix) - (gmt/translate shape-center) - (gmt/multiply matrix) - (gmt/translate (gpt/negate shape-center))))))) - -(defn- transform-rect - [{:keys [x y width height] :as shape} mx] - (let [tl (gpt/transform (gpt/point x y) mx) - tr (gpt/transform (gpt/point (+ x width) y) mx) - bl (gpt/transform (gpt/point x (+ y height)) mx) - br (gpt/transform (gpt/point (+ x width) (+ y height)) mx) - ;; TODO: replace apply with transduce (performance) - minx (apply min (map :x [tl tr bl br])) - maxx (apply max (map :x [tl tr bl br])) - miny (apply min (map :y [tl tr bl br])) - maxy (apply max (map :y [tl tr bl br]))] - (assoc shape - :x minx - :y miny - :width (- maxx minx) - :height (- maxy miny)))) - -(defn- transform-path - [{:keys [segments] :as shape} xfmt] - (let [segments (mapv #(gpt/transform % xfmt) segments)] - (assoc shape :segments segments))) ;; --- Outer Rect @@ -426,107 +263,14 @@ [shape {:keys [x y] :as frame}] (move shape (gpt/point x y))) -;; --- Alignment - -(s/def ::align-axis #{:hleft :hcenter :hright :vtop :vcenter :vbottom}) - -(declare calc-align-pos) - -(defn align-to-rect - "Move the shape so that it is aligned with the given rectangle - in the given axis. Take account the form of the shape and the - possible rotation. What is aligned is the rectangle that wraps - the shape with the given rectangle. If the shape is a group, - move also all of its recursive children." - [shape rect axis objects] - (let [wrapper-rect (selection-rect [shape]) - align-pos (calc-align-pos wrapper-rect rect axis) - delta {:x (- (:x align-pos) (:x wrapper-rect)) - :y (- (:y align-pos) (:y wrapper-rect))}] - (recursive-move shape delta objects))) - -(defn calc-align-pos - [wrapper-rect rect axis] - (case axis - :hleft (let [left (:x rect)] - {:x left - :y (:y wrapper-rect)}) - - :hcenter (let [center (+ (:x rect) (/ (:width rect) 2))] - {:x (- center (/ (:width wrapper-rect) 2)) - :y (:y wrapper-rect)}) - - :hright (let [right (+ (:x rect) (:width rect))] - {:x (- right (:width wrapper-rect)) - :y (:y wrapper-rect)}) - - :vtop (let [top (:y rect)] - {:x (:x wrapper-rect) - :y top}) - - :vcenter (let [center (+ (:y rect) (/ (:height rect) 2))] - {:x (:x wrapper-rect) - :y (- center (/ (:height wrapper-rect) 2))}) - - :vbottom (let [bottom (+ (:y rect) (:height rect))] - {:x (:x wrapper-rect) - :y (- bottom (:height wrapper-rect))}))) - -;; --- Distribute - -(s/def ::dist-axis #{:horizontal :vertical}) - -(defn distribute-space - "Distribute equally the space between shapes in the given axis. If - there is no space enough, it does nothing. It takes into account - the form of the shape and the rotation, what is distributed is - the wrapping recangles of the shapes. If any shape is a group, - move also all of its recursive children." - [shapes axis objects] - (let [coord (if (= axis :horizontal) :x :y) - other-coord (if (= axis :horizontal) :y :x) - size (if (= axis :horizontal) :width :height) - ; The rectangle that wraps the whole selection - wrapper-rect (selection-rect shapes) - ; Sort shapes by the center point in the given axis - sorted-shapes (sort-by #(coord (center %)) shapes) - ; Each shape wrapped in its own rectangle - wrapped-shapes (map #(selection-rect [%]) sorted-shapes) - ; The total space between shapes - space (reduce - (size wrapper-rect) (map size wrapped-shapes))] - - (if (<= space 0) - shapes - (let [unit-space (/ space (- (count wrapped-shapes) 1)) - ; Calculate the distance we need to move each shape. - ; The new position of each one is the position of the - ; previous one plus its size plus the unit space. - deltas (loop [shapes' wrapped-shapes - start-pos (coord wrapper-rect) - deltas []] - - (let [first-shape (first shapes') - delta (- start-pos (coord first-shape)) - new-pos (+ start-pos (size first-shape) unit-space)] - - (if (= (count shapes') 1) - (conj deltas delta) - (recur (rest shapes') - new-pos - (conj deltas delta)))))] - - (mapcat #(recursive-move %1 {coord %2 other-coord 0} objects) - sorted-shapes deltas))))) - - ;; --- Helpers (defn contained-in? "Check if a shape is contained in the provided selection rect." [shape selrect] - (let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (shape->rect-shape selrect) - {rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (shape->rect-shape shape)] + (let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (gpr/shape->rect-shape selrect) + {rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (gpr/shape->rect-shape shape)] (and (neg? (- sy1 ry1)) (neg? (- sx1 rx1)) (pos? (- sy2 ry2)) @@ -535,8 +279,8 @@ (defn overlaps? "Check if a shape overlaps with provided selection rect." [shape selrect] - (let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (shape->rect-shape selrect) - {rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (shape->rect-shape shape)] + (let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (gpr/shape->rect-shape selrect) + {rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (gpr/shape->rect-shape shape)] (and (< rx1 sx2) (> rx2 sx1) (< ry1 sy2) @@ -564,43 +308,6 @@ :type :rect}] (overlaps? shape selrect))) -(defn calculate-rec-path-skew-angle - [path-shape] - (let [p1 (get-in path-shape [:segments 2]) - p2 (get-in path-shape [:segments 3]) - p3 (get-in path-shape [:segments 4]) - v1 (gpt/to-vec p1 p2) - v2 (gpt/to-vec p2 p3)] - (- 90 (gpt/angle-with-other v1 v2)))) - -(defn calculate-rec-path-height - "Calculates the height of a paralelogram given by the path" - [path-shape] - (let [p1 (get-in path-shape [:segments 2]) - p2 (get-in path-shape [:segments 3]) - p3 (get-in path-shape [:segments 4]) - v1 (gpt/to-vec p1 p2) - v2 (gpt/to-vec p2 p3) - angle (gpt/angle-with-other v1 v2)] - (* (gpt/length v2) (mth/sin (mth/radians angle))))) - -(defn calculate-rec-path-rotation - [path-shape1 path-shape2 resize-vector] - - (let [idx-1 0 - idx-2 (cond (and (neg? (:x resize-vector)) (pos? (:y resize-vector))) 1 - (and (neg? (:x resize-vector)) (neg? (:y resize-vector))) 2 - (and (pos? (:x resize-vector)) (neg? (:y resize-vector))) 3 - :else 0) - p1 (get-in path-shape1 [:segments idx-1]) - p2 (get-in path-shape2 [:segments idx-2]) - v1 (gpt/to-vec (center path-shape1) p1) - v2 (gpt/to-vec (center path-shape2) p2) - - rot-angle (gpt/angle-with-other v1 v2) - rot-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)] - (* rot-sign rot-angle))) - (defn pad-selrec ([selrect] (pad-selrec selrect 1)) ([selrect size] @@ -658,305 +365,6 @@ (and (>= s1c1 s2c1) (<= s1c1 s2c2)) (and (>= s1c2 s2c1) (<= s1c2 s2c2))))) -(defn transform-shape-point - "Transform a point around the shape center" - [point shape transform] - (let [shape-center (center shape)] - (gpt/transform - point - (-> (gmt/multiply - (gmt/translate-matrix shape-center) - transform - (gmt/translate-matrix (gpt/negate shape-center))))))) - -(defn transform-apply-modifiers - [shape] - (let [modifiers (:modifiers shape) - ds-modifier (:displacement modifiers (gmt/matrix)) - {res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1)) - - ;; Normalize x/y vector coordinates because scale by 0 is infinite - res-x (cond - (and (< res-x 0) (> res-x -0.01)) -0.01 - (and (>= res-x 0) (< res-x 0.01)) 0.01 - :else res-x) - - res-y (cond - (and (< res-y 0) (> res-y -0.01)) -0.01 - (and (>= res-y 0) (< res-y 0.01)) 0.01 - :else res-y) - - resize (gpt/point res-x res-y) - - origin (:resize-origin modifiers (gpt/point 0 0)) - - resize-transform (:resize-transform modifiers (gmt/matrix)) - resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) - rt-modif (or (:rotation modifiers) 0) - - shape (-> shape - (transform ds-modifier)) - - shape-center (center shape)] - - (-> (shape->path shape) - (transform (-> (gmt/matrix) - - ;; Applies the current resize transformation - (gmt/translate origin) - (gmt/multiply resize-transform) - (gmt/scale resize) - (gmt/multiply resize-transform-inverse) - (gmt/translate (gpt/negate origin)) - - ;; Applies the stacked transformations - (gmt/translate shape-center) - (gmt/multiply (gmt/rotate-matrix rt-modif)) - (gmt/multiply (:transform shape (gmt/matrix))) - (gmt/translate (gpt/negate shape-center))))))) - -(defn rect-path-dimensions [rect-path] - (let [seg (:segments rect-path) - [width height] (mapv (fn [[c1 c2]] (gpt/distance c1 c2)) (take 2 (d/zip seg (rest seg))))] - {:width width - :height height})) - -(defn calculate-stretch [shape-path transform-inverse] - (let [shape-center (center shape-path) - shape-path-temp (transform - shape-path - (-> (gmt/matrix) - (gmt/translate shape-center) - (gmt/multiply transform-inverse) - (gmt/translate (gpt/negate shape-center)))) - - shape-path-temp-rec (shape->rect-shape shape-path-temp) - shape-path-temp-dim (rect-path-dimensions shape-path-temp)] - (gpt/divide (gpt/point (:width shape-path-temp-rec) (:height shape-path-temp-rec)) - (gpt/point (:width shape-path-temp-dim) (:height shape-path-temp-dim))))) - -(defn fix-invalid-rect-values - [rect-shape] - (letfn [(check [num] - (if (or (nil? num) (mth/nan? num) (= ##Inf num) (= ##-Inf num)) 0 num)) - (to-positive [num] (if (< num 1) 1 num))] - (-> rect-shape - (update :x check) - (update :y check) - (update :width (comp to-positive check)) - (update :height (comp to-positive check))))) - -(defn transform-rect-shape - [shape] - (let [;; Apply modifiers to the rect as a path so we have the end shape expected - shape-path (transform-apply-modifiers shape) - shape-center (center shape-path) - resize-vector (-> (get-in shape [:modifiers :resize-vector] (gpt/point 1 1)) - (update :x #(if (zero? %) 1 %)) - (update :y #(if (zero? %) 1 %))) - - ;; Reverse the current transformation stack to get the base rectangle - shape-path-temp (center-transform shape-path (:transform-inverse shape (gmt/matrix))) - shape-path-temp-dim (rect-path-dimensions shape-path-temp) - shape-path-temp-rec (shape->rect-shape shape-path-temp) - - ;; This rectangle is the new data for the current rectangle. We want to change our rectangle - ;; to have this width, height, x, y - rec (center->rect shape-center (:width shape-path-temp-dim) (:height shape-path-temp-dim)) - rec (fix-invalid-rect-values rec) - rec-path (rect->path rec) - - ;; The next matrix is a series of transformations we have to do to the previous rec so that - ;; after applying them the end result is the `shape-path-temp` - ;; This is compose of three transformations: skew, resize and rotation - stretch-matrix (gmt/matrix) - - skew-angle (calculate-rec-path-skew-angle shape-path-temp) - - ;; When one of the axis is flipped we have to reverse the skew - skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle ) - skew-angle (if (mth/nan? skew-angle) 0 skew-angle) - - - stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) - - h1 (calculate-rec-path-height shape-path-temp) - h2 (calculate-rec-path-height (center-transform rec-path stretch-matrix)) - h3 (/ h1 h2) - h3 (if (mth/nan? h3) 1 h3) - - stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point 1 h3))) - - rotation-angle (calculate-rec-path-rotation (center-transform rec-path stretch-matrix) - shape-path-temp resize-vector) - - stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix) - - ;; This is the inverse to be able to remove the transformation - stretch-matrix-inverse (-> (gmt/matrix) - (gmt/scale (gpt/point 1 h3)) - (gmt/skew (- skew-angle) 0) - (gmt/rotate (- rotation-angle))) - - - new-shape (as-> shape $ - (merge $ rec) - (update $ :x #(mth/precision % 0)) - (update $ :y #(mth/precision % 0)) - (update $ :width #(mth/precision % 0)) - (update $ :height #(mth/precision % 0)) - (update $ :transform #(gmt/multiply (or % (gmt/matrix)) stretch-matrix)) - (update $ :transform-inverse #(gmt/multiply stretch-matrix-inverse (or % (gmt/matrix)))) - (assoc $ :points (shape->points $)) - (assoc $ :selrect (points->selrect (:points $))) - (update $ :selrect fix-invalid-rect-values) - (update $ :rotation #(mod (+ (or % 0) - (or (get-in $ [:modifiers :rotation]) 0)) 360)))] - new-shape)) - -(declare update-path-selrect) -(defn transform-path-shape - [shape] - (-> shape - transform-apply-modifiers - update-path-selrect) - ;; TODO: Addapt for paths is not working - #_(let [shape-path (transform-apply-modifiers shape) - shape-path-center (center shape-path) - - shape-transform-inverse' (-> (gmt/matrix) - (gmt/translate shape-path-center) - (gmt/multiply (:transform-inverse shape (gmt/matrix))) - (gmt/multiply (gmt/rotate-matrix (- (:rotation-modifier shape 0)))) - (gmt/translate (gpt/negate shape-path-center)))] - (-> shape-path - (transform shape-transform-inverse') - (add-rotate-transform (:rotation-modifier shape 0))))) - -(defn transform-shape - "Transform the shape properties given the modifiers" - ([shape] (transform-shape nil shape)) - ([frame shape] - (let [new-shape - (if (:modifiers shape) - (-> (case (:type shape) - (:curve :path) (transform-path-shape shape) - (transform-rect-shape shape)) - (dissoc :modifiers)) - shape)] - (cond-> new-shape - frame (translate-to-frame frame))))) - - -(defn transform-matrix - "Returns a transformation matrix without changing the shape properties. - The result should be used in a `transform` attribute in svg" - ([{:keys [x y] :as shape}] - (let [shape-center (center shape)] - (-> (gmt/matrix) - (gmt/translate shape-center) - (gmt/multiply (:transform shape (gmt/matrix))) - (gmt/translate (gpt/negate shape-center)))))) - -(defn update-path-selrect [shape] - (as-> shape $ - (assoc $ :points (shape->points $)) - (assoc $ :selrect (points->selrect (:points $))) - (assoc $ :x (get-in $ [:selrect :x])) - (assoc $ :y (get-in $ [:selrect :y])) - (assoc $ :width (get-in $ [:selrect :width])) - (assoc $ :height (get-in $ [:selrect :height])))) - -(defn adjust-to-viewport - ([viewport srect] (adjust-to-viewport viewport srect nil)) - ([viewport srect {:keys [padding] :or {padding 0}}] - (let [gprop (/ (:width viewport) (:height viewport)) - srect (-> srect - (update :x #(- % padding)) - (update :y #(- % padding)) - (update :width #(+ % padding padding)) - (update :height #(+ % padding padding))) - width (:width srect) - height (:height srect) - lprop (/ width height)] - (cond - (> gprop lprop) - (let [width' (* (/ width lprop) gprop) - padding (/ (- width' width) 2)] - (-> srect - (update :x #(- % padding)) - (assoc :width width'))) - - (< gprop lprop) - (let [height' (/ (* height lprop) gprop) - padding (/ (- height' height) 2)] - (-> srect - (update :y #(- % padding)) - (assoc :height height'))) - - :else srect)))) - -(defn get-attrs-multi - [shapes attrs] - ;; Extract some attributes of a list of shapes. - ;; For each attribute, if the value is the same in all shapes, - ;; wll take this value. If there is any shape that is different, - ;; the value of the attribute will be the keyword :multiple. - ;; - ;; If some shape has the value nil in any attribute, it's - ;; considered a different value. If the shape does not contain - ;; the attribute, it's ignored in the final result. - ;; - ;; Example: - ;; (def shapes [{:stroke-color "#ff0000" - ;; :stroke-width 3 - ;; :fill-color "#0000ff" - ;; :x 1000 :y 2000 :rx nil} - ;; {:stroke-width "#ff0000" - ;; :stroke-width 5 - ;; :x 1500 :y 2000}]) - ;; - ;; (get-attrs-multi shapes [:stroke-color - ;; :stroke-width - ;; :fill-color - ;; :rx - ;; :ry]) - ;; >>> {:stroke-color "#ff0000" - ;; :stroke-width :multiple - ;; :fill-color "#0000ff" - ;; :rx nil - ;; :ry nil} - ;; - (let [defined-shapes (filter some? shapes) - - combine-value (fn [v1 v2] (cond - (= v1 v2) v1 - (= v1 :undefined) v2 - (= v2 :undefined) v1 - :else :multiple)) - - combine-values (fn [attrs shape values] - (map #(combine-value (get shape % :undefined) - (get values % :undefined)) attrs)) - - select-attrs (fn [shape attrs] - (zipmap attrs (map #(get shape % :undefined) attrs))) - - reducer (fn [result shape] - (zipmap attrs (combine-values attrs shape result))) - - combined (reduce reducer - (select-attrs (first defined-shapes) attrs) - (rest defined-shapes)) - - cleanup-value (fn [value] - (if (= value :undefined) nil value)) - - cleanup (fn [result] - (zipmap attrs (map #(cleanup-value (get result %)) attrs)))] - - (cleanup combined))) - (defn setup-selrect [{:keys [x y width height] :as shape}] (-> shape @@ -965,3 +373,18 @@ :x1 x :y1 y :x2 (+ x width) :y2 (+ y height)}))) + +;; EXPORTS +(def center gco/center) + +(def shape->rect-shape gpr/shape->rect-shape) +(def fix-invalid-rect-values gtr/fix-invalid-rect-values) +(def rect->rect-shape gpr/rect->rect-shape) +(def points->selrect gpr/points->selrect) + +(def transform-shape-point gtr/transform-shape-point) +(def update-path-selrect gtr/update-path-selrect) +(def transform gtr/transform) +(defn transform-shape [shape] (gtr/transform-shape shape)) +(def transform-matrix gtr/transform-matrix) + diff --git a/common/app/common/geom/shapes/common.cljc b/common/app/common/geom/shapes/common.cljc new file mode 100644 index 0000000000..1ef8728f7e --- /dev/null +++ b/common/app/common/geom/shapes/common.cljc @@ -0,0 +1,52 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.common.geom.shapes.common + (:require + [clojure.spec.alpha :as s] + [app.common.spec :as us] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gpa] + [app.common.math :as mth] + [app.common.data :as d])) + +;; --- Center + +(declare center-rect) +(declare center-path) + +(defn center + "Calculate the center of the shape." + [shape] + (case (:type shape) + :curve (center-path shape) + :path (center-path shape) + (center-rect shape))) + +(defn- center-rect + [{:keys [x y width height] :as shape}] + (gpt/point (+ x (/ width 2)) (+ y (/ height 2)))) + +(defn- center-path + [{:keys [segments] :as shape}] + (let [minx (apply min (map :x segments)) + miny (apply min (map :y segments)) + maxx (apply max (map :x segments)) + maxy (apply max (map :y segments))] + (gpt/point (/ (+ minx maxx) 2) (/ (+ miny maxy) 2)))) + +(defn center->rect + "Creates a rect given a center and a width and height" + [center width height] + {:x (- (:x center) (/ width 2)) + :y (- (:y center) (/ height 2)) + :width width + :height height}) + diff --git a/common/app/common/geom/shapes/path.cljc b/common/app/common/geom/shapes/path.cljc new file mode 100644 index 0000000000..60810fc7fb --- /dev/null +++ b/common/app/common/geom/shapes/path.cljc @@ -0,0 +1,21 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.common.geom.shapes.path + (:require + [clojure.spec.alpha :as s] + [app.common.spec :as us] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.math :as mth] + [app.common.data :as d])) + +(defn content->points [content] + (map #(gpt/point (-> % :param :x) (-> % :param :y)) content)) + diff --git a/common/app/common/geom/shapes/rect.cljc b/common/app/common/geom/shapes/rect.cljc new file mode 100644 index 0000000000..8f06cae97b --- /dev/null +++ b/common/app/common/geom/shapes/rect.cljc @@ -0,0 +1,83 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.common.geom.shapes.rect + (:require + [clojure.spec.alpha :as s] + [app.common.spec :as us] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as gpa] + [app.common.geom.shapes.common :as gco] + [app.common.math :as mth] + [app.common.data :as d])) + +;; --- SHAPE -> RECT + +(defn- rect->rect-shape + [{:keys [x y width height] :as shape}] + (assoc shape + :x1 x + :y1 y + :x2 (+ x width) + :y2 (+ y height))) + +(defn- path->rect-shape + [{:keys [segments] :as shape}] + (merge shape + {:type :rect} + (:selrect shape))) + +(defn shape->rect-shape + "Coerce shape to rect like shape." + + [{:keys [type] :as shape}] + (case type + (:curve :path) (path->rect-shape shape) + (rect->rect-shape shape))) + +;; Shape->PATH + +(declare rect->path) + +(defn shape->path + [shape] + (case (:type shape) + (:curve :path) shape + (rect->path shape))) + +(defn rect->path + [{:keys [x y width height] :as shape}] + + (let [points [(gpt/point x y) + (gpt/point (+ x width) y) + (gpt/point (+ x width) (+ y height)) + (gpt/point x (+ y height)) + (gpt/point x y)]] + (-> shape + (assoc :type :path) + (assoc :segments points)))) + +;; -- Points + +(defn points->selrect [points] + (let [minx (transduce (map :x) min ##Inf points) + miny (transduce (map :y) min ##Inf points) + maxx (transduce (map :x) max ##-Inf points) + maxy (transduce (map :y) max ##-Inf points)] + {:x1 minx + :y1 miny + :x2 maxx + :y2 maxy + :x minx + :y miny + :width (- maxx minx) + :height (- maxy miny) + :type :rect})) + diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc new file mode 100644 index 0000000000..ecb2acd163 --- /dev/null +++ b/common/app/common/geom/shapes/transforms.cljc @@ -0,0 +1,342 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.common.geom.shapes.transforms + (:require + [clojure.spec.alpha :as s] + [app.common.spec :as us] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.path :as gpa] + [app.common.geom.shapes.rect :as gpr] + [app.common.math :as mth] + [app.common.data :as d])) + +;; --- Transform Shape + +(declare transform-rect) +(declare transform-path) + +(defn transform + "Apply the matrix transformation to shape." + [{:keys [type] :as shape} xfmt] + (if (gmt/matrix? xfmt) + (case type + :path (transform-path shape xfmt) + :curve (transform-path shape xfmt) + (transform-rect shape xfmt)) + shape)) + +(defn center-transform [shape matrix] + (let [shape-center (gco/center shape)] + (-> shape + (transform + (-> (gmt/matrix) + (gmt/translate shape-center) + (gmt/multiply matrix) + (gmt/translate (gpt/negate shape-center))))))) + +(defn- transform-rect + [{:keys [x y width height] :as shape} mx] + (let [tl (gpt/transform (gpt/point x y) mx) + tr (gpt/transform (gpt/point (+ x width) y) mx) + bl (gpt/transform (gpt/point x (+ y height)) mx) + br (gpt/transform (gpt/point (+ x width) (+ y height)) mx) + ;; TODO: replace apply with transduce (performance) + minx (apply min (map :x [tl tr bl br])) + maxx (apply max (map :x [tl tr bl br])) + miny (apply min (map :y [tl tr bl br])) + maxy (apply max (map :y [tl tr bl br]))] + (assoc shape + :x minx + :y miny + :width (- maxx minx) + :height (- maxy miny)))) + +(defn- transform-path + [{:keys [segments] :as shape} xfmt] + (let [segments (mapv #(gpt/transform % xfmt) segments)] + (assoc shape :segments segments))) + + +(defn transform-shape-point + "Transform a point around the shape center" + [point shape transform] + (let [shape-center (gco/center shape)] + (gpt/transform + point + (-> (gmt/multiply + (gmt/translate-matrix shape-center) + transform + (gmt/translate-matrix (gpt/negate shape-center))))))) + +(defn shape->points [shape] + (let [points (case (:type shape) + (:curve :path) (if (:content shape) + (gpa/content->points (:content shape)) + (:segments shape)) + (let [{:keys [x y width height]} shape] + [(gpt/point x y) + (gpt/point (+ x width) y) + (gpt/point (+ x width) (+ y height)) + (gpt/point x (+ y height))]))] + (->> points + (map #(transform-shape-point % shape (:transform shape (gmt/matrix)))) + (map gpt/round) + (vec)))) + +(defn rect-path-dimensions [rect-path] + (let [seg (:segments rect-path) + [width height] (mapv (fn [[c1 c2]] (gpt/distance c1 c2)) (take 2 (d/zip seg (rest seg))))] + {:width width + :height height})) + +(defn update-path-selrect [shape] + (as-> shape $ + (assoc $ :points (shape->points $)) + (assoc $ :selrect (gpr/points->selrect (:points $))) + (assoc $ :x (get-in $ [:selrect :x])) + (assoc $ :y (get-in $ [:selrect :y])) + (assoc $ :width (get-in $ [:selrect :width])) + (assoc $ :height (get-in $ [:selrect :height])))) + +(defn fix-invalid-rect-values + [rect-shape] + (letfn [(check [num] + (if (or (nil? num) (mth/nan? num) (= ##Inf num) (= ##-Inf num)) 0 num)) + (to-positive [num] (if (< num 1) 1 num))] + (-> rect-shape + (update :x check) + (update :y check) + (update :width (comp to-positive check)) + (update :height (comp to-positive check))))) + +(defn calculate-rec-path-skew-angle + [path-shape] + (let [p1 (get-in path-shape [:segments 2]) + p2 (get-in path-shape [:segments 3]) + p3 (get-in path-shape [:segments 4]) + v1 (gpt/to-vec p1 p2) + v2 (gpt/to-vec p2 p3)] + (- 90 (gpt/angle-with-other v1 v2)))) + +(defn calculate-rec-path-height + "Calculates the height of a paralelogram given by the path" + [path-shape] + (let [p1 (get-in path-shape [:segments 2]) + p2 (get-in path-shape [:segments 3]) + p3 (get-in path-shape [:segments 4]) + v1 (gpt/to-vec p1 p2) + v2 (gpt/to-vec p2 p3) + angle (gpt/angle-with-other v1 v2)] + (* (gpt/length v2) (mth/sin (mth/radians angle))))) + +(defn calculate-rec-path-rotation + [path-shape1 path-shape2 resize-vector] + + (let [idx-1 0 + idx-2 (cond (and (neg? (:x resize-vector)) (pos? (:y resize-vector))) 1 + (and (neg? (:x resize-vector)) (neg? (:y resize-vector))) 2 + (and (pos? (:x resize-vector)) (neg? (:y resize-vector))) 3 + :else 0) + p1 (get-in path-shape1 [:segments idx-1]) + p2 (get-in path-shape2 [:segments idx-2]) + v1 (gpt/to-vec (gco/center path-shape1) p1) + v2 (gpt/to-vec (gco/center path-shape2) p2) + + rot-angle (gpt/angle-with-other v1 v2) + rot-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)] + (* rot-sign rot-angle))) + + +(defn transform-apply-modifiers + [shape] + (let [modifiers (:modifiers shape) + ds-modifier (:displacement modifiers (gmt/matrix)) + {res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1)) + + ;; Normalize x/y vector coordinates because scale by 0 is infinite + res-x (cond + (and (< res-x 0) (> res-x -0.01)) -0.01 + (and (>= res-x 0) (< res-x 0.01)) 0.01 + :else res-x) + + res-y (cond + (and (< res-y 0) (> res-y -0.01)) -0.01 + (and (>= res-y 0) (< res-y 0.01)) 0.01 + :else res-y) + + resize (gpt/point res-x res-y) + + origin (:resize-origin modifiers (gpt/point 0 0)) + + resize-transform (:resize-transform modifiers (gmt/matrix)) + resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) + rt-modif (or (:rotation modifiers) 0) + + shape (-> shape + (transform ds-modifier)) + + shape-center (gco/center shape)] + + (-> (gpr/shape->path shape) + (transform (-> (gmt/matrix) + + ;; Applies the current resize transformation + (gmt/translate origin) + (gmt/multiply resize-transform) + (gmt/scale resize) + (gmt/multiply resize-transform-inverse) + (gmt/translate (gpt/negate origin)) + + ;; Applies the stacked transformations + (gmt/translate shape-center) + (gmt/multiply (gmt/rotate-matrix rt-modif)) + (gmt/multiply (:transform shape (gmt/matrix))) + (gmt/translate (gpt/negate shape-center))))))) + +(defn transform-path-shape + [shape] + (-> shape + transform-apply-modifiers + update-path-selrect) + ;; TODO: Addapt for paths is not working + #_(let [shape-path (transform-apply-modifiers shape) + shape-path-center (center shape-path) + + shape-transform-inverse' (-> (gmt/matrix) + (gmt/translate shape-path-center) + (gmt/multiply (:transform-inverse shape (gmt/matrix))) + (gmt/multiply (gmt/rotate-matrix (- (:rotation-modifier shape 0)))) + (gmt/translate (gpt/negate shape-path-center)))] + (-> shape-path + (transform shape-transform-inverse') + (add-rotate-transform (:rotation-modifier shape 0))))) + +(defn transform-rect-shape + [shape] + (let [;; Apply modifiers to the rect as a path so we have the end shape expected + shape-path (transform-apply-modifiers shape) + shape-center (gco/center shape-path) + resize-vector (-> (get-in shape [:modifiers :resize-vector] (gpt/point 1 1)) + (update :x #(if (zero? %) 1 %)) + (update :y #(if (zero? %) 1 %))) + + ;; Reverse the current transformation stack to get the base rectangle + shape-path-temp (center-transform shape-path (:transform-inverse shape (gmt/matrix))) + shape-path-temp-dim (rect-path-dimensions shape-path-temp) + shape-path-temp-rec (gpr/shape->rect-shape shape-path-temp) + + ;; This rectangle is the new data for the current rectangle. We want to change our rectangle + ;; to have this width, height, x, y + rec (gco/center->rect shape-center (:width shape-path-temp-dim) (:height shape-path-temp-dim)) + rec (fix-invalid-rect-values rec) + rec-path (gpr/rect->path rec) + + ;; The next matrix is a series of transformations we have to do to the previous rec so that + ;; after applying them the end result is the `shape-path-temp` + ;; This is compose of three transformations: skew, resize and rotation + stretch-matrix (gmt/matrix) + + skew-angle (calculate-rec-path-skew-angle shape-path-temp) + + ;; When one of the axis is flipped we have to reverse the skew + skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle ) + skew-angle (if (mth/nan? skew-angle) 0 skew-angle) + + + stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) + + h1 (calculate-rec-path-height shape-path-temp) + h2 (calculate-rec-path-height (center-transform rec-path stretch-matrix)) + h3 (/ h1 h2) + h3 (if (mth/nan? h3) 1 h3) + + stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point 1 h3))) + + rotation-angle (calculate-rec-path-rotation (center-transform rec-path stretch-matrix) + shape-path-temp resize-vector) + + stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix) + + ;; This is the inverse to be able to remove the transformation + stretch-matrix-inverse (-> (gmt/matrix) + (gmt/scale (gpt/point 1 h3)) + (gmt/skew (- skew-angle) 0) + (gmt/rotate (- rotation-angle))) + + new-shape (as-> shape $ + (merge $ rec) + (update $ :x #(mth/precision % 0)) + (update $ :y #(mth/precision % 0)) + (update $ :width #(mth/precision % 0)) + (update $ :height #(mth/precision % 0)) + (update $ :transform #(gmt/multiply (or % (gmt/matrix)) stretch-matrix)) + (update $ :transform-inverse #(gmt/multiply stretch-matrix-inverse (or % (gmt/matrix)))) + (assoc $ :points (shape->points $)) + (assoc $ :selrect (gpr/points->selrect (:points $))) + (update $ :selrect fix-invalid-rect-values) + (update $ :rotation #(mod (+ (or % 0) + (or (get-in $ [:modifiers :rotation]) 0)) 360)))] + new-shape)) + +(defn transform-shape + "Transform the shape properties given the modifiers" + ([shape] + + (letfn [(transform-by-type [shape] + (case (:type shape) + (:curve :path) + (transform-path-shape shape) + + #_:default + (transform-rect-shape shape)))] + + (cond-> shape + (:modifiers shape) (transform-by-type) + :always (dissoc :modifiers))) + + #_(cond-> shape + (and (:modifiers shape) (#{:curve :path} (:type shape))) + (transform-path-shape shape) + + (and (:modifiers shape) (not (#{:curve :path} (:type shape)))) + (transform-rect-shape shape) + + true + (dissoc :modifiers) + )) + #_([frame shape kk] + + + + + #_(if (:modifiers shape) + (-> (case (:type shape) + (:curve :path) (transform-path-shape shape) + (transform-rect-shape shape)) + (dissoc :modifiers)) + shape) + #_(let [new-shape + ] + + #_(cond-> new-shape + frame (translate-to-frame frame))))) + + +(defn transform-matrix + "Returns a transformation matrix without changing the shape properties. + The result should be used in a `transform` attribute in svg" + ([{:keys [x y] :as shape}] + (let [shape-center (gco/center shape)] + (-> (gmt/matrix) + (gmt/translate shape-center) + (gmt/multiply (:transform shape (gmt/matrix))) + (gmt/translate (gpt/negate shape-center)))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index ea2c54522b..ea4755fcc6 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -13,7 +13,8 @@ [app.common.exceptions :as ex] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] + [app.common.geom.shapes :as gsh] + [app.common.geom.align :as gal] [app.common.math :as mth] [app.common.pages :as cp] [app.common.pages-helpers :as cph] @@ -339,7 +340,7 @@ (let [page-id (:current-page-id state) objects (dwc/lookup-page-objects state page-id) shapes (cph/select-toplevel-shapes objects {:include-frames? true}) - srect (geom/selection-rect shapes) + srect (gsh/selection-rect shapes) local (assoc local :vport size :zoom 1)] (cond (or (not (mth/finite? (:width srect))) @@ -348,7 +349,7 @@ (or (> (:width srect) width) (> (:height srect) height)) - (let [srect (geom/adjust-to-viewport size srect {:padding 40}) + (let [srect (gal/adjust-to-viewport size srect {:padding 40}) zoom (/ (:width size) (:width srect))] (-> local (assoc :zoom zoom) @@ -471,10 +472,10 @@ (let [vbox (update vbox :x + (:left-offset vbox)) new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom) old-zoom (:zoom local) - center (if center center (geom/center vbox)) + center (if center center (gsh/center vbox)) scale (/ old-zoom new-zoom) mtx (gmt/scale-matrix (gpt/point scale) center) - vbox' (geom/transform vbox mtx) + vbox' (gsh/transform vbox mtx) vbox' (update vbox' :x - (:left-offset vbox))] (-> local (assoc :zoom new-zoom) @@ -510,14 +511,14 @@ (let [page-id (:current-page-id state) objects (dwc/lookup-page-objects state page-id) shapes (cph/select-toplevel-shapes objects {:include-frames? true}) - srect (geom/selection-rect shapes)] + srect (gsh/selection-rect shapes)] (if (or (mth/nan? (:width srect)) (mth/nan? (:height srect))) state (update state :workspace-local (fn [{:keys [vbox vport] :as local}] - (let [srect (geom/adjust-to-viewport vport srect {:padding 40}) + (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) zoom (/ (:width vport) (:width srect))] (-> local (assoc :zoom zoom) @@ -534,10 +535,10 @@ objects (dwc/lookup-page-objects state page-id) srect (->> selected (map #(get objects %)) - (geom/selection-rect))] + (gsh/selection-rect))] (update state :workspace-local (fn [{:keys [vbox vport] :as local}] - (let [srect (geom/adjust-to-viewport vport srect {:padding 40}) + (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) zoom (/ (:width vport) (:width srect))] (-> local (assoc :zoom zoom) @@ -614,8 +615,8 @@ (merge data) (merge {:x x :y y}) (assoc :frame-id frame-id) - (geom/setup-selrect))] (rx/of (add-shape shape)))))) + (gsh/setup-selrect))] ;; --- Update Shape Attrs diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index a4d7938fe2..24ffb0d65e 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -44,7 +44,7 @@ (if (:click-draw? shape) :auto-width :fixed))) shape (-> shape - gsh/transform-shape + (gsh/transform-shape) (dissoc :initialized? :click-draw?))] ;; Add & select the created shape to the workspace (rx/concat diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 34616e94c4..c13a6dda23 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -17,6 +17,7 @@ [goog.object :as gobj] [potok.core :as ptk] [app.common.geom.shapes :as geom] + [app.common.attrs :as attrs] [app.main.data.workspace.common :as dwc] [app.main.fonts :as fonts] [app.util.object :as obj] @@ -125,7 +126,7 @@ (map #(if (is-text-node? %) (merge ut/default-text-attrs %) %)))] - (geom/get-attrs-multi nodes attrs))) + (attrs/get-attrs-multi nodes attrs))) (defn current-text-values [{:keys [editor default attrs shape]}] diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index cf8a129dd1..6629597a8b 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -15,7 +15,8 @@ [app.common.pages :as cp] [app.common.pages-helpers :as cph] [app.common.math :as mth] - [app.common.geom.shapes :as geom] + [app.common.geom.shapes :as gsh] + [app.common.geom.align :as gal] [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] [app.main.ui.shapes.filters :as filters] @@ -42,9 +43,9 @@ (defn- calculate-dimensions [{:keys [objects] :as data} vport] (let [shapes (cph/select-toplevel-shapes objects {:include-frames? true})] - (->> (geom/selection-rect shapes) - (geom/adjust-to-viewport vport) - (geom/fix-invalid-rect-values)))) + (->> (gsh/selection-rect shapes) + (gal/adjust-to-viewport vport) + (gsh/fix-invalid-rect-values)))) (declare shape-wrapper-factory) @@ -55,7 +56,7 @@ (mf/fnc frame-wrapper [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape)) - shape (geom/transform-shape shape)] + shape (gsh/transform-shape shape)] [:> shape-container {:shape shape} [:& frame-shape {:shape shape :childs childs}]])))) @@ -78,7 +79,8 @@ (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects)) frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] (when (and shape (not (:hidden shape))) - (let [shape (geom/transform-shape frame shape) + (let [shape (-> (gsh/transform-shape shape) + (gsh/translate-to-frame frame)) opts #js {:shape shape}] [:> shape-container {:shape shape} (case (:type shape) diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs index af90f2b936..597f89223a 100644 --- a/frontend/src/app/main/streams.cljs +++ b/frontend/src/app/main/streams.cljs @@ -41,6 +41,11 @@ (and (mouse-event? v) (= :click (:type v)))) +(defn mouse-double-click? + [v] + (and (mouse-event? v) + (= :double-click (:type v)))) + (defrecord PointerEvent [source pt ctrl shift alt]) (defn pointer-event? diff --git a/frontend/src/app/main/ui/handoff/render.cljs b/frontend/src/app/main/ui/handoff/render.cljs index 26e0c8d5d0..d1893c0aef 100644 --- a/frontend/src/app/main/ui/handoff/render.cljs +++ b/frontend/src/app/main/ui/handoff/render.cljs @@ -122,7 +122,8 @@ (mf/deps objects) #(group-container-factory objects))] (when (and shape (not (:hidden shape))) - (let [shape (geom/transform-shape frame shape) + (let [shape (-> (geom/transform-shape shape) + (geom/translate-to-frame frame)) opts #js {:shape shape :frame frame}] (case (:type shape) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 8cca3ce1e6..a17bd9f313 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -149,7 +149,8 @@ shape (unchecked-get props "shape") frame (unchecked-get props "frame")] (when (and shape (not (:hidden shape))) - (let [shape (geom/transform-shape frame shape) + (let [shape (-> (geom/transform-shape shape) + (geom/translate-to-frame frame)) opts #js {:shape shape}] (case (:type shape) :curve [:> path-wrapper opts] diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index f0b8bc2b55..cd9dd7521c 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -82,7 +82,8 @@ (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") ghost? (unchecked-get props "ghost?") - shape (geom/transform-shape frame shape) + shape (-> (geom/transform-shape shape) + (geom/translate-to-frame frame)) opts #js {:shape shape :frame frame} alt? (mf/use-state false) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/group.cljs index 43711deb7f..4dac6c4d80 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/group.cljs @@ -11,6 +11,7 @@ (ns app.main.ui.workspace.sidebar.options.group (:require [rumext.alpha :as mf] + [app.common.attrs :as attrs] [app.common.geom.shapes :as geom] [app.common.pages-helpers :as cph] [app.main.refs :as refs] @@ -43,7 +44,7 @@ (merge ;; All values extracted from the group shape, except ;; border radius, that needs to be looked up from children - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % measure-attrs nil @@ -51,7 +52,7 @@ nil) [shape]) measure-attrs) - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % [:rx :ry] nil @@ -64,10 +65,10 @@ (select-keys shape component-attrs) fill-values - (geom/get-attrs-multi shape-with-children fill-attrs) + (attrs/get-attrs-multi shape-with-children fill-attrs) stroke-values - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % stroke-attrs nil @@ -77,7 +78,7 @@ stroke-attrs) font-values - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % nil text-font-attrs @@ -87,7 +88,7 @@ text-font-attrs) align-values - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % nil text-align-attrs @@ -97,7 +98,7 @@ text-align-attrs) spacing-values - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % nil text-spacing-attrs @@ -107,7 +108,7 @@ text-spacing-attrs) valign-values - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % nil text-valign-attrs @@ -117,7 +118,7 @@ text-valign-attrs) decoration-values - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % nil text-decoration-attrs @@ -127,7 +128,7 @@ text-decoration-attrs) transform-values - (geom/get-attrs-multi (map #(get-shape-attrs + (attrs/get-attrs-multi (map #(get-shape-attrs % nil text-transform-attrs diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs index b13a69acbf..03f45783b7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs @@ -11,6 +11,7 @@ (:require [rumext.alpha :as mf] [app.common.geom.shapes :as geom] + [app.common.attrs :as attrs] [app.main.data.workspace.texts :as dwt] [app.main.ui.workspace.sidebar.options.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]] @@ -48,9 +49,9 @@ text-attrs convert-attrs extract-fn))] - (geom/get-attrs-multi (map mapfn shapes) (or attrs text-attrs)))) + (attrs/get-attrs-multi (map mapfn shapes) (or attrs text-attrs)))) - measure-values (geom/get-attrs-multi shapes measure-attrs) + measure-values (attrs/get-attrs-multi shapes measure-attrs) fill-values (extract {:attrs fill-attrs :text-attrs ot/text-fill-attrs