penpot/frontend/src/app/main/data/workspace/modifiers.cljs

568 lines
22 KiB
Clojure

;; 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/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.workspace.modifiers
"Events related with shapes transformations"
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.modifiers :as gm]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.modifiers :as ctm]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.attrs :refer [editable-attrs]]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.constants :refer [zoom-half-pixel-precision]]
[app.main.data.workspace.comments :as-alias dwcm]
[app.main.data.workspace.guides :as-alias dwg]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
;; -- temporary modifiers -------------------------------------------
;; During an interactive transformation of shapes (e.g. when resizing or rotating
;; a group with the mouse), there are a lot of objects that need to be modified
;; (in this case, the group and all its children).
;;
;; To avoid updating the shapes theirselves, and forcing redraw of all components
;; that depend on the "objects" global state, we set a "modifiers" structure, with
;; the changes that need to be applied, and store it in :workspace-modifiers global
;; variable. The viewport reads this and merges it into the objects list it uses to
;; paint the viewport content, redrawing only the objects that have new modifiers.
;;
;; When the interaction is finished (e.g. user releases mouse button), the
;; apply-modifiers event is done, that consolidates all modifiers into the base
;; geometric attributes of the shapes.
(defn- check-delta
"If the shape is a component instance, check its relative position and rotation respect
the root of the component, and see if it changes after applying a transformation."
[shape root transformed-shape transformed-root]
(let [shape-delta
(when root
(gpt/point (- (gsh/left-bound shape) (gsh/left-bound root))
(- (gsh/top-bound shape) (gsh/top-bound root))))
transformed-shape-delta
(when transformed-root
(gpt/point (- (gsh/left-bound transformed-shape) (gsh/left-bound transformed-root))
(- (gsh/top-bound transformed-shape) (gsh/top-bound transformed-root))))
distance
(if (and shape-delta transformed-shape-delta)
(gpt/distance-vector shape-delta transformed-shape-delta)
(gpt/point 0 0))
rotation-delta
(if (and (some? (:rotation shape)) (some? (:rotation shape)))
(- (:rotation transformed-shape) (:rotation shape))
0)
selrect (:selrect shape)
transformed-selrect (:selrect transformed-shape)]
;; There are cases in that the coordinates change slightly (e.g. when rounding
;; to pixel, or when recalculating text positions in different zoom levels).
;; To take this into account, we ignore movements smaller than 1 pixel.
;;
;; When the change is a resize, also has a transformation that may have the
;; shape position unchanged. But in this case we do not want to ignore it.
(and (and (< (:x distance) 1) (< (:y distance) 1))
(mth/close? (:width selrect) (:width transformed-selrect))
(mth/close? (:height selrect) (:height transformed-selrect))
(mth/close? rotation-delta 0))))
(defn calculate-ignore-tree
"Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"
[modif-tree objects]
(letfn [(get-ignore-tree
([ignore-tree shape]
(let [shape-id (dm/get-prop shape :id)
transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers]))
root
(if (:component-root shape)
shape
(ctn/get-component-shape objects shape {:allow-main? true}))
transformed-root
(if (:component-root shape)
transformed-shape
(gsh/transform-shape root (dm/get-in modif-tree [(:id root) :modifiers])))]
(get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))
([ignore-tree shape root transformed-root]
(let [shape-id (dm/get-prop shape :id)
transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers]))]
(get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))
([ignore-tree shape transformed-shape root transformed-root]
(let [shape-id (dm/get-prop shape :id)
ignore-tree
(cond-> ignore-tree
(and (some? root) (ctk/in-component-copy? shape))
(assoc
shape-id
(check-delta shape root transformed-shape transformed-root)))
set-child
(fn [ignore-tree child]
(get-ignore-tree ignore-tree child root transformed-root))]
(->> (:shapes shape)
(map (d/getf objects))
(reduce set-child ignore-tree)))))]
;; we check twice because we want only to search parents of components but once the
;; tree is traversed we only want to process the objects in components
(->> (keys modif-tree)
(map #(get objects %))
(reduce get-ignore-tree nil))))
(defn assoc-position-data
[shape position-data old-shape]
(let [deltav (gpt/to-vec (gpt/point (:selrect old-shape))
(gpt/point (:selrect shape)))
position-data
(-> position-data
(gsh/move-position-data deltav))]
(cond-> shape
(d/not-empty? position-data)
(assoc :position-data position-data))))
(defn update-grow-type
[shape old-shape]
(let [auto-width? (= :auto-width (:grow-type shape))
auto-height? (= :auto-height (:grow-type shape))
changed-width? (> (mth/abs (- (:width shape) (:width old-shape))) 0.1)
changed-height? (> (mth/abs (- (:height shape) (:height old-shape))) 0.1)
change-to-fixed? (or (and auto-width? (or changed-height? changed-width?))
(and auto-height? changed-height?))]
(cond-> shape
change-to-fixed?
(assoc :grow-type :fixed))))
(defn- clear-local-transform []
(ptk/reify ::clear-local-transform
ptk/UpdateEvent
(update [_ state]
(-> state
(dissoc :workspace-modifiers)
(dissoc :app.main.data.workspace.transforms/current-move-selected)))))
(defn create-modif-tree
[ids modifiers]
(dm/assert!
"expected valid coll of uuids"
(every? uuid? ids))
(into {} (map #(vector % {:modifiers modifiers})) ids))
(defn build-modif-tree
[ids objects get-modifier]
(dm/assert!
"expected valid coll of uuids"
(every? uuid? ids))
(into {} (map #(vector % {:modifiers (get-modifier (get objects %))})) ids))
(defn modifier-remove-from-parent
[modif-tree objects shapes]
(->> shapes
(reduce
(fn [modif-tree child-id]
(let [parent-id (get-in objects [child-id :parent-id])]
(update-in modif-tree [parent-id :modifiers] ctm/remove-children [child-id])))
modif-tree)))
(defn add-grid-children-modifiers
[modifiers frame-id shapes objects [row column :as cell]]
(let [frame (get objects frame-id)
ids (set shapes)
;; Temporary remove the children when moving them
frame (-> frame
(update :shapes #(d/removev ids %))
(ctl/assign-cells objects))
ids (->> ids
(remove #(ctl/position-absolute? objects %))
(ctst/sort-z-index objects)
reverse)
frame (-> frame
(update :shapes d/concat-vec ids)
(cond-> (some? cell)
(ctl/push-into-cell ids row column))
(ctl/assign-cells objects))]
(-> modifiers
(ctm/change-property :layout-grid-rows (:layout-grid-rows frame))
(ctm/change-property :layout-grid-columns (:layout-grid-columns frame))
(ctm/change-property :layout-grid-cells (:layout-grid-cells frame)))))
(defn build-change-frame-modifiers
[modif-tree objects selected target-frame-id drop-index cell-data]
(let [origin-frame-ids (->> selected (group-by #(get-in objects [% :frame-id])))
child-set (set (get-in objects [target-frame-id :shapes]))
target-frame (get objects target-frame-id)
target-flex-layout? (ctl/flex-layout? target-frame)
target-grid-layout? (ctl/grid-layout? target-frame)
children-ids (concat (:shapes target-frame) selected)
set-parent-ids
(fn [modif-tree shapes target-frame-id]
(reduce
(fn [modif-tree id]
(update-in
modif-tree
[id :modifiers]
#(-> %
(ctm/change-property :frame-id target-frame-id)
(ctm/change-property :parent-id target-frame-id))))
modif-tree
shapes))
update-frame-modifiers
(fn [modif-tree [original-frame shapes]]
(let [shapes (->> shapes (d/removev #(= target-frame-id %)))
shapes (cond->> shapes
(and (or target-grid-layout? target-flex-layout?)
(= original-frame target-frame-id))
;; When movining inside a layout frame remove the shapes that are not immediate children
(filterv #(contains? child-set %)))
children-ids (->> (dm/get-in objects [original-frame :shapes])
(remove (set selected)))
h-sizing? (and (ctl/flex-layout? objects original-frame)
(ctl/change-h-sizing? original-frame objects children-ids))
v-sizing? (and (ctl/flex-layout? objects original-frame)
(ctl/change-v-sizing? original-frame objects children-ids))]
(cond-> modif-tree
(not= original-frame target-frame-id)
(-> (modifier-remove-from-parent objects shapes)
(update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index)
(set-parent-ids shapes target-frame-id)
(cond-> h-sizing?
(update-in [original-frame :modifiers] ctm/change-property :layout-item-h-sizing :fix))
(cond-> v-sizing?
(update-in [original-frame :modifiers] ctm/change-property :layout-item-v-sizing :fix)))
(and target-flex-layout? (= original-frame target-frame-id))
(update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index)
;; Add the object to the cell
target-grid-layout?
(update-in [target-frame-id :modifiers] add-grid-children-modifiers target-frame-id shapes objects cell-data))))]
(as-> modif-tree $
(reduce update-frame-modifiers $ origin-frame-ids)
(cond-> $
;; Set fix position to target frame (horizontal)
(and (ctl/flex-layout? objects target-frame-id)
(ctl/change-h-sizing? target-frame-id objects children-ids))
(update-in [target-frame-id :modifiers] ctm/change-property :layout-item-h-sizing :fix)
;; Set fix position to target frame (vertical)
(and (ctl/flex-layout? objects target-frame-id)
(ctl/change-v-sizing? target-frame-id objects children-ids))
(update-in [target-frame-id :modifiers] ctm/change-property :layout-item-v-sizing :fix)))))
(defn modif->js
[modif-tree objects]
(clj->js (into {}
(map (fn [[k v]]
[(get-in objects [k :name]) v]))
modif-tree)))
(defn apply-text-modifier
[shape {:keys [width height]}]
(cond-> shape
(some? width)
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :width width {:ignore-lock? true}))
(some? height)
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :height height {:ignore-lock? true}))))
(defn apply-text-modifiers
[objects text-modifiers]
(loop [modifiers (seq text-modifiers)
result objects]
(if (empty? modifiers)
result
(let [[id text-modifier] (first modifiers)]
(recur (rest modifiers)
(d/update-when result id apply-text-modifier text-modifier))))))
#_(defn apply-path-modifiers
[objects path-modifiers]
(letfn [(apply-path-modifier
[shape {:keys [content-modifiers]}]
(let [shape (update shape :content upc/apply-content-modifiers content-modifiers)
[points selrect] (helpers/content->points+selrect shape (:content shape))]
(assoc shape :selrect selrect :points points)))]
(loop [modifiers (seq path-modifiers)
result objects]
(if (empty? modifiers)
result
(let [[id path-modifier] (first modifiers)]
(recur (rest modifiers)
(update objects id apply-path-modifier path-modifier)))))))
(defn- calculate-modifiers
([state modif-tree]
(calculate-modifiers state false false modif-tree))
([state ignore-constraints ignore-snap-pixel modif-tree]
(calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree nil))
([state ignore-constraints ignore-snap-pixel modif-tree params]
(let [objects
(wsh/lookup-page-objects state)
snap-pixel?
(and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
zoom (dm/get-in state [:workspace-local :zoom])
snap-precision (if (>= zoom zoom-half-pixel-precision) 0.5 1)]
(as-> objects $
(apply-text-modifiers $ (get state :workspace-text-modifier))
;;(apply-path-modifiers $ (get-in state [:workspace-local :edit-path]))
(gm/set-objects-modifiers modif-tree $ (merge
params
{:ignore-constraints ignore-constraints
:snap-pixel? snap-pixel?
:snap-precision snap-precision}))))))
(defn- calculate-update-modifiers
[old-modif-tree state ignore-constraints ignore-snap-pixel modif-tree]
(let [objects
(wsh/lookup-page-objects state)
snap-pixel?
(and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
zoom (dm/get-in state [:workspace-local :zoom])
snap-precision (if (>= zoom zoom-half-pixel-precision) 0.5 1)
objects
(-> objects
(apply-text-modifiers (get state :workspace-text-modifier)))]
(gm/set-objects-modifiers
old-modif-tree
modif-tree
objects
{:ignore-constraints ignore-constraints
:snap-pixel? snap-pixel?
:snap-precision snap-precision})))
(defn update-modifiers
([modif-tree]
(update-modifiers modif-tree false))
([modif-tree ignore-constraints]
(update-modifiers modif-tree ignore-constraints false))
([modif-tree ignore-constraints ignore-snap-pixel]
(ptk/reify ::update-modifiers
ptk/UpdateEvent
(update [_ state]
(update state :workspace-modifiers calculate-update-modifiers state ignore-constraints ignore-snap-pixel modif-tree)))))
(defn set-modifiers
([modif-tree]
(set-modifiers modif-tree false))
([modif-tree ignore-constraints]
(set-modifiers modif-tree ignore-constraints false))
([modif-tree ignore-constraints ignore-snap-pixel]
(set-modifiers modif-tree ignore-constraints ignore-snap-pixel nil))
([modif-tree ignore-constraints ignore-snap-pixel params]
(ptk/reify ::set-modifiers
ptk/UpdateEvent
(update [_ state]
(assoc state :workspace-modifiers (calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree params))))))
(def ^:private
xf-rotation-shape
(comp
(remove #(get % :blocked false))
(filter #(:rotation (get editable-attrs (:type %))))
(map :id)))
;; Rotation use different algorithm to calculate children
;; modifiers (and do not use child constraints).
(defn set-rotation-modifiers
([angle shapes]
(set-rotation-modifiers angle shapes (-> shapes gsh/shapes->rect grc/rect->center)))
([angle shapes center]
(ptk/reify ::set-rotation-modifiers
ptk/UpdateEvent
(update [_ state]
(let [objects (wsh/lookup-page-objects state)
ids (sequence xf-rotation-shape shapes)
get-modifier
(fn [shape]
(ctm/rotation-modifiers shape center angle))
modif-tree
(-> (build-modif-tree ids objects get-modifier)
(gm/set-objects-modifiers objects))]
(assoc state :workspace-modifiers modif-tree))))))
;; This function is similar to set-rotation-modifiers but:
;; - It consideres the center for everyshape instead of the center of the total selrect
;; - The angle param is the desired final value, not a delta
(defn set-delta-rotation-modifiers
[angle shapes {:keys [center delta?] :or {center nil delta? false}}]
(ptk/reify ::set-delta-rotation-modifiers
ptk/UpdateEvent
(update [_ state]
(let [objects (wsh/lookup-page-objects state)
ids
(->> shapes
(remove #(get % :blocked false))
(filter #(contains? (get editable-attrs (:type %)) :rotation))
(map :id))
get-modifier
(fn [shape]
(let [delta (if delta? angle (- angle (:rotation shape)))
center (or center (gsh/shape->center shape))]
(ctm/rotation-modifiers shape center delta)))
modif-tree
(-> (build-modif-tree ids objects get-modifier)
(gm/set-objects-modifiers objects))]
(assoc state :workspace-modifiers modif-tree)))))
(defn apply-modifiers
([]
(apply-modifiers nil))
([{:keys [modifiers undo-transation? stack-undo? ignore-constraints ignore-snap-pixel undo-group]
:or {undo-transation? true stack-undo? false ignore-constraints false ignore-snap-pixel false}}]
(ptk/reify ::apply-modifiers
ptk/WatchEvent
(watch [_ state _]
(let [text-modifiers (get state :workspace-text-modifier)
objects (wsh/lookup-page-objects state)
object-modifiers
(if (some? modifiers)
(calculate-modifiers state ignore-constraints ignore-snap-pixel modifiers)
(get state :workspace-modifiers))
ids
(into []
(remove #(= % uuid/zero))
(keys object-modifiers))
ids-with-children
(into ids
(mapcat (partial cfh/get-children-ids objects))
ids)
ignore-tree
(calculate-ignore-tree object-modifiers objects)
undo-id (js/Symbol)]
(rx/concat
(if undo-transation?
(rx/of (dwu/start-undo-transaction undo-id))
(rx/empty))
(rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers})
(ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
(dwsh/update-shapes
ids
(fn [shape]
(let [modif (get-in object-modifiers [(:id shape) :modifiers])
text-shape? (cfh/text-shape? shape)
position-data (when text-shape?
(dm/get-in text-modifiers [(:id shape) :position-data]))]
(-> shape
(gsh/transform-shape modif)
(cond-> (d/not-empty? position-data)
(assoc-position-data position-data shape))
(cond-> text-shape?
(update-grow-type shape)))))
{:reg-objects? true
:stack-undo? stack-undo?
:ignore-tree ignore-tree
:undo-group undo-group
;; Attributes that can change in the transform. This way we don't have to check
;; all the attributes
:attrs [:selrect
:points
:x
:y
:rx
:ry
:r1
:r2
:r3
:r4
:shadow
:blur
:strokes
:width
:height
:content
:transform
:transform-inverse
:rotation
:flip-x
:flip-y
:grow-type
:position-data
:layout-gap
:layout-padding
:layout-item-h-sizing
:layout-item-margin
:layout-item-max-h
:layout-item-max-w
:layout-item-min-h
:layout-item-min-w
:layout-item-v-sizing
:layout-padding-type
:layout-gap
:layout-item-margin
:layout-item-margin-type
:layout-grid-cells
:layout-grid-columns
:layout-grid-rows]})
;; We've applied the text-modifier so we can dissoc the temporary data
(fn [state]
(update state :workspace-text-modifier #(apply dissoc % ids))))
(if (nil? modifiers)
(rx/of (clear-local-transform))
(rx/empty))
(if undo-transation?
(rx/of (dwu/commit-undo-transaction undo-id))
(rx/empty))))))))