;; 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))))))))