diff --git a/resources/assets/css/components/canvas.less b/resources/assets/css/components/canvas.less index daeb4f10..7327aceb 100644 --- a/resources/assets/css/components/canvas.less +++ b/resources/assets/css/components/canvas.less @@ -158,7 +158,7 @@ stroke: @color_select; stroke-width: 2; &.selection { - stroke: @white; + stroke: @gray_lighter; stroke-width: 1; } } @@ -400,3 +400,32 @@ #selected-arrow-point { fill: @color_select; } +.clips { + opacity: @opacity_darker_gray; + .shape-layer { + stroke: @white; + stroke-dasharray: 2, 3; + } + .text-layer { + fill: @white; + } + .active { + transition: all 400ms ease-in-out; + fill: @color_select; + .shape-layer { + stroke: @color_select; + } + .text-layer { + fill: @color_select; + } + } + .inactive { + transition: all 400ms ease-in-out; + } +} + +.clip-in-progress { + .layer-handle { + pointer-events: none; + } +} diff --git a/resources/assets/css/components/menu.less b/resources/assets/css/components/menu.less index 78239049..7893ddcd 100644 --- a/resources/assets/css/components/menu.less +++ b/resources/assets/css/components/menu.less @@ -733,10 +733,10 @@ .slack-channel-remove { margin-left: auto; } -.clips { +.clips-list { .flexy(wrap); } -.clip { +.clip-item { .flexy(center; center); position: relative; width: 50%; diff --git a/src-cljs/frontend/clipboard.cljs b/src-cljs/frontend/clipboard.cljs index 6a7d8bb8..4c0039d6 100644 --- a/src-cljs/frontend/clipboard.cljs +++ b/src-cljs/frontend/clipboard.cljs @@ -282,7 +282,5 @@ ;; element is selected (see components.canvas) (or (= "_copy-hack" (.-id target)) (not (contains? #{"input" "textarea"} (str/lower-case (.-tagName js/document.activeElement)))))) - (when-let [layer-data (some->> (.getData (.-clipboardData e) "text") - (parse-pasted))] - (let [canvas-size (utils/canvas-size)] - (put! (get-in app-state [:comms :controls]) [:layers-pasted (assoc layer-data :canvas-size canvas-size)]))))) + (put! (get-in app-state [:comms :controls]) [:layers-pasted (some->> (.getData (.-clipboardData e) "text") + (parse-pasted))]))) diff --git a/src-cljs/frontend/components/app.cljs b/src-cljs/frontend/components/app.cljs index 1341ec2c..1d962924 100644 --- a/src-cljs/frontend/components/app.cljs +++ b/src-cljs/frontend/components/app.cljs @@ -85,6 +85,8 @@ state/right-click-learned-path [:drawing :in-progress?] [:drawing :relation-in-progress?] + [:drawing :moving?] + [:drawing :clip?] [:mouse-down] [:layer-properties-menu] [:radial] @@ -92,7 +94,8 @@ [:cust-data] [:document/id] [:keyboard] - [:keyboard-shortcuts]]) + [:keyboard-shortcuts] + [:cust :cust/clips]]) {:react-key "canvas"}) (om/build chat/chat (select-in app [state/chat-opened-path diff --git a/src-cljs/frontend/components/canvas.cljs b/src-cljs/frontend/components/canvas.cljs index cb4ea29b..d3e011ab 100644 --- a/src-cljs/frontend/components/canvas.cljs +++ b/src-cljs/frontend/components/canvas.cljs @@ -477,6 +477,7 @@ (common/svg-icon icon {:svg-props {:height 16 :width 16 :className "mouse-tool" + ;; TODO: should subscriber mouse position be a map? :x (- (first (:mouse-position subscriber)) (- 16)) :y (- (last (:mouse-position subscriber)) 8) :key (:client-id subscriber)} @@ -501,6 +502,7 @@ (common/svg-icon (subscriber-cursor-icon (:tool subscriber)) {:svg-props {:height 16 :width 16 :className "mouse-tool" + ;; TODO: should subscriber mouse position be a map? :x (- (first (:mouse-position subscriber)) 8) :y (- (last (:mouse-position subscriber)) 8) :key (:client-id subscriber)} @@ -710,6 +712,58 @@ (.focus (om/get-node owner "target-input")))} target)))))))))) +(defn pasted [{:keys [clips]} owner] + (reify + om/IDisplayName (display-name [_] "Pasted Layers") + om/IRender + (render [_] + (let [drawing (cursors/observe-drawing owner) + camera (cursors/observe-camera owner) + normalized-layer-datas (map layers/normalize-pasted-layer-data (concat (when (:layers drawing) + [drawing]) + (map :layer-data (filter :clip/important? clips)))) + scrolled-layer-index (:scrolled-layer drawing) + clip-scroll (layers/clip-scroll normalized-layer-datas scrolled-layer-index) + [mouse-x mouse-y] (cameras/snap-to-grid camera + (:rx (:current-mouse-position drawing)) + (:ry (:current-mouse-position drawing)))] + (apply dom/g #js {:className "layers clips" + :transform (str "translate(" mouse-x "," mouse-y ")")} + (map-indexed (fn [i layer-data] + (let [active? (= i scrolled-layer-index) + scale (if active? + 1 + (layers/pasted-inactive-scale layer-data)) + translate-x (- (layers/clip-offset normalized-layer-datas scrolled-layer-index i) + (* scale (:min-x layer-data))) + translate-y (* scale + (- (+ (/ (:height layer-data) 2) + (:min-y layer-data)))) + [translate-x translate-y] (cameras/snap-to-grid camera translate-x translate-y) + ] + (apply dom/g #js {:className (if active? + "active" + "inactive") + :transform (str "translate(" + translate-x + "," + translate-y + ") " + "scale(" scale ")") + :key i} + (map (fn [layer] + (let [layer (if (:force-even? layer) + (layers/force-even layer) + layer) + layer (merge layer + {:layer/current-x (:layer/end-x layer) + :layer/current-y (:layer/end-y layer) + :className "layer-in-progress"})] + (svg-element (assoc layer :key (str (:db/id layer) "-clip") + :vectorEffect "non-scaling-stroke")))) + (:layers layer-data))))) + normalized-layer-datas)))))) + (defn in-progress [{:keys [mouse-down]} owner] (reify om/IDisplayName (display-name [_] "In Progress Layers") @@ -992,6 +1046,7 @@ camera (cursors/observe-camera owner) in-progress? (settings/drawing-in-progress? app) relation-in-progress? (get-in app [:drawing :relation-in-progress?]) + clip? (get-in app [:drawing :clip?]) tool (get-in app state/current-tool-path) mouse-down? (get-in app [:mouse-down]) right-click-learned? (get-in app state/right-click-learned-path)] @@ -1016,7 +1071,9 @@ " relation-in-progress ") (when-not right-click-learned? - "radial-not-learned")) + "radial-not-learned ") + + (when clip? "clip-in-progress ")) :onTouchStart (fn [event] (let [touches (.-touches event)] (cond @@ -1092,18 +1149,21 @@ (.stopPropagation event)) :onWheel (fn [event] (let [dx (- (aget event "deltaX")) - dy (aget event "deltaY")] - (om/transact! camera (fn [c] - (if (or (aget event "altKey") - ;; http://stackoverflow.com/questions/15416851/catching-mac-trackpad-zoom - ;; ctrl means pinch-to-zoom - (aget event "ctrlKey")) - (cameras/set-zoom c (cameras/screen-event-coords event) - (partial + (* -0.002 - dy - ;; pinch-to-zoom needs a boost to feel natural - (if (.-ctrlKey event) 10 1)))) - (cameras/move-camera c dx (- dy)))))) + dy (aget event "deltaY") + zoom? (or (aget event "altKey") + ;; http://stackoverflow.com/questions/15416851/catching-mac-trackpad-zoom + ;; ctrl means pinch-to-zoom + (aget event "ctrlKey"))] + (if (and clip? (not zoom?)) + (cast! :clip-scrolled {:dx dx :dy dy}) + (om/transact! camera (fn [c] + (if zoom? + (cameras/set-zoom c (cameras/screen-event-coords event) + (partial + (* -0.002 + dy + ;; pinch-to-zoom needs a boost to feel natural + (if (.-ctrlKey event) 10 1)))) + (cameras/move-camera c dx (- dy))))))) (utils/stop-event event))} (defs camera) @@ -1132,8 +1192,15 @@ {:react-key "subscribers-layers"}) (om/build arrows app {:react-key "arrows"}) - - (om/build in-progress (select-keys app [:mouse-down]) {:react-key "in-progress"}))))))) + (cond + (or (get-in app [:drawing :in-progress?]) + (get-in app [:drawing :moving?])) + (om/build in-progress (select-keys app [:mouse-down]) {:react-key "in-progress"}) + + (get-in app [:drawing :clip?]) + (om/build pasted + {:clips (get-in app [:cust :cust/clips])} + {:react-key "pasted"})))))))) (defn needs-copy-paste-hack? [] (not (ua-browser/isChrome))) diff --git a/src-cljs/frontend/components/clip_viewer.cljs b/src-cljs/frontend/components/clip_viewer.cljs index 462594bc..314f63d9 100644 --- a/src-cljs/frontend/components/clip_viewer.cljs +++ b/src-cljs/frontend/components/clip_viewer.cljs @@ -52,10 +52,10 @@ "Cmd+C." "Ctrl+C.") " Star clips to pin them to the top. "] - [:div.clips + [:div.clips-list (for [clip clips] (html - [:div.clip.make + [:div.clip-item.make [:a.clip-preview {:role "button" :on-click #(cast! :clip-pasted clip)} [:img.clip-thumbnail {:src (:clip/s3-url clip)}]] diff --git a/src-cljs/frontend/components/drawing.cljs b/src-cljs/frontend/components/drawing.cljs index 8a0d5c69..05bbd147 100644 --- a/src-cljs/frontend/components/drawing.cljs +++ b/src-cljs/frontend/components/drawing.cljs @@ -27,8 +27,8 @@ ((om/get-shared owner :cast!) :subscriber-updated {:client-id (:client-id (:bot tick-state)) :fields (merge (:bot tick-state) {:mouse-position nil - :tool nil - :show-mouse? false})}))) + :tool nil + :show-mouse? false})}))) (annotate-keyframes tick))) (defn move-mouse [tick-state {:keys [start-tick end-tick start-x end-x start-y end-y tool] @@ -46,8 +46,8 @@ ((om/get-shared owner :cast!) :subscriber-updated {:client-id (:client-id (:bot tick-state)) :fields (merge (:bot tick-state) {:mouse-position [ex ey] - :show-mouse? true - :tool tool})}))))) + :show-mouse? true + :tool tool})}))))) tick-state (range 0 (inc (- end-tick start-tick)))) (annotate-keyframes end-tick))) @@ -92,11 +92,11 @@ ((om/get-shared owner :cast!) :subscriber-updated {:client-id (:client-id (:bot tick-state)) :fields (merge (:bot tick-state) {:mouse-position [ex ey] - :show-mouse? true - :layers [(assoc base-layer - :layer/current-x ex - :layer/current-y ey)] - :tool tool})}))))) + :show-mouse? true + :layers [(assoc base-layer + :layer/current-x ex + :layer/current-y ey)] + :tool tool})}))))) tick-state (range 0 (inc (- end-tick start-tick pause-ticks)))) (add-tick end-tick @@ -150,11 +150,11 @@ ((om/get-shared owner :cast!) :subscriber-updated {:client-id (:client-id (:bot tick-state)) :fields (merge (:bot tick-state) {:mouse-position [(+ start-x move-x) - (+ start-y move-y)] - :show-mouse? true - :layers (map (fn [l] (move-layer l move-x move-y)) - base-layers) - :tool :select})}))))) + (+ start-y move-y)] + :show-mouse? true + :layers (map (fn [l] (move-layer l move-x move-y)) + base-layers) + :tool :select})}))))) tick-state (range 0 (inc (- end-tick start-tick pause-ticks)))) (add-tick end-tick @@ -199,12 +199,12 @@ ((om/get-shared owner :cast!) :subscriber-updated {:client-id (:client-id (:bot tick-state)) :fields (merge (:bot tick-state) {:mouse-position [start-x (- start-y (/ text-height 2))] - :show-mouse? true - :layers [(assoc base-layer - :layer/text (apply str (take letter-count text)) - :layer/current-x end-x - :layer/current-y end-y)] - :tool :text})}))))) + :show-mouse? true + :layers [(assoc base-layer + :layer/text (apply str (take letter-count text)) + :layer/current-x end-x + :layer/current-y end-y)] + :tool :text})}))))) tick-state (map int (range 0 @@ -216,8 +216,8 @@ ((om/get-shared owner :cast!) :subscriber-updated {:client-id (:client-id (:bot tick-state)) :fields (merge (:bot tick-state) {:mouse-position nil - :layers nil - :tool :text})}) + :layers nil + :tool :text})}) (d/transact! (om/get-shared owner :db) [base-layer] {:bot-layer true}))) (annotate-keyframes start-tick end-tick)))) diff --git a/src-cljs/frontend/controllers/api.cljs b/src-cljs/frontend/controllers/api.cljs index 7151562d..04573ebf 100644 --- a/src-cljs/frontend/controllers/api.cljs +++ b/src-cljs/frontend/controllers/api.cljs @@ -86,9 +86,32 @@ (defmethod api-event [:cust-clips :success] [target message status {:keys [clips]} state] - (let [sorted-clips (sort clip-compare clips)] + (let [existing-clips (get-in state [:cust :cust/clips]) + all-clips (-> (reduce (fn [acc clip] + (if-let [existing (get-in acc [:existing-uuid->clip (:clip/uuid clip)])] + (-> acc + (update-in [:existing-uuid->clip] dissoc (:clip/uuid clip)) + (update-in [:clips] conj (merge existing clip))) + (update-in acc [:clips] conj clip))) + {:existing-uuid->clip (reduce (fn [acc clip] + (assoc acc (:clip/uuid clip) clip)) + {} existing-clips) + :clips []} + clips) + (#(concat (:clips %) + (vals (:existing-uuid->clip %))))) + sorted-clips (sort clip-compare all-clips)] (assoc-in state [:cust :cust/clips] sorted-clips))) +(defmethod api-event [:clip-layers :success] + [target message status {:keys [layer-data clip/uuid]} state] + (update-in state [:cust :cust/clips] (fn [clips] + (map (fn [clip] + (if (= uuid (:clip/uuid clip)) + (assoc clip :layer-data layer-data) + clip)) + clips)))) + (defmethod api-event [:team-docs :success] [target message status {:keys [docs]} state] (assoc-in state [:team :recent-docs] docs)) diff --git a/src-cljs/frontend/controllers/controls.cljs b/src-cljs/frontend/controllers/controls.cljs index 4452629c..10dfef8a 100644 --- a/src-cljs/frontend/controllers/controls.cljs +++ b/src-cljs/frontend/controllers/controls.cljs @@ -257,24 +257,62 @@ (cond-> (= :layer.type/path (:layer/type layer)) (assoc :layer/path (svg/points->path (nudge-points (parse-points-from-path (:layer/path layer)) x y)))))) +(defn scroll-clips [state up?] + (let [layer-data-count (+ (if (seq (get-in state [:drawing :layers])) + 1 + 0) + (count (filter :clip/important? (get-in state [:cust :cust/clips])))) + scrolled-layer (if up? + (if (= layer-data-count (inc (get-in state [:drawing :scrolled-layer]))) + (get-in state [:drawing :scrolled-layer]) + (inc (get-in state [:drawing :scrolled-layer]))) + (if (= 0 (get-in state [:drawing :scrolled-layer])) + 0 + (dec (get-in state [:drawing :scrolled-layer]))))] + (-> state + (assoc-in [:drawing :scroll-offset] 0) + (assoc-in [:drawing :scrolled-layer] scrolled-layer)))) + +(defn maybe-scroll-clips [state up?] + (if (get-in state [:drawing :clip?]) + (scroll-clips state up?) + state)) + +(defmethod handle-keyboard-shortcut :nudge-shapes-left + [state shortcut-name key-set] + (maybe-scroll-clips state false)) + +(defmethod handle-keyboard-shortcut :nudge-shapes-right + [state shortcut-name key-set] + (maybe-scroll-clips state true)) + +(defmethod handle-keyboard-shortcut :nudge-shapes-up + [state shortcut-name key-set] + (maybe-scroll-clips state false)) + +(defmethod handle-keyboard-shortcut :nudge-shapes-down + [state shortcut-name key-set] + (maybe-scroll-clips state true)) + (defn nudge-shapes [state key-set direction] - (let [db (:db state) - layers (map (partial d/entity @db) (get-in state [:selected-eids :selected-eids])) - increment (cameras/grid-size->snap-increment (cameras/grid-width (:camera state))) - shift? (contains? key-set "shift") - x (* (if shift? 10 1) - (case direction - :left (- increment) - :right increment - 0)) - y (* (if shift? 10 1) - (case direction - :up (- increment) - :down increment - 0))] - (when (seq layers) - (d/transact! db (mapv #(nudge-layer % {:x x :y y}) layers) - {:can-undo? true})))) + (when-not (get-in state [:drawing :clip?]) + (let [db (:db state) + layers (map (partial d/entity @db) (get-in state [:selected-eids :selected-eids])) + increment (cameras/grid-size->snap-increment (cameras/grid-width (:camera state))) + shift? (contains? key-set "shift") + x (* (if shift? 10 1) + (case direction + :left (- increment) + :right increment + 0)) + y (* (if shift? 10 1) + (case direction + :up (- increment) + :down increment + 0))] + (when (seq layers) + (d/transact! db (mapv #(nudge-layer % {:x x :y y}) layers) + {:can-undo? true}))))) (defmethod handle-keyboard-shortcut-after :nudge-shapes-left [state shortcut-name key-set] @@ -371,6 +409,26 @@ key-set))) (maybe-notify-subscribers! previous-state current-state nil nil)) +(defmethod control-event :clip-scrolled + [browser-state message {:keys [dx dy]} state] + (let [layer-data-count (+ (if (seq (get-in state [:drawing :layers])) + 1 + 0) + (count (filter :clip/important? (get-in state [:cust :cust/clips])))) + up? (pos? (+ dx (- dy))) + scrolled-layer (if up? + (if (= layer-data-count (inc (get-in state [:drawing :scrolled-layer]))) + (get-in state [:drawing :scrolled-layer]) + (inc (get-in state [:drawing :scrolled-layer]))) + (if (= 0 (get-in state [:drawing :scrolled-layer])) + 0 + (dec (get-in state [:drawing :scrolled-layer])))) + scroll-offset (+ (get-in state [:drawing :scroll-offset]) + (+ dx (- dy)))] + (if (> 15 (Math/abs scroll-offset)) + (update-in state [:drawing :scroll-offset] (fnil + 0) dx (- dy)) + (scroll-clips state up?)))) + (defn update-mouse [state x y] (if (and x y) (let [[rx ry] (cameras/screen->point (:camera state) x y)] @@ -469,7 +527,7 @@ :layer/ui-target (when (:layer/ui-target layer) (:layer/ui-target layer)))]) (assoc-in [:drawing :moving?] true) - (assoc-in [:drawing :starting-mouse-position] [rx ry])))) + (assoc-in [:drawing :starting-mouse-position] (:mouse state))))) (defmethod control-event :group-duplicated [browser-state message {:keys [x y]} state] @@ -504,7 +562,7 @@ ps)))))) layers (range))) (assoc-in [:drawing :moving?] true) - (assoc-in [:drawing :starting-mouse-position] [rx ry])))) + (assoc-in [:drawing :starting-mouse-position] (:mouse state))))) (defmethod control-event :text-layer-edited [browser-state message {:keys [value]} state] @@ -762,7 +820,8 @@ (update-in [:drawing :relation] merge {:rx rx :ry ry})))) (defn move-drawings [state x y] - (let [[start-x start-y] (get-in state [:drawing :starting-mouse-position]) + (let [start-x (get-in state [:drawing :starting-mouse-position :rx]) + start-y (get-in state [:drawing :starting-mouse-position :ry]) [rx ry] (cameras/screen->point (:camera state) x y) [move-x move-y] [(- rx start-x) (- ry start-y)] [snap-move-x snap-move-y] (cameras/snap-to-grid (:camera state) move-x move-y) @@ -774,6 +833,7 @@ (get-in state [:drawing :original-layers]))] (-> state (assoc-in [:drawing :layers] layers) + (assoc-in [:drawing :current-mouse-position] {:x x :y y :rx rx :ry ry}) (assoc-in [:editing-eids :editing-eids] (set (map :db/id layers)))))) (defn pan-canvas [state x y] @@ -788,23 +848,26 @@ [browser-state message [x y {:keys [shift?]}] state] (-> state - (update-mouse x y) - (cond-> (get-in state [:drawing :in-progress?]) - (draw-in-progress-drawing x y {:force-even? shift? - :delta {:x (- x (get-in state [:mouse :x])) - :y (- y (get-in state [:mouse :y]))}}) + (update-mouse x y) + (cond-> (get-in state [:drawing :in-progress?]) + (draw-in-progress-drawing x y {:force-even? shift? + :delta {:x (- x (get-in state [:mouse :x])) + :y (- y (get-in state [:mouse :y]))}}) - (get-in state [:drawing :relation-in-progress?]) - (draw-in-progress-relation x y) + (get-in state [:drawing :relation-in-progress?]) + (draw-in-progress-relation x y) - (get-in state [:drawing :moving?]) - (move-drawings x y) + (get-in state [:drawing :moving?]) + (move-drawings x y) + + (get-in state [:drawing :clip?]) + (#(assoc-in % [:drawing :current-mouse-position] (:mouse %))) - (keyboard/pan-shortcut-active? state) - ((fn [s] - (if (:mouse-down s) - (pan-canvas s x y) - (assoc-in s [:pan :position] {:x x :y y}))))))) + (keyboard/pan-shortcut-active? state) + ((fn [s] + (if (:mouse-down s) + (pan-canvas s x y) + (assoc-in s [:pan :position] {:x x :y y}))))))) (defmethod post-control-event! :text-layer-edited [browser-state message _ previous-state current-state] @@ -876,6 +939,7 @@ (assoc-in [:drawing :finished-layers] (mapv #(dissoc % :layer/current-x :layer/current-y) layers)) (assoc-in [:drawing :layers] []) (assoc-in [:drawing :moving?] false) + (assoc-in [:drawing :clip?] false) (assoc-in [:mouse-down] false) (assoc-in [:editing-eids :editing-eids] #{})))) @@ -898,6 +962,7 @@ (and (= button 0) ctrl? (not shift?)) [:open-radial] (and (= button 0) meta? (not shift?)) [:open-radial] (get-in state [:layer-properties-menu :opened?]) [:submit-layer-properties] + (get-in state [:drawing :clip?]) [:drop-clip] (contains? #{:pen :rect :circle :line :select} tool) [:start-drawing] (and (keyword-identical? tool :text) (not drawing-text?)) [:start-drawing] :else nil)))) @@ -908,6 +973,7 @@ (declare handle-layer-properties-submitted-after) (declare handle-text-layer-finished) (declare handle-text-layer-finished-after) +(declare handle-drawing-finalized-after) (defmethod control-event :mouse-depressed [browser-state message [x y {:keys [button type ctrl? shift? meta? outside-canvas?]}] state] @@ -921,6 +987,7 @@ (reduce (fn [s intent] (case intent :finish-text-layer (handle-text-layer-finished s) + :drop-clip (assoc-in s [:drawing :clip?] false) :open-radial (handle-radial-opened s) :start-drawing (handle-drawing-started s x y) :submit-layer-properties (handle-layer-properties-submitted s) @@ -928,6 +995,56 @@ s)) new-state intents)))) +(defn something-something [{:keys [layers height width min-x min-y canvas-size center?] :as layer-data} state] + (let [{:keys [entity-ids state]} (frontend.db/get-entity-ids state (count layers)) + eid-map (zipmap (map :db/id layers) entity-ids) + doc-id (:document/id state) + camera (:camera state) + zoom (:zf camera) + center-x (+ min-x (/ width 2)) + center-y (+ min-y (/ height 2)) + [mouse-x mouse-y] (cameras/snap-to-grid camera + (get-in state [:drawing :current-mouse-position :rx]) + (get-in state [:drawing :current-mouse-position :ry])) + move-x (+ (* (- center-x) zoom) + (get-in state [:drawing :current-mouse-position :rx])) + move-y (+ (* (- center-y) zoom) + (get-in state [:drawing :current-mouse-position :ry])) + [snap-move-x snap-move-y] (cameras/snap-to-grid camera move-x move-y) + new-layers (mapv (fn [l] + (-> l + (assoc :layer/ancestor (:db/id l) + :db/id (get eid-map (:db/id l)) + :layer/document doc-id + :points (when (:layer/path l) (parse-points-from-path (:layer/path l)))) + (utils/update-when-in [:layer/points-to] (fn [dests] + (set (filter :db/id (map #(update-in % [:db/id] eid-map) dests))))) + (#(move-layer % % + {:snap-x snap-move-x :snap-y snap-move-y + :move-x move-x :move-y move-y :snap-paths? true})) + (dissoc :points :layer/current-x :layer/current-y))) + layers)] + new-layers)) + +(defn handle-drop-clip-after [previous-state current-state] + (let [db (:db current-state) + layer-datas (concat (when (get-in previous-state [:drawing :layers]) + [(:drawing previous-state)]) + (map :layer-data (filter :clip/important? (get-in previous-state [:cust :cust/clips])))) + start-x (get-in previous-state [:drawing :current-mouse-position :rx]) + start-y (get-in previous-state [:drawing :current-mouse-position :ry]) + end-x (get-in previous-state [:drawing :starting-mouse-position :rx]) + end-y (get-in previous-state [:drawing :starting-mouse-position :ry]) + layer-index (get-in previous-state [:drawing :scrolled-layer]) + layer-data (layers/normalize-pasted-layer-data (nth layer-datas layer-index)) + layers (something-something layer-data previous-state)] + (doseq [layer-group (partition-all 100 layers)] + (d/transact! db + (if (= :read (:max-document-scope current-state)) + (map #(assoc % :unsaved true) layer-group) + layer-group) + {:can-undo? true})))) + (defmethod post-control-event! :mouse-depressed [browser-state message [x y {:keys [button ctrl? shift? meta? outside-canvas?]}] previous-state current-state] (when-not (empty? (:frontend-id-state previous-state)) @@ -936,7 +1053,8 @@ (doseq [intent intents] (case intent :finish-text-layer (handle-text-layer-finished-after previous-state current-state) - :open-radial (handle-radial-opened-after current-state previous-state) + :drop-clip (handle-drop-clip-after previous-state current-state) + :open-radial (handle-radial-opened-after previous-state current-state) :start-drawing nil :submit-layer-properties (handle-layer-properties-submitted-after current-state) nil))))) @@ -1025,6 +1143,24 @@ (get-in state [:drawing :moving?]) (drop-layers))))) +(defn handle-drawing-finalized-after [previous-state current-state x y] + (let [db (:db current-state) + original-layers (get-in previous-state [:drawing :original-layers]) + layers (mapv #(-> % + (dissoc :points) + (utils/update-when-in [:layer/points-to] (fn [p] (set (map :db/id p)))) + (utils/remove-map-nils)) + (get-in current-state [:drawing :finished-layers]))] + (do (when (and (some layer-model/detectable? layers) + (or (not (get-in previous-state [:drawing :moving?])) + (some true? (map detectable-movement? original-layers layers)))) + (doseq [layer-group (partition-all 100 layers)] + (d/transact! db (if (= :read (:max-document-scope current-state)) + (map #(assoc % :unsaved true) layer-group) + layer-group) + {:can-undo? true}))) + (maybe-notify-subscribers! previous-state current-state x y)))) + (defmethod post-control-event! :mouse-released [browser-state message [x y {:keys [button type ctrl? meta?]}] previous-state current-state] (let [cast! #(put! (get-in current-state [:comms :controls]) %) @@ -1059,15 +1195,7 @@ (every? #(= :layer.type/text (:layer/type %)) layers)) nil - was-drawing? (do (when (and (some layer-model/detectable? layers) - (or (not (get-in previous-state [:drawing :moving?])) - (some true? (map detectable-movement? original-layers layers)))) - (doseq [layer-group (partition-all 100 layers)] - (d/transact! db (if (= :read (:max-document-scope current-state)) - (map #(assoc % :unsaved true) layer-group) - layer-group) - {:can-undo? true}))) - (maybe-notify-subscribers! previous-state current-state x y)) + was-drawing? (handle-drawing-finalized-after previous-state current-state x y) :else nil))) @@ -1139,7 +1267,7 @@ (conjv (if append? layers []) layer))) (assoc-in [:drawing :moving?] true) - (assoc-in [:drawing :starting-mouse-position] [rx ry])))) + (assoc-in [:drawing :starting-mouse-position] {:x x :y y :rx rx :ry ry})))) (defmethod control-event :arrow-selected [browser-state message {:keys [origin dest append?]} state] @@ -1194,7 +1322,7 @@ layers)) (assoc-in [:drawing :original-layers] layers) (assoc-in [:drawing :moving?] true) - (assoc-in [:drawing :starting-mouse-position] [rx ry])))) + (assoc-in [:drawing :starting-mouse-position] {:x x :y y :rx rx :ry ry})))) (defn handle-radial-opened [state] (-> state @@ -1561,8 +1689,8 @@ (-> state (assoc-in [:layer-properties-menu :layer :layer/ui-target] (empty-str->nil value)))) -(defmethod control-event :layers-pasted - [browser-state message {:keys [layers height width min-x min-y canvas-size] :as layer-data} state] +#_(defmethod control-event :layers-pasted + [browser-state message {:keys [layers height width min-x min-y canvas-size center?] :as layer-data} state] (let [{:keys [entity-ids state]} (frontend.db/get-entity-ids state (count layers)) eid-map (zipmap (map :db/id layers) entity-ids) doc-id (:document/id state) @@ -1570,10 +1698,16 @@ zoom (:zf camera) center-x (+ min-x (/ width 2)) center-y (+ min-y (/ height 2)) - new-x (+ (* (- center-x) zoom) - (/ (:width canvas-size) 2)) - new-y (+ (* (- center-y) zoom) - (/ (:height canvas-size) 2)) + new-x (if center? + (+ (* (- center-x) zoom) + (/ (:width canvas-size) 2)) + (+ (* (- center-x) zoom) + (get-in state [:mouse :x]))) + new-y (if center? + (+ (* (- center-y) zoom) + (/ (:height canvas-size) 2)) + (+ (* (- center-y) zoom) + (get-in state [:mouse :y]))) [move-x move-y] (cameras/screen->point camera new-x new-y) [snap-move-x snap-move-y] (cameras/snap-to-grid (:camera state) move-x move-y) new-layers (mapv (fn [l] @@ -1587,19 +1721,95 @@ (#(move-layer % % {:snap-x snap-move-x :snap-y snap-move-y :move-x move-x :move-y move-y :snap-paths? true})) - (dissoc :layer/current-x :layer/current-y :points))) + (#(if center? + (dissoc % :points :layer/current-x :layer/current-y) + %)))) layers)] + (if center? + (-> state + (assoc-in [:clipboard :layers] new-layers) + (assoc-in [:selected-eids :selected-eids] (set entity-ids)) + (assoc-in [:selected-arrows :selected-arrows] (set (reduce (fn [acc layer] + (if-let [pointer (:layer/points-to layer)] + (conj acc {:origin-id (:db/id layer) + :dest-id (:db/id pointer)}) + acc)) + #{} new-layers)))) + (-> state + (dissoc-in [:clipboard :layers]) + (assoc-in [:mouse-down] true) + (update :drawing merge {:clip? true + :starting-mouse-position (:mouse state) + :layers new-layers + :original-layers new-layers}) + (assoc-in [:editing-eids :editing-eids] (set entity-ids)) + (assoc-in [:selected-eids :selected-eids] (set entity-ids)) + (assoc-in [:selected-arrows :selected-arrows] (set (reduce (fn [acc layer] + (if-let [pointer (:layer/points-to layer)] + (conj acc {:origin-id (:db/id layer) + :dest-id (:db/id pointer)}) + acc)) + #{} new-layers))))))) + +(defmethod control-event :layers-pasted + [browser-state message {:keys [layers height width min-x min-y canvas-size] :as layer-data} state] + (if layer-data + (let [{:keys [entity-ids state]} (frontend.db/get-entity-ids state (count layers)) + eid-map (zipmap (map :db/id layers) entity-ids) + doc-id (:document/id state) + camera (:camera state) + zoom (:zf camera) + center-x (+ min-x (/ width 2)) + center-y (+ min-y (/ height 2)) + new-x (+ (* (- center-x) zoom) + (get-in state [:mouse :x])) + new-y (+ (* (- center-y) zoom) + (get-in state [:mouse :y])) + [move-x move-y] (cameras/screen->point camera new-x new-y) + [snap-move-x snap-move-y] (cameras/snap-to-grid (:camera state) move-x move-y) + new-layers (mapv (fn [l] + (-> l + (assoc :db/id (get eid-map (:db/id l)) + :layer/document doc-id) + (utils/update-when-in [:layer/points-to] (fn [dests] + (set (filter :db/id (map #(update-in % [:db/id] eid-map) dests))))))) + layers)] + (-> state + (dissoc-in [:clipboard :layers]) + (assoc-in [:mouse-down] true) + ;; has to account for no clips + (assoc-in [:drawing :clip-scroll] (/ width 2)) + (assoc-in [:drawing :scrolled-layer] 0) + (update :drawing merge {:clip? true + :starting-mouse-position (:mouse state) + :current-mouse-position (:mouse state) + :layers new-layers + :width width + :height height + :min-x min-x + :min-y min-y + :original-layers new-layers}) + (assoc-in [:editing-eids :editing-eids] (set entity-ids)) + (assoc-in [:selected-eids :selected-eids] (set entity-ids)) + (assoc-in [:selected-arrows :selected-arrows] (set (reduce (fn [acc layer] + (if-let [pointer (:layer/points-to layer)] + (conj acc {:origin-id (:db/id layer) + :dest-id (:db/id pointer)}) + acc)) + #{} new-layers))))) (-> state - (assoc-in [:clipboard :layers] new-layers) - (assoc-in [:selected-eids :selected-eids] (set entity-ids)) - (assoc-in [:selected-arrows :selected-arrows] (set (reduce (fn [acc layer] - (if-let [pointer (:layer/points-to layer)] - (conj acc {:origin-id (:db/id layer) - :dest-id (:db/id pointer)}) - acc)) - #{} new-layers)))))) + (dissoc-in [:clipboard :layers]) + (assoc-in [:mouse-down] true) + (assoc-in [:drawing :scrolled-layer] 0) + (update :drawing merge {:clip? true + :starting-mouse-position (:mouse state) + :current-mouse-position (:mouse state) + :layers nil}) + (assoc-in [:editing-eids :editing-eids] #{}) + (assoc-in [:selected-eids :selected-eids] #{}) + (assoc-in [:selected-arrows :selected-arrows] #{})))) -(defmethod post-control-event! :layers-pasted +#_(defmethod post-control-event! :layers-pasted [browser-state message _ previous-state current-state] (let [db (:db current-state) layers (mapv utils/remove-map-nils (get-in current-state [:clipboard :layers]))] @@ -1610,6 +1820,19 @@ layer-group) {:can-undo? true})))) +(defmethod post-control-event! :layers-pasted + [browser-state message _ previous-state current-state] + (doseq [clip (filter #(and (:clip/important? %) + (empty? (:layer-data %))) + (get-in current-state [:cust :cust/clips]))] + (go + (let [res (async/ state @@ -2041,9 +2264,10 @@ (go ;; add xhr=true b/c it will use response from image request, which doesn't have cors headers (let [res (async/! layer-data + (update :min-x - (/ width-delta 2)) + (update :min-y + 16) ; don't let cursor overlap shapes + (assoc :width pasted-unscaled-width))))) + +(defn pasted-inactive-scale [normalized-layer-data] + (/ pasted-scaled-width + (:width normalized-layer-data))) + +;; remember that it's scrolling to the center +(defn clip-scroll [normalized-layer-datas scrolled-layer-index] + (- (/ (:width (nth normalized-layer-datas scrolled-layer-index)) + 2))) + +(defn clip-offset [normalized-layer-datas scrolled-layer-index layer-index] + (cond (= scrolled-layer-index layer-index) + (clip-scroll normalized-layer-datas layer-index) + + (> scrolled-layer-index layer-index) + (- (clip-scroll normalized-layer-datas scrolled-layer-index) + (* (- scrolled-layer-index layer-index) + (+ pasted-scaled-width pasted-padding))) + + :else + (+ (clip-scroll normalized-layer-datas scrolled-layer-index) + (:width (nth normalized-layer-datas scrolled-layer-index)) + (* (- layer-index scrolled-layer-index) + pasted-padding) + (* (dec (- layer-index scrolled-layer-index)) + pasted-scaled-width )))) diff --git a/src/pc/http/routes.clj b/src/pc/http/routes.clj index fa8ab378..091cfac7 100644 --- a/src/pc/http/routes.clj +++ b/src/pc/http/routes.clj @@ -20,6 +20,7 @@ [pc.http.urls :as urls] [pc.models.access-request :as access-request-model] [pc.models.chat-bot :as chat-bot-model] + [pc.models.clip :as clip-model] [pc.models.cust :as cust-model] [pc.models.doc :as doc-model] [pc.models.invoice :as invoice-model] @@ -44,7 +45,8 @@ :sente-id (-> req :session :sente-id) :hostname (profile/hostname)} (when-let [cust (-> req :auth :cust)] - {:cust (cust-model/read-api cust) + {:cust (assoc (cust-model/read-api cust) + :cust/clips (mapv clip-model/read-api (clip-model/find-important-by-cust (pcd/default-db) cust))) :admin? (contains? cust-model/admin-emails (:cust/email cust))}) (when-let [team (-> req :team)] {:team (team-model/public-read-api team)}) diff --git a/src/pc/http/sente.clj b/src/pc/http/sente.clj index 51147150..5bc420f0 100644 --- a/src/pc/http/sente.clj +++ b/src/pc/http/sente.clj @@ -482,8 +482,7 @@ (let [clips (clip-model/find-by-cust (:db req) cust)] (log/infof "sending %s clips to %s" (count clips) (:cust/email cust)) (send-reply req {:clips (map (fn [c] (-> c - (select-keys [:clip/uuid :clip/important?]) - (assoc :clip/s3-url (clipboard/create-presigned-clip-url c)))) + (clip-model/read-api))) clips)})))) (defmethod ws-handler :cust/delete-clip [{:keys [client-id ?data ?reply-fn] :as req}] diff --git a/src/pc/models/clip.clj b/src/pc/models/clip.clj index 2391ab3e..b4f9f94a 100644 --- a/src/pc/models/clip.clj +++ b/src/pc/models/clip.clj @@ -1,7 +1,8 @@ (ns pc.models.clip "cust is what would usually be called user, we call it cust b/c Clojure has already taken the name user in the repl" - (:require [pc.datomic :as pcd] + (:require [pc.http.clipboard :as clipboard] + [pc.datomic :as pcd] [pc.profile :as profile] [datomic.api :refer [db q] :as d])) @@ -9,7 +10,14 @@ (map #(d/entity db (:v %)) (d/datoms db :eavt (:db/id cust) :cust/clips))) -;; TODO: make sure this lookup is fast +(defn find-important-by-cust [db cust] + (map (partial d/entity db) + (d/q '{:find [[?e ...]] + :in [$ ?cust-id] + :where [[?cust-id :cust/clips ?e] + [?e :clip/important? true]]} + db (:db/id cust)))) + (defn find-by-cust-and-uuid [db cust uuid] (d/entity db (d/q '{:find [?e .] :in [$ ?cust-id ?uuid] @@ -23,3 +31,8 @@ :clip/s3-key "iphone" :clip/uuid (d/squuid) :clip/important? true}}) + +(defn read-api [clip] + (-> clip + (select-keys [:clip/uuid :clip/important?]) + (assoc :clip/s3-url (clipboard/create-presigned-clip-url clip))))