0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-11 23:31:21 -05:00

Changes indices to update only necesary data

This commit is contained in:
alonso.torres 2021-05-03 10:32:32 +02:00 committed by Andrey Antukh
parent 308fd8d4b0
commit 285a0d5f47
11 changed files with 382 additions and 219 deletions

View file

@ -461,3 +461,9 @@
kw (if (keyword? kw) (name kw) kw)]
(keyword (str prefix kw))))
(defn tap
"Simpilar to the tap in rxjs but for plain collections"
[f coll]
(f coll)
coll)

View file

@ -11,6 +11,7 @@
[app.common.pages.changes :as changes]
[app.common.pages.common :as common]
[app.common.pages.helpers :as helpers]
[app.common.pages.indices :as indices]
[app.common.pages.init :as init]
[app.common.pages.spec :as spec]
[clojure.spec.alpha :as s]))
@ -42,7 +43,6 @@
(d/export helpers/is-shape-grouped)
(d/export helpers/get-parent)
(d/export helpers/get-parents)
(d/export helpers/generate-child-parent-index)
(d/export helpers/clean-loops)
(d/export helpers/calculate-invalid-targets)
(d/export helpers/valid-frame-target)
@ -60,13 +60,17 @@
(d/export helpers/get-base-shape)
(d/export helpers/is-parent?)
(d/export helpers/get-index-in-parent)
(d/export helpers/calculate-z-index)
(d/export helpers/generate-child-all-parents-index)
(d/export helpers/parse-path-name)
(d/export helpers/merge-path-item)
(d/export helpers/compact-path)
(d/export helpers/compact-name)
;; Indices
(d/export indices/calculate-z-index)
(d/export indices/generate-child-all-parents-index)
(d/export indices/generate-child-parent-index)
(d/export indices/create-mask-index)
;; Process changes
(d/export changes/process-changes)

View file

