🎉 Add library page for components on migration

This commit is contained in:
Andrés Moya 2022-06-13 16:30:29 +02:00
parent bdcbe46d0d
commit ce09ea6eb5
10 changed files with 150 additions and 252 deletions

View file

@ -173,6 +173,10 @@
[data] [data]
(into {} (remove (comp nil? second)) data)) (into {} (remove (comp nil? second)) data))
(defn vec-without-nils
[coll]
(into [] (remove nil?) coll))
(defn without-qualified (defn without-qualified
[data] [data]
(into {} (remove (comp qualified-keyword? first)) data)) (into {} (remove (comp qualified-keyword? first)) data))

View file

@ -13,6 +13,7 @@
[app.common.pages.changes :as ch] [app.common.pages.changes :as ch]
[app.common.pages.changes-spec :as pcs] [app.common.pages.changes-spec :as pcs]
[app.common.pages.init :as init] [app.common.pages.init :as init]
[app.common.types.page :as ctp]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -179,8 +180,7 @@
(assert (nil? (:current-component-id file))) (assert (nil? (:current-component-id file)))
(let [page-id (or (:id data) (uuid/next)) (let [page-id (or (:id data) (uuid/next))
page (-> init/empty-page-data page (-> (ctp/make-empty-page page-id "Page-1")
(assoc :id page-id)
(d/deep-merge data))] (d/deep-merge data))]
(-> file (-> file
(commit-change (commit-change

View file

@ -41,11 +41,21 @@
;; --- Helpers ;; --- Helpers
(defn bounding-box
"Returns a rect that wraps the shape after all transformations applied."
[shape]
; TODO: perhaps we need to store this calculation in a shape attribute
(gpr/points->rect (:points shape)))
(defn left-bound (defn left-bound
"Returns the lowest x coord of the shape BEFORE applying transformations."
; TODO: perhaps some day we want after transformations, but for the
; moment it's enough as is now.
[shape] [shape]
(get shape :x (:x (:selrect shape)))) ; Paths don't have :x attribute (get shape :x (:x (:selrect shape)))) ; Paths don't have :x attribute
(defn top-bound (defn top-bound
"Returns the lowest y coord of the shape BEFORE applying transformations."
[shape] [shape]
(get shape :y (:y (:selrect shape)))) ; Paths don't have :y attribute (get shape :y (:y (:selrect shape)))) ; Paths don't have :y attribute

View file

@ -17,7 +17,10 @@
[app.common.pages.init :as init] [app.common.pages.init :as init]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.pages.changes-spec :as pcs] [app.common.pages.changes-spec :as pcs]
[app.common.types.shape :as cts])) [app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Specific helpers ;; Specific helpers
@ -28,10 +31,6 @@
[coll o] [coll o]
(into [] (filter #(not= % o)) coll)) (into [] (filter #(not= % o)) coll))
(defn vec-without-nils
[coll]
(into [] (remove nil?) coll))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Page Transformation Changes ;; Page Transformation Changes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -74,44 +73,9 @@
(defmethod process-change :add-obj (defmethod process-change :add-obj
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}] [data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
(letfn [(update-parent-shapes [shapes] (let [update-container
;; Ensure that shapes is always a vector. (fn [container]
(let [shapes (into [] shapes)] (ctst/add-shape id obj container frame-id parent-id index ignore-touched))]
(cond
(some #{id} shapes)
shapes
(nil? index)
(conj shapes id)
:else
(cph/insert-at-index shapes index [id]))))
(update-parent [parent]
(-> parent
(update :shapes update-parent-shapes)
(update :shapes vec-without-nils)
(cond-> (and (:shape-ref parent)
(not= (:id parent) frame-id)
(not ignore-touched))
(-> (update :touched cph/set-touched-group :shapes-group)
(dissoc :remote-synced?)))))
;; TODO: this looks wrong, why we allow nil values?
(update-objects [objects parent-id]
(if (and (or (nil? parent-id) (contains? objects parent-id))
(or (nil? frame-id) (contains? objects frame-id)))
(-> objects
(assoc id (-> obj
(assoc :frame-id frame-id)
(assoc :parent-id parent-id)
(assoc :id id)))
(update parent-id update-parent))
objects))
(update-container [data]
(let [parent-id (or parent-id frame-id)]
(update data :objects update-objects parent-id)))]
(if page-id (if page-id
(d/update-in-when data [:pages-index page-id] update-container) (d/update-in-when data [:pages-index page-id] update-container)
@ -237,7 +201,7 @@
;; We need to ensure that no `nil` in the ;; We need to ensure that no `nil` in the
;; shapes list after adding all the ;; shapes list after adding all the
;; incoming shapes to the parent. ;; incoming shapes to the parent.
(update :shapes vec-without-nils))] (update :shapes d/vec-without-nils))]
(cond-> parent (cond-> parent
(and (:shape-ref parent) (= (:type parent) :group) (not ignore-touched)) (and (:shape-ref parent) (= (:type parent) :group) (not ignore-touched))
(-> (update :touched cph/set-touched-group :shapes-group) (-> (update :touched cph/set-touched-group :shapes-group)
@ -258,7 +222,7 @@
(-> objects (-> objects
(d/update-in-when [pid :shapes] without-obj sid) (d/update-in-when [pid :shapes] without-obj sid)
(d/update-in-when [pid :shapes] vec-without-nils) (d/update-in-when [pid :shapes] d/vec-without-nils)
(cond-> component? (d/update-when pid #(-> % (cond-> component? (d/update-when pid #(-> %
(update :touched cph/set-touched-group :shapes-group) (update :touched cph/set-touched-group :shapes-group)
(dissoc :remote-synced?))))))))) (dissoc :remote-synced?)))))))))
@ -323,22 +287,11 @@
[data {:keys [id name page]}] [data {:keys [id name page]}]
(when (and id name page) (when (and id name page)
(ex/raise :type :conflict (ex/raise :type :conflict
:hint "name or page should be provided, never both")) :hint "id+name or page should be provided, never both"))
(letfn [(conj-if-not-exists [pages id] (let [page (if (and (string? name) (uuid? id))
(cond-> pages (ctp/make-empty-page id name)
(not (d/seek #(= % id) pages)) page)]
(conj id)))] (ctpl/add-page data page)))
(if (and (string? name) (uuid? id))
(let [page (assoc init/empty-page-data
:id id
:name name)]
(-> data
(update :pages conj-if-not-exists id)
(update :pages-index assoc id page)))
(-> data
(update :pages conj-if-not-exists (:id page))
(update :pages-index assoc (:id page) page)))))
(defmethod process-change :mod-page (defmethod process-change :mod-page
[data {:keys [id name]}] [data {:keys [id name]}]

View file

@ -9,7 +9,7 @@
[app.common.colors :as clr] [app.common.colors :as clr]
[app.common.uuid :as uuid])) [app.common.uuid :as uuid]))
(def file-version 19) (def file-version 20)
(def default-color clr/gray-20) (def default-color clr/gray-20)
(def root uuid/zero) (def root uuid/zero)

View file

@ -1,186 +0,0 @@
;; 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.init
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.shapes :as gsh]
[app.common.pages.common :refer [file-version default-color]]
[app.common.uuid :as uuid]))
(def root uuid/zero)
(def empty-page-data
{:options {}
:name "Page-1"
:objects
{root
{:id root
:type :frame
:name "Root Frame"}}})
(def empty-file-data
{:version file-version
:pages []
:pages-index {}})
(def default-shape-attrs
{})
(def default-frame-attrs
{:frame-id uuid/zero
:fills [{:fill-color clr/white
:fill-opacity 1}]
:strokes []
:shapes []
:hide-fill-on-export false})
(def ^:private minimal-shapes
[{:type :rect
:name "Rect-1"
:fills [{:fill-color default-color
:fill-opacity 1}]
:strokes []
:rx 0
:ry 0}
{:type :image
:rx 0
:ry 0
:fills []
:strokes []}
{:type :circle
:name "Circle-1"
:fills [{:fill-color default-color
:fill-opacity 1}]
:strokes []}
{:type :path
:name "Path-1"
:fills []
:strokes [{:stroke-style :solid
:stroke-alignment :center
:stroke-width 2
:stroke-color clr/black
:stroke-opacity 1}]}
{:type :frame
:name "Board-1"
:fills [{:fill-color clr/white
:fill-opacity 1}]
:strokes []
:stroke-style :none
:stroke-alignment :center
:stroke-width 0
:stroke-color clr/black
:stroke-opacity 0
:rx 0
:ry 0}
{:type :text
:name "Text-1"
:content nil}
{:type :svg-raw}])
(def empty-selrect
{:x 0 :y 0
:x1 0 :y1 0
:x2 0.01 :y2 0.01
:width 0.01 :height 0.01})
(defn make-minimal-shape
[type]
(let [type (cond (= type :curve) :path
:else type)
shape (d/seek #(= type (:type %)) minimal-shapes)]
(when-not shape
(ex/raise :type :assertion
:code :shape-type-not-implemented
:context {:type type}))
(cond-> shape
:always
(assoc :id (uuid/next))
(not= :path (:type shape))
(assoc :x 0
:y 0
:width 0.01
:height 0.01
:selrect {:x 0
:y 0
:x1 0
:y1 0
:x2 0.01
:y2 0.01
:width 0.01
:height 0.01}))))
(defn make-minimal-group
[frame-id selection-rect group-name]
{:id (uuid/next)
:type :group
:name group-name
:shapes []
:frame-id frame-id
:x (:x selection-rect)
:y (:y selection-rect)
:width (:width selection-rect)
:height (:height selection-rect)})
(defn make-file-data
([file-id]
(make-file-data file-id (uuid/next)))
([file-id page-id]
(let [pd (assoc empty-page-data
:id page-id
:name "Page-1")]
(-> empty-file-data
(assoc :id file-id)
(update :pages conj page-id)
(update :pages-index assoc page-id pd)))))
(defn setup-rect-selrect
"Initializes the selrect and points for a shape"
[shape]
(let [selrect (gsh/rect->selrect shape)
points (gsh/rect->points shape)]
(-> shape
(assoc :selrect selrect
:points points))))
(defn- setup-rect
"A specialized function for setup rect-like shapes."
[shape {:keys [x y width height]}]
(-> shape
(assoc :x x :y y :width width :height height)
(setup-rect-selrect)))
(defn- setup-image
[{:keys [metadata] :as shape} props]
(-> (setup-rect shape props)
(assoc
:proportion (/ (:width metadata)
(:height metadata))
:proportion-lock true)))
(defn setup-shape
"A function that initializes the first coordinates for
the shape. Used mainly for draw operations."
([props]
(setup-shape {:type :rect} props))
([shape props]
(case (:type shape)
:image (setup-image shape props)
(setup-rect shape props))))

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.path :as gsp]
[app.common.geom.shapes.text :as gsht] [app.common.geom.shapes.text :as gsht]
@ -15,6 +16,10 @@
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.types.container :as ctc]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -432,5 +437,72 @@
(update :pages-index d/update-vals update-container) (update :pages-index d/update-vals update-container)
(update :components d/update-vals update-container)))) (update :components d/update-vals update-container))))
(defmethod migrate 20
[data]
(let [page-id (uuid/next)
components (->> (:components data)
vals
(sort-by :name))
add-library-page
(fn [data]
(let [page (ctp/make-empty-page page-id "Library page")]
(-> data
(ctpl/add-page page))))
add-main-instance
(fn [data component position]
(let [page (ctpl/get-page data page-id)
[new-shape new-shapes]
(ctc/instantiate-component page
component
(:id data)
position)
add-shape
(fn [data shape]
(update-in data [:pages-index page-id]
#(ctst/add-shape (:id shape)
shape
%
(:frame-id shape)
(:parent-id shape)
nil ; <- As shapes are ordered, we can safely add each
true))) ; one at the end of the parent's children list.
update-component
(fn [component]
(assoc component
:main-instance-id (:id new-shape)
:main-instance-page page-id))]
(as-> data $
(reduce add-shape $ new-shapes)
(update-in $ [:components (:id component)] update-component))))
add-instance-grid
(fn [data components]
(let [position-seq (ctst/generate-shape-grid
(map cph/get-component-root components)
50)]
(loop [data data
components-seq (seq components)
position-seq position-seq]
(let [component (first components-seq)
position (first position-seq)]
(if (nil? component)
data
(recur (add-main-instance data component position)
(rest components-seq)
(rest position-seq)))))))]
(if (empty? components)
data
(-> data
(add-library-page)
(add-instance-grid components)))))
;; TODO: pending to do a migration for delete already not used fill ;; TODO: pending to do a migration for delete already not used fill
;; and stroke props. This should be done for >1.14.x version. ;; and stroke props. This should be done for >1.14.x version.

