;; 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.shapes (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.proportions :as gpp] [app.common.geom.shapes :as gsh] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.schema :as sm] [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.page :as ctp] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.comments :as dc] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.features :as features] [app.main.streams :as ms] [beicon.core :as rx] [potok.core :as ptk])) (defn get-shape-layer-position [objects selected attrs] ;; Calculate the frame over which we're drawing (let [position @ms/mouse-position frame-id (:frame-id attrs (ctst/top-nested-frame objects position)) shape (when-not (empty? selected) (cph/get-base-shape objects selected))] ;; When no shapes has been selected or we're over a different frame ;; we add it as the latest shape of that frame (if (or (not shape) (not= (:frame-id shape) frame-id)) [frame-id frame-id nil] ;; Otherwise, we add it to next to the selected shape (let [index (cph/get-position-on-parent objects (:id shape)) {:keys [frame-id parent-id]} shape] [frame-id parent-id (inc index)])))) (defn make-new-shape [attrs objects selected] (let [default-attrs (if (= :frame (:type attrs)) cts/default-frame-attrs cts/default-shape-attrs) selected-non-frames (into #{} (comp (map (d/getf objects)) (remove cph/frame-shape?)) selected) [frame-id parent-id index] (get-shape-layer-position objects selected-non-frames attrs)] (-> (merge default-attrs attrs) (gpp/setup-proportions) (assoc :frame-id frame-id :parent-id parent-id :index index)))) (defn prepare-add-shape [changes attrs objects selected] (let [id (or (:id attrs) (uuid/next)) name (:name attrs) shape (make-new-shape (assoc attrs :id id :name name) objects selected) index (:index (meta attrs)) [row column :as cell] (:cell (meta attrs)) changes (-> changes (pcb/with-objects objects) (cond-> (some? index) (pcb/add-object shape {:index index})) (cond-> (nil? index) (pcb/add-object shape)) (cond-> (some? (:parent-id attrs)) (pcb/change-parent (:parent-id attrs) [shape] index)) (cond-> (some? cell) (pcb/update-shapes [(:parent-id shape)] #(ctl/push-into-cell % [id] row column))) (cond-> (ctl/grid-layout? objects (:parent-id shape)) (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)))] [shape changes])) (defn add-shape ([attrs] (add-shape attrs {})) ([attrs {:keys [no-select? no-update-layout?]}] (dm/assert! (cts/shape-attrs? attrs)) (ptk/reify ::add-shape ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)) [shape changes] (prepare-add-shape changes attrs objects selected) undo-id (js/Symbol)] (rx/concat (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (when-not no-update-layout? (ptk/data-event :layout/update [(:parent-id shape)])) (when-not no-select? (dws/select-shapes (d/ordered-set (:id shape)))) (dwu/commit-undo-transaction undo-id)) (when (= :text (:type attrs)) (->> (rx/of (dwe/start-edition-mode (:id shape))) (rx/observe-on :async))))))))) (defn prepare-move-shapes-into-frame [changes frame-id shapes objects] (let [ordered-indexes (cph/order-by-indexed-shapes objects shapes) parent-id (get-in objects [frame-id :parent-id]) ordered-indexes (->> ordered-indexes (remove #(= % parent-id))) to-move-shapes (map (d/getf objects) ordered-indexes)] (when (d/not-empty? to-move-shapes) (-> changes (cond-> (not (ctl/any-layout? objects frame-id)) (pcb/update-shapes ordered-indexes ctl/remove-layout-item-data)) (pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true))) (pcb/change-parent frame-id to-move-shapes 0) (cond-> (ctl/grid-layout? objects frame-id) (pcb/update-shapes [frame-id] ctl/assign-cells)))))) (defn move-shapes-into-frame [frame-id shapes] (ptk/reify ::move-shapes-into-frame ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) shapes (->> shapes (remove #(dm/get-in objects [% :blocked]))) changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)) changes (prepare-move-shapes-into-frame changes frame-id shapes objects)] (if (some? changes) (rx/of (dch/commit-changes changes)) (rx/empty)))))) (declare real-delete-shapes) (declare update-shape-flags) (defn delete-shapes ([ids] (delete-shapes nil ids)) ([page-id ids] (dm/assert! (sm/set-of-uuid? ids)) (ptk/reify ::delete-shapes ptk/WatchEvent (watch [it state _] (let [file-id (:current-file-id state) page-id (or page-id (:current-page-id state)) file (wsh/get-file state file-id) page (wsh/lookup-page state page-id) objects (wsh/lookup-page-objects state page-id) components-v2 (features/active-feature? state :components-v2) ids (cph/clean-loops objects ids) in-component-copy? (fn [shape-id] ;; Look for shapes that are inside a component copy, but are ;; not the root. In this case, they must not be deleted, ;; but hidden (to be able to recover them more easily). (let [shape (get objects shape-id) component-shape (ctn/get-component-shape objects shape)] (and (ctk/in-component-copy? shape) (not= shape component-shape) (not (ctk/main-instance? component-shape))))) [ids-to-delete ids-to-hide] (if components-v2 (loop [ids-seq (seq ids) ids-to-delete [] ids-to-hide []] (let [id (first ids-seq)] (if (nil? id) [ids-to-delete ids-to-hide] (if (in-component-copy? id) (recur (rest ids-seq) ids-to-delete (conj ids-to-hide id)) (recur (rest ids-seq) (conj ids-to-delete id) ids-to-hide))))) [ids []]) undo-id (js/Symbol)] (rx/concat (rx/of (dwu/start-undo-transaction undo-id) (update-shape-flags ids-to-hide {:hidden true})) (real-delete-shapes file page objects ids-to-delete it components-v2) (rx/of (dwu/commit-undo-transaction undo-id)))))))) (defn- real-delete-shapes-changes ([file page objects ids it components-v2] (let [changes (-> (pcb/empty-changes it (:id page)) (pcb/with-page page) (pcb/with-objects objects) (pcb/with-library-data file))] (real-delete-shapes-changes changes file page objects ids it components-v2))) ([changes file page objects ids _it components-v2] (let [lookup (d/getf objects) groups-to-unmask (reduce (fn [group-ids id] ;; When the shape to delete is the mask of a masked group, ;; the mask condition must be removed, and it must be ;; converted to a normal group. (let [obj (lookup id) parent (lookup (:parent-id obj))] (if (and (:masked-group? parent) (= id (first (:shapes parent)))) (conj group-ids (:id parent)) group-ids))) #{} ids) interacting-shapes (filter (fn [shape] ;; If any of the deleted shapes is the destination of ;; some interaction, this must be deleted, too. (let [interactions (:interactions shape)] (some #(and (ctsi/has-destination %) (contains? ids (:destination %))) interactions))) (vals objects)) ;; If any of the deleted shapes is a frame with guides guides (into {} (comp (map second) (remove #(contains? ids (:frame-id %))) (map (juxt :id identity))) (dm/get-in page [:options :guides])) starting-flows (filter (fn [flow] ;; If any of the deleted is a frame that starts a flow, ;; this must be deleted, too. (contains? ids (:starting-frame flow))) (-> page :options :flows)) all-parents (reduce (fn [res id] ;; All parents of any deleted shape must be resized. (into res (cph/get-parent-ids objects id))) (d/ordered-set) ids) all-children (->> ids ;; Children of deleted shapes must be also deleted. (reduce (fn [res id] (into res (cph/get-children-ids objects id))) []) (reverse) (into (d/ordered-set))) find-all-empty-parents (fn recursive-find-empty-parents [empty-parents] (let [all-ids (into empty-parents ids) contains? (partial contains? all-ids) xform (comp (map lookup) (filter cph/group-shape?) (remove #(->> (:shapes %) (remove contains?) seq)) (map :id)) parents (into #{} xform all-parents)] (if (= empty-parents parents) empty-parents (recursive-find-empty-parents parents)))) empty-parents ;; Any parent whose children are all deleted, must be deleted too. (into (d/ordered-set) (find-all-empty-parents #{})) components-to-delete (if components-v2 (reduce (fn [components id] (let [shape (get objects id)] (if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file (:main-instance? shape)) ;; but check anyway (conj components (:component-id shape)) components))) [] (into ids all-children)) []) changes (-> changes (pcb/set-page-option :guides guides)) changes (reduce (fn [changes component-id] ;; It's important to delete the component before the main instance, because we ;; need to store the instance position if we want to restore it later. (pcb/delete-component changes component-id)) changes components-to-delete) changes (-> changes (pcb/remove-objects all-children {:ignore-touched true}) (pcb/remove-objects ids) (pcb/remove-objects empty-parents) (pcb/resize-parents all-parents) (pcb/update-shapes groups-to-unmask (fn [shape] (assoc shape :masked-group? false))) (pcb/update-shapes (map :id interacting-shapes) (fn [shape] (d/update-when shape :interactions (fn [interactions] (into [] (remove #(and (ctsi/has-destination %) (contains? ids (:destination %)))) interactions))))) (cond-> (seq starting-flows) (pcb/update-page-option :flows (fn [flows] (->> (map :id starting-flows) (reduce ctp/remove-flow flows))))))] [changes all-parents]))) (defn delete-shapes-changes [changes file page objects ids it components-v2] (let [[changes _all-parents] (real-delete-shapes-changes changes file page objects ids it components-v2)] changes)) (defn- real-delete-shapes [file page objects ids it components-v2] (let [[changes all-parents] (real-delete-shapes-changes file page objects ids it components-v2) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) (dc/detach-comment-thread ids) (dch/commit-changes changes) (dwu/commit-undo-transaction undo-id) (ptk/data-event :layout/update all-parents)))) (defn create-and-add-shape [type frame-x frame-y data] (ptk/reify ::create-and-add-shape ptk/WatchEvent (watch [_ state _] (let [{:keys [width height]} data vbc (wsh/viewport-center state) x (:x data (- (:x vbc) (/ width 2))) y (:y data (- (:y vbc) (/ height 2))) page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) frame-id (-> (wsh/lookup-page-objects state page-id) (ctst/top-nested-frame {:x frame-x :y frame-y})) selected (wsh/lookup-selected state) page-objects (wsh/lookup-page-objects state) base (cph/get-base-shape page-objects selected) selected-frame? (and (= 1 (count selected)) (= :frame (get-in objects [(first selected) :type]))) parent-id (if (or selected-frame? (empty? selected)) frame-id (:parent-id base)) shape (-> (cts/make-minimal-shape type) (merge data) (merge {:x x :y y}) (assoc :frame-id frame-id :parent-id parent-id) (cts/setup-rect-selrect))] (rx/of (add-shape shape)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Artboard ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn prepare-create-artboard-from-selection [changes id parent-id objects selected index frame-name without-fill?] (let [selected-objs (map #(get objects %) selected) new-index (or index (cph/get-index-replacement selected objects))] (when (d/not-empty? selected) (let [srect (gsh/selection-rect selected-objs) frame-id (get-in objects [(first selected) :frame-id]) parent-id (or parent-id (get-in objects [(first selected) :parent-id])) shape (-> (cts/make-minimal-shape :frame) (merge {:x (:x srect) :y (:y srect) :width (:width srect) :height (:height srect)}) (cond-> id (assoc :id id)) (cond-> frame-name (assoc :name frame-name)) (assoc :frame-id frame-id :parent-id parent-id) (with-meta {:index new-index}) (cond-> (or (not= frame-id uuid/zero) without-fill?) (assoc :fills [] :hide-in-viewer true)) (cts/setup-rect-selrect)) [shape changes] (prepare-add-shape changes shape objects selected) changes (prepare-move-shapes-into-frame changes (:id shape) selected objects)] [shape changes])))) (defn create-artboard-from-selection ([] (create-artboard-from-selection nil)) ([id] (create-artboard-from-selection id nil)) ([id parent-id] (create-artboard-from-selection id parent-id nil)) ([id parent-id index] (ptk/reify ::create-artboard-from-selection ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) selected (cph/clean-loops objects selected) changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)) [frame-shape changes] (prepare-create-artboard-from-selection changes id parent-id objects selected index nil false) undo-id (js/Symbol)] (when changes (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (dws/select-shapes (d/ordered-set (:id frame-shape))) (ptk/data-event :layout/update [(:id frame-shape)]) (dwu/commit-undo-transaction undo-id)))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shape Flags ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn update-shape-flags [ids {:keys [blocked hidden] :as flags}] (dm/assert! "expected valid coll of uuids" (every? uuid? ids)) (dm/assert! "expected valid shape-attrs value for `flags`" (cts/shape-attrs? flags)) (ptk/reify ::update-shape-flags ptk/WatchEvent (watch [_ state _] (let [update-fn (fn [obj] (cond-> obj (boolean? blocked) (assoc :blocked blocked) (boolean? hidden) (assoc :hidden hidden))) objects (wsh/lookup-page-objects state) ;; We have change only the hidden behaviour, to hide only the ;; selected shape, block behaviour remains the same. ids (if (boolean? blocked) (into ids (->> ids (mapcat #(cph/get-children-ids objects %)))) ids)] (rx/of (dch/update-shapes ids update-fn)))))) (defn toggle-visibility-selected [] (ptk/reify ::toggle-visibility-selected ptk/WatchEvent (watch [_ state _] (let [selected (wsh/lookup-selected state)] (rx/of (dch/update-shapes selected #(update % :hidden not))))))) (defn toggle-lock-selected [] (ptk/reify ::toggle-lock-selected ptk/WatchEvent (watch [_ state _] (let [selected (wsh/lookup-selected state)] (rx/of (dch/update-shapes selected #(update % :blocked not))))))) ;; FIXME: this need to be refactored (defn toggle-file-thumbnail-selected [] (ptk/reify ::toggle-file-thumbnail-selected ptk/WatchEvent (watch [_ state _] (let [selected (wsh/lookup-selected state) pages (-> state :workspace-data :pages-index vals)] (rx/concat ;; First: clear the `:use-for-thumbnail?` flag from all not ;; selected frames. (rx/from (->> pages (mapcat (fn [{:keys [objects id] :as page}] (->> (ctst/get-frames objects) (sequence (comp (filter :use-for-thumbnail?) (map :id) (remove selected) (map (partial vector id))))))) (d/group-by first second) (map (fn [[page-id frame-ids]] (dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail?) {:page-id page-id}))))) ;; And finally: toggle the flag value on all the selected shapes (rx/of (dch/update-shapes selected #(update % :use-for-thumbnail? not))))))))