@ -160,27 +160,6 @@
(when parent-id
(lazy-seq (cons parent-id (get-parents parent-id objects))))))
(defn generate-child-parent-index
[objects]
(reduce-kv
(fn [index id obj]
(assoc index id (:parent-id obj)))
{} objects))
(defn generate-child-all-parents-index
"Creates an index where the key is the shape id and the value is a set
with all the parents"
([objects]
(generate-child-all-parents-index objects (vals objects)))
([objects shapes]
(let [shape->parents
(fn [shape]
(->> (get-parents (:id shape) objects)
(into [])))]
(->> shapes
(map #(vector (:id %) (shape->parents %)))
(into {})))))
(defn clean-loops
"Clean a list of ids from circular references."
@ -347,40 +326,7 @@
(reduce red-fn cur-idx (reverse (:shapes object)))))]
(into {} (rec-index '() uuid/zero))))
(defn calculate-z-index
"Given a collection of shapes calculates their z-index. Greater index
means is displayed over other shapes with less index."
[objects]
(let [is-frame? (fn [id] (= :frame (get-in objects [id :type])))
root-children (get-in objects [uuid/zero :shapes])
num-frames (->> root-children (filter is-frame?) count)]
(when (seq root-children)
(loop [current (peek root-children)
pending (pop root-children)
current-idx (+ (count objects) num-frames -1)
z-index {}]
(let [children (->> (get-in objects [current :shapes]))
children (cond
(and (is-frame? current) (contains? z-index current))
[]
(and (is-frame? current)
(not (contains? z-index current)))
(into [current] children)
:else
children)
pending (into (vec pending) children)]
(if (empty? pending)
(assoc z-index current current-idx)
(let []
(recur (peek pending)
(pop pending)
(dec current-idx)
(assoc z-index current current-idx)))))))))
(defn expand-region-selection
"Given a selection selects all the shapes between the first and last in

View file

@ -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/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.common.pages.indices
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as helpers]
[app.common.uuid :as uuid]))
(defn calculate-z-index
"Given a collection of shapes calculates their z-index. Greater index
means is displayed over other shapes with less index."
[objects]
(let [is-frame? (fn [id] (= :frame (get-in objects [id :type])))
root-children (or (get-in objects [uuid/zero :shapes]) [])
num-frames (->> root-children (filterv is-frame?) count)]
(when-not (empty? root-children)
(loop [current (peek root-children)
pending (pop root-children)
current-idx (+ (count objects) num-frames -1)
z-index (transient {})]
(let [children (get-in objects [current :shapes])
assigned? (contains? z-index current)
is-frame? (is-frame? current)
pending (cond
(not is-frame?)
(d/concat pending children)
(not assigned?)
(d/concat pending [current] children)
:else
pending)]
(if (empty? pending)
(-> (assoc! z-index current current-idx)
(persistent!))
(recur (peek pending)
(pop pending)
(dec current-idx)
(assoc! z-index current current-idx))))))))
(defn generate-child-parent-index
[objects]
(reduce-kv
(fn [index id obj]
(assoc index id (:parent-id obj)))
{} objects))
(defn generate-child-all-parents-index
"Creates an index where the key is the shape id and the value is a set
with all the parents"
([objects]
(generate-child-all-parents-index objects (vals objects)))
([objects shapes]
(let [shape->parents
(fn [shape]
(->> (helpers/get-parents (:id shape) objects)
(into [])))]
(->> shapes
(map #(vector (:id %) (shape->parents %)))
(into {})))))
(defn create-mask-index
"Retrieves the mask information for an object"
[objects parents-index]
(let [retrieve-masks
(fn [id parents]
(->> parents
(map #(get objects %))
(filter #(:masked-group? %))
;; Retrieve the masking element
(mapv #(get objects (->> % :shapes first)))))]
(->> parents-index
(d/mapm retrieve-masks))))

View file

@ -264,7 +264,7 @@
:index (cp/get-index-in-parent objects (:id shape))
:shapes [(:id shape)]})))]
(when-not (empty? rch)
(when-not (empty? uch)
(rx/of dwu/pop-undo-into-transaction
(dch/commit-changes rch uch {:commit-local? true})
(dwu/commit-undo-transaction)
@ -294,12 +294,15 @@
(rx/take-until stopper)
(rx/map #(gpt/to-vec from-position %)))
snap-delta (->> position
(rx/throttle 20)
(rx/switch-map
(fn [pos]
(->> (snap/closest-snap-move page-id shapes objects layout zoom pos)
(rx/map #(vector pos %))))))]
snap-delta (rx/concat
;; We send the nil first so the stream is not waiting for the first value
(rx/of nil)
(->> position
(rx/throttle 20)
(rx/switch-map
(fn [pos]
(->> (snap/closest-snap-move page-id shapes objects layout zoom pos)
(rx/map #(vector pos %)))))))]
(if (empty? shapes)
(rx/empty)
(rx/concat

View file

@ -19,7 +19,7 @@
[beicon.core :as rx]
[clojure.set :as set]))
(defonce ^:private snap-accuracy 10)
(defonce ^:private snap-accuracy 5)
(defonce ^:private snap-path-accuracy 10)
(defonce ^:private snap-distance-accuracy 10)
@ -337,13 +337,15 @@
"Snaps a position given an old snap to a different position. We use this to provide a temporal
snap while the new is being processed."
[[position [snap-pos snap-delta]]]
(let [dx (if (not= 0 (:x snap-delta))
(- (+ (:x snap-pos) (:x snap-delta)) (:x position))
0)
dy (if (not= 0 (:y snap-delta))
(- (+ (:y snap-pos) (:y snap-delta)) (:y position))
0)]
(if (some? snap-delta)
(let [dx (if (not= 0 (:x snap-delta))
(- (+ (:x snap-pos) (:x snap-delta)) (:x position))
0)
dy (if (not= 0 (:y snap-delta))
(- (+ (:y snap-pos) (:y snap-delta)) (:y position))
0)]
(cond-> position
(<= (mth/abs dx) snap-accuracy) (update :x + dx)
(<= (mth/abs dy) snap-accuracy) (update :y + dy))))
(cond-> position
(<= (mth/abs dx) snap-accuracy) (update :x + dx)
(<= (mth/abs dy) snap-accuracy) (update :y + dy)))
position))

View file

@ -32,12 +32,16 @@
"use strict";
goog.provide("app.util.quadtree");
goog.require("cljs.core");
goog.scope(function() {
const self = app.util.quadtree;
const eq = cljs.core._EQ_;
const contains = cljs.core.contains_QMARK_;
class Node {
constructor(bounds, data) {
constructor(id, bounds, data) {
this.id = id;
this.bounds = bounds;
this.data = data;
}
@ -51,8 +55,8 @@ goog.scope(function() {
this.level = level || 0;
this.bounds = bounds;
this.objects = [];
this.indexes = [];
this.objects = [];
this.indexes = [];
}
split() {
@ -183,14 +187,18 @@ goog.scope(function() {
this.objects = [];
this.indexes = [];
}
getObjects() {
return this.objects;
}
}
self.create = function(rect) {
return new Quadtree(rect, 10, 4, 0);
};
self.insert = function(index, bounds, data) {
const node = new Node(bounds, data);
self.insert = function(index, id, bounds, data) {
const node = new Node(id, bounds, data);
index.insert(node);
return index;
};
@ -210,4 +218,29 @@ goog.scope(function() {
}
};
self.remove = function(index, id) {
const result = self.create(index.bounds);
for (let node of index.objects) {
if (!eq(id, node.id)) {
self.insert(result, node.id, node.bounds, node.data);
}
}
return result;
}
// FIXME: Inefficient to recreate the index. Needs to be improved
self.remove_all = function(index, ids) {
const result = self.create(index.bounds);
for (let node of self.search(index, index.bounds)) {
if (!contains(ids, node.id)) {
self.insert(result, node.id, node.bounds, node.data);
}
}
return result;
}
});

View file

@ -13,7 +13,7 @@
"use strict";
goog.provide("app.util.range_tree");
goog.require("cljs.core")
goog.require("cljs.core");
goog.scope(function() {
const eq = cljs.core._EQ_;
@ -92,7 +92,7 @@ goog.scope(function() {
}
isEmpty() {
return this.root === null;
return !this.root;
}
toString() {
@ -116,7 +116,7 @@ goog.scope(function() {
// Insert recursively in the tree
function recInsert (branch, value, data) {
if (branch === null) {
if (!branch) {
const ret = new Node(value, data);
ret.color = Color.RED;
return ret;
@ -144,7 +144,7 @@ goog.scope(function() {
// Search for the min node
function searchMin(branch) {
if (branch.left === null) {
if (!branch.left) {
return branch;
} else {
return searchMin(branch.left);
@ -153,7 +153,7 @@ goog.scope(function() {
// Remove the lefmost node of the current branch
function recRemoveMin(branch) {
if (branch.left === null) {
if (!branch.left) {
return null;
}
@ -167,7 +167,7 @@ goog.scope(function() {
// Remove the data element for the value given
// this will not remove the node, we have to remove the empty node afterwards
function recRemoveData(branch, value, data) {
if (branch === null) {
if (!branch) {
// Not found
return branch;
} else if (branch.value === value) {
@ -193,7 +193,7 @@ goog.scope(function() {
if (isRed(branch.left)) {
branch = rotateRight(branch);
}
if (value === branch.value && branch.right === null) {
if (value === branch.value && !branch.right) {
return null;
}
if (!isRed(branch.right) && !isRed(branch.right.left)) {
@ -214,7 +214,7 @@ goog.scope(function() {
// Retrieve all the data related to value
function recGet(branch, value) {
if (branch === null) {
if (!branch) {
return null;
} else if (branch.value === value) {
return branch.data;
@ -226,7 +226,7 @@ goog.scope(function() {
}
function recUpdate(branch, value, oldData, newData) {
if (branch === null) {
if (!branch) {
return branch;
} else if (branch.value === value) {
branch.data = branch.data.map((it) => (eq(it, oldData)) ? newData : it);
@ -239,7 +239,7 @@ goog.scope(function() {
}
function recRangeQuery(branch, fromValue, toValue, result) {
if (branch === null) {
if (!branch) {
return result;
}
if (fromValue < branch.value) {
@ -329,7 +329,7 @@ goog.scope(function() {
// This will return the string representation. We don't care about internal structure
// only the data
function recToString(branch, result) {
if (branch === null) {
if (!branch) {
return;
}

View file

@ -39,11 +39,15 @@
(defmethod handler :update-page-indices
[{:keys [page-id changes] :as message}]
(swap! state ch/process-changes changes false)
(let [old-objects (get-in @state [:pages-index page-id :objects])]
(swap! state ch/process-changes changes false)
(let [objects (get-in @state [:pages-index page-id :objects])
message (assoc message :objects objects)]
(handler (-> message
(assoc :cmd :selection/update-index)))
(handler (-> message
(assoc :cmd :snaps/update-index)))))
(let [new-objects (get-in @state [:pages-index page-id :objects])
message (assoc message
:objects new-objects
:new-objects new-objects
:old-objects old-objects)]
(handler (-> message
(assoc :cmd :selection/update-index)))
(handler (-> message
(assoc :cmd :snaps/update-index))))))

View file

@ -15,12 +15,110 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.util.quadtree :as qdt]
[app.worker.impl :as impl]))
[app.worker.impl :as impl]
[clojure.set :as set]))
(defonce state (l/atom {}))
(declare index-object)
(declare create-index)
(defn- index-object
[objects z-index parents-index masks-index index obj]
(let [{:keys [x y width height]} (:selrect obj)
shape-bound #js {:x x :y y :width width :height height}
parents (get parents-index (:id obj))
masks (get masks-index (:id obj))
z (get z-index (:id obj))
frame (when (and (not= :frame (:type obj))
(not= (:frame-id obj) uuid/zero))
(get objects (:frame-id obj)))]
(qdt/insert index
(:id obj)
shape-bound
(assoc obj :frame frame :masks masks :parents parents :z z))))
(defn- create-index
[objects]
(let [shapes (-> objects (dissoc uuid/zero) (vals))
z-index (cp/calculate-z-index objects)
parents-index (cp/generate-child-all-parents-index objects)
masks-index (cp/create-mask-index objects parents-index)
bounds (gsh/selection-rect shapes)
bounds #js {:x (:x bounds)
:y (:y bounds)
:width (:width bounds)
:height (:height bounds)}]
(reduce (partial index-object objects z-index parents-index masks-index)
(qdt/create bounds)
shapes)))
(defn- update-index
[index old-objects new-objects]
(let [changes? (fn [id]
(not= (get old-objects id)
(get new-objects id)))
changed-ids (into #{}
(filter changes?)
(set/union (keys old-objects)
(keys new-objects)))
shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?)))
z-index (cp/calculate-z-index new-objects)
parents-index (cp/generate-child-all-parents-index new-objects shapes)
masks-index (cp/create-mask-index new-objects parents-index)
new-index (qdt/remove-all index changed-ids)]
(reduce (partial index-object new-objects z-index parents-index masks-index)
new-index
shapes)))
(defn- query-index
[index rect frame-id include-frames? include-groups? disabled-masks reverse?]
(let [result (-> (qdt/search index (clj->js rect))
(es6-iterator-seq))
;; Check if the shape matches the filter criteria
match-criteria?
(fn [shape]
(and (not (:hidden shape))
(not (:blocked shape))
(or (not frame-id) (= frame-id (:frame-id shape)))
(case (:type shape)
:frame include-frames?
:group include-groups?
true)))
overlaps?
(fn [shape]
(gsh/overlaps? shape rect))
overlaps-masks?
(fn [masks]
(->> masks
(some (comp not overlaps?))
not))
;; Shapes after filters of overlapping and criteria
matching-shapes
(into []
(comp (map #(unchecked-get % "data"))
(filter match-criteria?)
(filter (comp overlaps? :frame))
(filter (comp overlaps-masks? :masks))
(filter overlaps?))
result)
keyfn (if reverse? (comp - :z) :z)]
(into (d/ordered-set)
(->> matching-shapes
(sort-by keyfn)
(map :id)))))
(defmethod impl/handler :selection/initialize-index
[{:keys [file-id data] :as message}]
@ -35,96 +133,13 @@
nil))
(defmethod impl/handler :selection/update-index
[{:keys [page-id objects] :as message}]
(let [index (create-index objects)]
(swap! state update page-id (constantly index))
nil))
[{:keys [page-id old-objects new-objects] :as message}]
(swap! state update page-id update-index old-objects new-objects)
nil)
(defmethod impl/handler :selection/query
[{:keys [page-id rect frame-id include-frames? include-groups? disabled-masks reverse?]
:or {include-groups? true disabled-masks #{} reverse? false} :as message}]
(when-let [index (get @state page-id)]
(let [result (-> (qdt/search index (clj->js rect))
(es6-iterator-seq))
;; Check if the shape matches the filter criteria
match-criteria?
(fn [shape]
(and (not (:hidden shape))
(not (:blocked shape))
(or (not frame-id) (= frame-id (:frame-id shape)))
(case (:type shape)
:frame include-frames?
:group include-groups?
true)))
overlaps?
(fn [shape]
(gsh/overlaps? shape rect))
overlaps-masks?
(fn [masks]
(->> masks
(some (comp not overlaps?))
not))
;; Shapes after filters of overlapping and criteria
matching-shapes
(into []
(comp (map #(unchecked-get % "data"))
(filter match-criteria?)
(filter (comp overlaps? :frame))
(filter (comp overlaps-masks? :masks))
(filter overlaps?))
result)
keyfn (if reverse? (comp - :z) :z)]
(into (d/ordered-set)
(->> matching-shapes
(sort-by keyfn)
(map :id))))))
(defn create-mask-index
"Retrieves the mask information for an object"
[objects parents-index]
(let [retrieve-masks
(fn [id parents]
(->> parents
(map #(get objects %))
(filter #(:masked-group? %))
;; Retrieve the masking element
(mapv #(get objects (->> % :shapes first)))))]
(->> parents-index
(d/mapm retrieve-masks))))
(defn- create-index
[objects]
(let [shapes (-> objects (dissoc uuid/zero) (vals))
z-index (cp/calculate-z-index objects)
parents-index (cp/generate-child-all-parents-index objects)
masks-index (create-mask-index objects parents-index)
bounds (gsh/selection-rect shapes)
bounds #js {:x (:x bounds)
:y (:y bounds)
:width (:width bounds)
:height (:height bounds)}]
(reduce (partial index-object objects z-index parents-index masks-index)
(qdt/create bounds)
shapes)))
(defn- index-object
[objects z-index parents-index masks-index index obj]
(let [{:keys [x y width height]} (:selrect obj)
shape-bound #js {:x x :y y :width width :height height}
parents (get parents-index (:id obj))
masks (get masks-index (:id obj))
z (get z-index (:id obj))
frame (when (and (not= :frame (:type obj))
(not= (:frame-id obj) uuid/zero))
(get objects (:frame-id obj)))]
(qdt/insert index
shape-bound
(assoc obj :frame frame :masks masks :parents parents :z z))))
(query-index index rect frame-id include-frames? include-groups? disabled-masks reverse?)))

View file

@ -6,48 +6,105 @@
(ns app.worker.snaps
(:require
[okulary.core :as l]
[app.common.uuid :as uuid]
[app.common.pages :as cp]
[app.common.data :as d]
[app.worker.impl :as impl]
[app.util.range-tree :as rt]
[app.common.pages :as cp]
[app.common.uuid :as uuid]
[app.util.geom.grid :as gg]
[app.util.geom.snap-points :as snap]
[app.util.geom.grid :as gg]))
[app.util.range-tree :as rt]
[app.worker.impl :as impl]
[clojure.set :as set]
[okulary.core :as l]))
(defonce state (l/atom {}))
(defn- create-coord-data
"Initializes the range tree given the shapes"
[frame-id shapes coord]
(let [process-shape (fn [coord]
(fn [shape]
(concat
(let [points (snap/shape-snap-points shape)]
(map #(vector % (:id shape)) points))
(defn process-shape [frame-id coord]
(fn [shape]
(let [points (snap/shape-snap-points shape)
shape-data (->> points (mapv #(vector % (:id shape))))]
(if (= (:id shape) frame-id)
(d/concat
shape-data
;; The grid points are only added by the "root" of the coord-dat
(when (= (:id shape) frame-id)
(let [points (gg/grid-snap-points shape coord)]
(map #(vector % :layout) points))))))
into-tree (fn [tree [point _ :as data]]
(rt/insert tree (coord point) data))]
;; The grid points are only added by the "root" of the coord-dat
(->> (gg/grid-snap-points shape coord)
(map #(vector % :layout))))
shape-data))))
(defn- add-coord-data
"Initializes the range tree given the shapes"
[data frame-id shapes coord]
(letfn [(into-tree [tree [point _ :as data]]
(rt/insert tree (coord point) data))]
(->> shapes
(mapcat (process-shape coord))
(reduce into-tree (rt/make-tree)))))
(mapcat (process-shape frame-id coord))
(reduce into-tree data))))
(defn remove-coord-data
[data frame-id shapes coord]
(letfn [(remove-tree [tree [point _ :as data]]
(rt/remove tree (coord point) data))]
(->> shapes
(mapcat (process-shape frame-id coord))
(reduce remove-tree data))))
(defn aggregate-data
([objects]
(aggregate-data objects (keys objects)))
([objects ids]
(->> ids
(filter #(contains? objects %))
(map #(get objects %))
(filter :frame-id)
(group-by :frame-id)
;; Adds the frame
(d/mapm #(conj %2 (get objects %1))))))
(defn- initialize-snap-data
"Initialize the snap information with the current workspace information"
[objects]
(let [frame-shapes (->> (vals objects)
(filter :frame-id)
(group-by :frame-id))
frame-shapes (->> (cp/select-frames objects)
(reduce #(update %1 (:id %2) conj %2) frame-shapes))]
(let [shapes-data (aggregate-data objects)
(d/mapm (fn [frame-id shapes] {:x (create-coord-data frame-id shapes :x)
:y (create-coord-data frame-id shapes :y)})
frame-shapes)))
create-index
(fn [frame-id shapes] {:x (add-coord-data (rt/make-tree) frame-id shapes :x)
:y (add-coord-data (rt/make-tree) frame-id shapes :y)})]
(d/mapm create-index shapes-data)))
(defn- update-snap-data
[snap-data old-objects new-objects]
(let [changed? #(not= (get old-objects %) (get new-objects %))
changed-ids (into #{}
(filter changed?)
(set/union (keys old-objects) (keys new-objects)))
to-delete (aggregate-data old-objects changed-ids)
to-add (aggregate-data new-objects changed-ids)
delete-data
(fn [snap-data [frame-id shapes]]
(-> snap-data
(update-in [frame-id :x] remove-coord-data frame-id shapes :x)
(update-in [frame-id :y] remove-coord-data frame-id shapes :y)))
add-data
(fn [snap-data [frame-id shapes]]
(-> snap-data
(update-in [frame-id :x] add-coord-data frame-id shapes :x)
(update-in [frame-id :y] add-coord-data frame-id shapes :y)))
snap-data (->> to-delete
(reduce delete-data snap-data))
snap-data (->> to-add
(reduce add-data snap-data))]
snap-data))
(defn- log-state
"Helper function to print a friendly version of the snap tree. Debugging purposes"
@ -60,6 +117,16 @@
(let [snap-data (initialize-snap-data objects)]
(assoc state page-id snap-data)))
(defn- update-page [state page-id old-objects new-objects]
(let [changed? #(not= (get old-objects %) (get new-objects %))
changed-ids (into #{}
(filter changed?)
(set/union (keys old-objects) (keys new-objects)))
snap-data (get state page-id)
snap-data (update-snap-data snap-data old-objects new-objects)]
(assoc state page-id snap-data)))
;; Public API
(defmethod impl/handler :snaps/initialize-index
[{:keys [file-id data] :as message}]
@ -74,9 +141,9 @@
nil))
(defmethod impl/handler :snaps/update-index
[{:keys [page-id objects] :as message}]
[{:keys [page-id old-objects new-objects] :as message}]
;; TODO: Check the difference and update the index acordingly
(swap! state index-page page-id objects)
(swap! state update-page page-id old-objects new-objects)
;; (log-state)
nil)