View file

@ -9,6 +9,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
;; --- Grid options ;; --- Grid options
@ -95,6 +96,23 @@
(s/def ::page (s/def ::page
(s/keys :req-un [::id ::name ::objects ::options])) (s/keys :req-un [::id ::name ::objects ::options]))
;; --- Initialization
(def root uuid/zero)
(def empty-page-data
{:options {}
:objects {root
{:id root
:type :frame
:name "Root Frame"}}})
(defn make-empty-page
[id name]
(assoc empty-page-data
:id id
:name name))
;; --- Helpers for flow ;; --- Helpers for flow
(defn rename-flow (defn rename-flow

View file

@ -272,10 +272,13 @@
original ones. original ones.
Returns the cloned object, the list of all new objects (including Returns the cloned object, the list of all new objects (including
the cloned one), and possibly a list of original objects modified." the cloned one), and possibly a list of original objects modified.
The list of objects are returned in tree traversal order, respecting
the order of the children of each parent."
([object parent-id objects update-new-object] ([object parent-id objects update-new-object]
(clone-object object parent-id objects update-new-object identity)) (clone-object object parent-id objects update-new-object (fn [object _] object)))
([object parent-id objects update-new-object update-original-object] ([object parent-id objects update-new-object update-original-object]
(let [new-id (uuid/next)] (let [new-id (uuid/next)]
@ -316,3 +319,27 @@
(into new-children new-child-objects) (into new-children new-child-objects)
(into updated-children updated-child-objects)))))))) (into updated-children updated-child-objects))))))))
(defn generate-shape-grid
"Generate a sequence of positions that lays out the list of
shapes in a grid of equal-sized rows and columns."
[shapes gap]
(let [shapes-bounds (map gsh/bounding-box shapes)
grid-size (mth/ceil (mth/sqrt (count shapes)))
row-size (+ (apply max (map :height shapes-bounds))
gap)
column-size (+ (apply max (map :width shapes-bounds))
gap)
next-pos (fn [position]
(let [counter (inc (:counter (meta position)))
row (quot counter grid-size)
column (mod counter grid-size)
new-pos (gpt/point (* column column-size)
(* row row-size))]
(with-meta new-pos
{:counter counter})))]
(iterate next-pos
(with-meta (gpt/point 0 0)
{:counter 0}))))