diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d219ba911..ee749d9ffa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,7 +34,7 @@ jobs: working_directory: "./frontend" command: | yarn install - yarn run lint-scss + yarn run lint:scss - run: name: common lint diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index f5321b2f6d..a2bc5ee818 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -17,6 +17,7 @@ {app.common.data.macros/export hooks.export/export potok.core/reify hooks.export/potok-reify app.util.services/defmethod hooks.export/service-defmethod + app.common.record/defrecord hooks.export/penpot-defrecord }} :output diff --git a/.clj-kondo/hooks/export.clj b/.clj-kondo/hooks/export.clj index f59bd669e8..9486ed67c1 100644 --- a/.clj-kondo/hooks/export.clj +++ b/.clj-kondo/hooks/export.clj @@ -39,6 +39,29 @@ other))] {:node result}))) + +(defn penpot-defrecord + [{:keys [:node]}] + (let [[rnode rtype rparams & other] (:children node) + + nodes [(api/token-node (symbol "do")) + (api/list-node + (into [(api/token-node (symbol (name (:value rnode)))) rtype rparams] other)) + (api/list-node + [(api/token-node (symbol "defn")) + (api/token-node (symbol (str "pos->" (:string-value rtype)))) + (api/vector-node + (->> (:children rparams) + (mapv (fn [t] + (api/token-node (symbol (str "_" (:string-value t)))))))) + (api/token-node nil)])] + + result (api/list-node nodes)] + + ;; (prn "=====>" (into {} rparams)) + ;; (prn (api/sexpr result)) + {:node result})) + (defn clojure-specify [{:keys [:node]}] (let [[rnode rtype & other] (:children node) @@ -48,7 +71,6 @@ other))] {:node result})) - (defn service-defmethod [{:keys [:node]}] (let [[rnode rtype ?meta & other] (:children node) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index b039b60a7c..e94474db56 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -11,9 +11,9 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.files.features :as ffeat] + [app.common.files.migrations :as pmg] [app.common.fressian :as fres] [app.common.logging :as l] - [app.common.pages.migrations :as pmg] [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cf] diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 7eeaaa7561..d171f28b32 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -9,8 +9,8 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.files.migrations :as pmg] [app.common.pages.helpers :as cph] - [app.common.pages.migrations :as pmg] [app.common.schema :as sm] [app.common.schema.desc-js-like :as-alias smdj] [app.common.schema.generators :as sg] @@ -46,11 +46,14 @@ (def supported-features #{"storage/objects-map" "storage/pointer-map" + "internal/shape-record" + "internal/geom-record" "components/v2"}) (defn get-default-features [] - (cond-> #{} + (cond-> #{"internal/shape-record" + "internal/geom-record"} (contains? cf/flags :fdata-storage-pointer-map) (conj "storage/pointer-map") @@ -305,17 +308,17 @@ ;; --- COMMAND QUERY: get-file (by id) -(sm/def! ::features +(def schema:features [:schema {:title "FileFeatures" ::smdj/inline true :gen/gen (sg/subseq supported-features)} ::sm/set-of-strings]) -(sm/def! ::file +(def schema:file [:map {:title "File"} [:id ::sm/uuid] - [:features ::features] + [:features schema:features] [:has-media-trimmed :boolean] [:comment-thread-seqn {:min 0} :int] [:name :string] @@ -326,18 +329,18 @@ [:created-at ::dt/instant] [:data {:optional true} :any]]) -(sm/def! ::permissions-mixin +(def schema:permissions-mixin [:map {:title "PermissionsMixin"} [:permissions ::perms/permissions]]) -(sm/def! ::file-with-permissions +(def schema:file-with-permissions [:merge {:title "FileWithPermissions"} - ::file - ::permissions-mixin]) + schema:file + schema:permissions-mixin]) -(sm/def! ::get-file +(def schema:get-file [:map {:title "get-file"} - [:features {:optional true} ::features] + [:features {:optional true} schema:features] [:id ::sm/uuid] [:project-id {:optional true} ::sm/uuid]]) @@ -377,8 +380,8 @@ {::doc/added "1.17" ::cond/get-object #(get-minimal-file %1 (:id %2)) ::cond/key-fn get-file-etag - ::sm/params ::get-file - ::sm/result ::file-with-permissions} + ::sm/params schema:get-file + ::sm/result schema:file-with-permissions} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}] (dm/with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id id)] @@ -390,14 +393,14 @@ ;; --- COMMAND QUERY: get-file-fragment (by id) -(sm/def! ::file-fragment +(def schema:file-fragment [:map {:title "FileFragment"} [:id ::sm/uuid] [:file-id ::sm/uuid] [:created-at ::dt/instant] [:content any?]]) -(sm/def! ::get-file-fragment +(def schema:get-file-fragment [:map {:title "get-file-fragment"} [:file-id ::sm/uuid] [:fragment-id ::sm/uuid] @@ -411,8 +414,8 @@ (sv/defmethod ::get-file-fragment "Retrieve a file by its ID. Only authenticated users." {::doc/added "1.17" - ::sm/params ::get-file-fragment - ::sm/result ::file-fragment} + ::sm/params schema:get-file-fragment + ::sm/result schema:file-fragment} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }] (dm/with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id file-id share-id)] @@ -447,12 +450,18 @@ (assoc :thumbnail-uri (resolve-public-uri media-id))) (dissoc row :media-id)))))) +(def schema:get-project-files + [:map {:title "get-project-files"} + [:project-id ::sm/uuid]]) + +(def schema:files + [:vector schema:file]) + (sv/defmethod ::get-project-files "Get all files for the specified project." {::doc/added "1.17" - ::sm/params [:map {:title "get-project-files"} - [:project-id ::sm/uuid]] - ::sm/result [:vector ::file]} + ::sm/params schema:get-project-files + ::sm/result schema:files} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}] (dm/with-open [conn (db/open pool)] (projects/check-read-permissions! conn profile-id project-id) @@ -463,11 +472,14 @@ (declare get-has-file-libraries) +(def schema:has-file-libraries + [:map {:title "has-file-libraries"} + [:file-id ::sm/uuid]]) + (sv/defmethod ::has-file-libraries "Checks if the file has libraries. Returns a boolean" {::doc/added "1.15.1" - ::sm/params [:map {:title "has-file-libraries"} - [:file-id ::sm/uuid]] + ::sm/params schema:has-file-libraries ::sm/result :boolean} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] (dm/with-open [conn (db/open pool)] @@ -522,13 +534,13 @@ (uuid? object-id) (prune-objects object-id)))) -(sm/def! ::get-page +(def schema:get-page [:map {:title "GetPage"} [:file-id ::sm/uuid] [:page-id {:optional true} ::sm/uuid] [:share-id {:optional true} ::sm/uuid] [:object-id {:optional true} ::sm/uuid] - [:features {:optional true} ::features]]) + [:features {:optional true} schema:features]]) (sv/defmethod ::get-page "Retrieves the page data from file and returns it. If no page-id is @@ -541,7 +553,7 @@ Mainly used for rendering purposes." {::doc/added "1.17" - ::sm/params ::get-page} + ::sm/params schema:get-page} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] (dm/with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id file-id share-id)] diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index 9b54efe683..ceb40be9dc 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -162,18 +162,18 @@ frames (filter cph/frame-shape? (vals objects))] (if-let [frame (-> frames first)] - (let [frame-id (:id frame) + (let [frame-id (:id frame) object-id (str page-id frame-id) - frame (if-let [thumb (get thumbnails object-id)] - (assoc frame :thumbnail thumb :shapes []) - (dissoc frame :thumbnail)) + frame (if-let [thumb (get thumbnails object-id)] + (assoc frame :thumbnail thumb :shapes []) + (dissoc frame :thumbnail)) children-ids (cph/get-children-ids objects frame-id) bounds (when (:show-content frame) - (gsh/selection-rect (concat [frame] (->> children-ids (map (d/getf objects)))))) + (gsh/shapes->rect (cons frame (map (d/getf objects) children-ids)))) frame (cond-> frame @@ -215,18 +215,25 @@ :always (update :objects assoc-thumbnails page-id thumbs)))))) +(def ^:private schema:get-file-data-for-thumbnail + [:map {:title "get-file-data-for-thumbnail"} + [:file-id ::sm/uuid] + [:features {:optional true} files/schema:features]]) + +(def ^:private schema:partial-file + [:map {:title "PartialFile"} + [:id ::sm/uuid] + [:revn {:min 0} :int] + [:page :any]]) + (sv/defmethod ::get-file-data-for-thumbnail "Retrieves the data for generate the thumbnail of the file. Used mainly for render thumbnails on dashboard." {::doc/added "1.17" - ::sm/params [:map {:title "get-file-data-for-thumbnail"} - [:file-id ::sm/uuid] - [:features {:optional true} ::files/features]] - ::sm/result [:map {:title "PartialFile"} - [:id ::sm/uuid] - [:revn {:min 0} :int] - [:page :any]]} + ::sm/params schema:get-file-data-for-thumbnail + ::sm/result schema:partial-file} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}] (dm/with-open [conn (db/open pool)] (files/check-read-permissions! conn profile-id file-id) diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index 0accca0a37..2b9b94d512 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -8,13 +8,12 @@ (:require [app.common.exceptions :as ex] [app.common.files.features :as ffeat] + [app.common.files.migrations :as pmg] [app.common.logging :as l] [app.common.pages :as cp] [app.common.pages.changes :as cpc] - [app.common.pages.migrations :as pmg] [app.common.schema :as sm] [app.common.schema.generators :as smg] - [app.common.spec :as us] [app.common.types.file :as ctf] [app.common.uuid :as uuid] [app.config :as cf] @@ -32,37 +31,7 @@ [app.util.objects-map :as omap] [app.util.pointer-map :as pmap] [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s])) - -;; --- SPECS - -(s/def ::changes - (s/coll-of map? :kind vector?)) - -(s/def ::hint-origin ::us/keyword) -(s/def ::hint-events - (s/every ::us/keyword :kind vector?)) - -(s/def ::change-with-metadata - (s/keys :req-un [::changes] - :opt-un [::hint-origin - ::hint-events])) - -(s/def ::changes-with-metadata - (s/every ::change-with-metadata :kind vector?)) - -(s/def ::session-id ::us/uuid) -(s/def ::revn ::us/integer) -(s/def ::update-file - (s/and - (s/keys :req [::rpc/profile-id] - :req-un [::files/id ::session-id ::revn] - :opt-un [::changes ::changes-with-metadata ::features]) - (fn [o] - (or (contains? o :changes) - (contains? o :changes-with-metadata))))) - + [app.util.time :as dt])) ;; --- SCHEMA @@ -177,6 +146,7 @@ (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id id) (db/xact-lock! conn id) + (let [cfg (assoc cfg ::db/conn conn) params (assoc params :profile-id profile-id) tpoint (dt/tpoint)] diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 23dbd13581..3de69a9dd2 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -9,7 +9,7 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] - [app.common.pages.migrations :as pmg] + [app.common.files.migrations :as pmg] [app.common.schema :as sm] [app.common.spec :as us] [app.common.uuid :as uuid] diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index e7db69eced..6ee1977347 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -83,7 +83,7 @@ [:map {:title "get-view-only-bundle"} [:file-id ::sm/uuid] [:share-id {:optional true} ::sm/uuid] - [:features {:optional true} ::files/features]]) + [:features {:optional true} files/schema:features]]) (sv/defmethod ::get-view-only-bundle {::rpc/auth false diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index ca09826766..715fb77329 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -14,7 +14,7 @@ [app.common.files.features :as ffeat] [app.common.logging :as l] [app.common.pages :as cp] - [app.common.pages.migrations :as pmg] + [app.common.files.migrations :as pmg] [app.common.pprint :refer [pprint]] [app.common.spec :as us] [app.common.uuid :as uuid] diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index 9b9c2134a9..1fd72721dd 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -11,8 +11,8 @@ inactivity (the default threshold is 72h)." (:require [app.common.data :as d] + [app.common.files.migrations :as pmg] [app.common.logging :as l] - [app.common.pages.migrations :as pmg] [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] [app.common.types.shape-tree :as ctt] diff --git a/backend/src/app/util/async.clj b/backend/src/app/util/async.clj deleted file mode 100644 index 217f54ac02..0000000000 --- a/backend/src/app/util/async.clj +++ /dev/null @@ -1,113 +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) KALEIDOS INC - -(ns app.util.async - (:require - [app.common.exceptions :as ex] - [clojure.core.async :as a] - [clojure.core.async.impl.protocols :as ap] - [clojure.spec.alpha :as s]) - (:import - java.util.concurrent.Executor - java.util.concurrent.RejectedExecutionException)) - -(s/def ::executor #(instance? Executor %)) -(s/def ::channel #(satisfies? ap/Channel %)) - -(defonce processors - (delay (.availableProcessors (Runtime/getRuntime)))) - -(defmacro go-try - [& body] - `(a/go - (try - ~@body - (catch Exception e# e#)))) - -(defmacro thread - [& body] - `(a/thread - (try - ~@body - (catch Exception e# - e#)))) - -(defmacro ~ch a/close!)))) - -(defn thread-call - [^Executor executor f] - (let [ch (a/chan 1) - f' (fn [] - (try - (let [ret (ex/try* f identity)] - (when (some? ret) (a/>!! ch ret))) - (finally - (a/close! ch))))] - (try - (.execute executor f') - (catch RejectedExecutionException _cause - (a/close! ch))) - - ch)) - -(defmacro with-thread - [executor & body] - (if (= executor ::default) - `(a/thread-call (^:once fn* [] (try ~@body (catch Exception e# e#)))) - `(thread-call ~executor (^:once fn* [] ~@body)))) - -(defn batch - [in {:keys [max-batch-size - max-batch-age - buffer-size - init] - :or {max-batch-size 200 - max-batch-age (* 30 1000) - buffer-size 128 - init #{}} - :as opts}] - (let [out (a/chan buffer-size)] - (a/go-loop [tch (a/timeout max-batch-age) buf init] - (let [[val port] (a/alts! [tch in])] - (cond - (identical? port tch) - (if (empty? buf) - (recur (a/timeout max-batch-age) buf) - (do - (a/>! out [:timeout buf]) - (recur (a/timeout max-batch-age) init))) - - (nil? val) - (if (empty? buf) - (a/close! out) - (do - (a/offer! out [:timeout buf]) - (a/close! out))) - - (identical? port in) - (let [buf (conj buf val)] - (if (>= (count buf) max-batch-size) - (do - (a/>! out [:size buf]) - (recur (a/timeout max-batch-age) init)) - (recur tch buf)))))) - out)) - -(defn thread-sleep - [ms] - (Thread/sleep (long ms))) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index d39fd2d393..a4a1631400 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -7,6 +7,7 @@ (ns backend-tests.rpc-file-test (:require [app.common.uuid :as uuid] + [app.common.types.shape :as cts] [app.db :as db] [app.db.sql :as sql] [app.http :as http] @@ -187,11 +188,12 @@ :parent-id uuid/zero :frame-id uuid/zero :components-v2 true - :obj {:id shape-id - :name "image" - :frame-id uuid/zero - :parent-id uuid/zero - :type :rect}}]) + :obj (cts/setup-shape + {:id shape-id + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :rect})}]) ;; Check the number of fragments (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] @@ -282,12 +284,13 @@ :parent-id uuid/zero :frame-id uuid/zero :components-v2 true - :obj {:id shid - :name "image" - :frame-id uuid/zero - :parent-id uuid/zero - :type :image - :metadata {:id (:id fmo1) :width 200 :height 200 :mtype "image/jpeg"}}}]) + :obj (cts/setup-shape + {:id shid + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :image + :metadata {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"}})}]) ;; Check that reference storage objects on filemediaobjects ;; are the same because of deduplication feature. @@ -547,38 +550,42 @@ shape2-id (uuid/next) changes [{:type :add-obj - :page-id page-id - :id frame1-id - :parent-id uuid/zero - :frame-id uuid/zero - :obj {:id frame1-id - :use-for-thumbnail? true - :name "test-frame1" - :type :frame}} - {:type :add-obj - :page-id page-id - :id shape1-id - :parent-id frame1-id - :frame-id frame1-id - :obj {:id shape1-id - :name "test-shape1" - :type :rect}} - {:type :add-obj - :page-id page-id - :id frame2-id - :parent-id uuid/zero - :frame-id uuid/zero - :obj {:id frame2-id - :name "test-frame2" - :type :frame}} - {:type :add-obj - :page-id page-id - :id shape2-id - :parent-id frame2-id - :frame-id frame2-id - :obj {:id shape2-id - :name "test-shape2" - :type :rect}}]] + :page-id page-id + :id frame1-id + :parent-id uuid/zero + :frame-id uuid/zero + :obj (cts/setup-shape + {:id frame1-id + :use-for-thumbnail? true + :name "test-frame1" + :type :frame})} + {:type :add-obj + :page-id page-id + :id shape1-id + :parent-id frame1-id + :frame-id frame1-id + :obj (cts/setup-shape + {:id shape1-id + :name "test-shape1" + :type :rect})} + {:type :add-obj + :page-id page-id + :id frame2-id + :parent-id uuid/zero + :frame-id uuid/zero + :obj (cts/setup-shape + {:id frame2-id + :name "test-frame2" + :type :frame})} + {:type :add-obj + :page-id page-id + :id shape2-id + :parent-id frame2-id + :frame-id frame2-id + :obj (cts/setup-shape + {:id shape2-id + :name "test-shape2" + :type :rect})}]] ;; Update the file (th/update-file* {:file-id (:id file) :profile-id (:id prof) diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj index 14b0f72da7..c46a57d8c6 100644 --- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj +++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj @@ -7,6 +7,7 @@ (ns backend-tests.rpc-file-thumbnails-test (:require [app.common.uuid :as uuid] + [app.common.types.shape :as cts] [app.config :as cf] [app.db :as db] [app.rpc :as-alias rpc] @@ -46,11 +47,12 @@ :parent-id uuid/zero :frame-id uuid/zero :components-v2 true - :obj {:id shid - :name "Artboard" - :frame-id uuid/zero - :parent-id uuid/zero - :type :frame}}]) + :obj (cts/setup-shape + {:id shid + :name "Artboard" + :frame-id uuid/zero + :parent-id uuid/zero + :type :frame})}]) data1 {::th/type :create-file-object-thumbnail ::rpc/profile-id (:id profile) diff --git a/common/dev/user.clj b/common/dev/user.clj index 3f56de5f4e..e73de3a908 100644 --- a/common/dev/user.clj +++ b/common/dev/user.clj @@ -6,13 +6,18 @@ (ns user (:require + [app.common.schema :as sm] + [app.common.schema.desc-js-like :as smdj] + [app.common.schema.desc-native :as smdn] + [app.common.schema.generators :as sg] + [app.common.pprint :as pp] [clojure.java.io :as io] [clojure.pprint :refer [pprint print-table]] [clojure.repl :refer :all] [clojure.spec.alpha :as s] [clojure.spec.gen.alpha :as sgen] [clojure.test :as test] - [clojure.test.check.generators :as gen] + [clojure.test.check.generators :as tgen] [clojure.tools.namespace.repl :as repl] [clojure.walk :refer [macroexpand-all]] [criterium.core :as crit])) diff --git a/common/package.json b/common/package.json index 74298bf21b..1342e02f82 100644 --- a/common/package.json +++ b/common/package.json @@ -7,10 +7,10 @@ "luxon": "^3.3.0" }, "scripts": { - "compile-and-watch-test": "clojure -M:dev:shadow-cljs watch test", - "compile-test": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", - "run-test": "node target/test.js", - "test": "yarn run compile-test && yarn run run-test" + "test:watch": "clojure -M:dev:shadow-cljs watch test", + "test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", + "test:run": "node target/test.js", + "test": "yarn run test:compile && yarn run test:run" }, "devDependencies": { "shadow-cljs": "2.20.16", diff --git a/common/src/app/common/attrs.cljc b/common/src/app/common/attrs.cljc index 7bc8d5001e..ca27f3eb5d 100644 --- a/common/src/app/common/attrs.cljc +++ b/common/src/app/common/attrs.cljc @@ -6,7 +6,7 @@ (ns app.common.attrs (:require - [app.common.geom.shapes.transforms :as gtr] + [app.common.geom.shapes :as gsh] [app.common.math :as mth])) (defn- get-attr @@ -24,7 +24,8 @@ value (if-let [points (:points obj)] (if (not= points :multiple) - (let [rect (gtr/selection-rect [obj])] + ;; FIXME: consider using gsh/shape->rect ?? + (let [rect (gsh/shapes->rect [obj])] (if (= attr :ox) (:x rect) (:y rect))) :multiple) (get obj attr ::unset))) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 2a3320f6b1..7460bb9d7e 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -9,7 +9,7 @@ data resources." (:refer-clojure :exclude [read-string hash-map merge name update-vals parse-double group-by iteration concat mapcat - parse-uuid]) + parse-uuid max min]) #?(:cljs (:require-macros [app.common.data])) @@ -590,23 +590,47 @@ ([a] (mth/finite? a)) ([a b] - (and (mth/finite? a) - (mth/finite? b))) + (and ^boolean (mth/finite? a) + ^boolean (mth/finite? b))) ([a b c] - (and (mth/finite? a) - (mth/finite? b) - (mth/finite? c))) + (and ^boolean (mth/finite? a) + ^boolean (mth/finite? b) + ^boolean (mth/finite? c))) ([a b c d] - (and (mth/finite? a) - (mth/finite? b) - (mth/finite? c) - (mth/finite? d))) + (and ^boolean (mth/finite? a) + ^boolean (mth/finite? b) + ^boolean (mth/finite? c) + ^boolean (mth/finite? d))) ([a b c d & others] - (and (mth/finite? a) - (mth/finite? b) - (mth/finite? c) - (mth/finite? d) - (every? mth/finite? others)))) + (and ^boolean (mth/finite? a) + ^boolean (mth/finite? b) + ^boolean (mth/finite? c) + ^boolean (mth/finite? d) + ^boolean (every? mth/finite? others)))) + +(defn safe+ + [a b] + (if (mth/finite? a) (+ a b) a)) + +(defn max + ([a] a) + ([a b] (mth/max a b)) + ([a b c] (mth/max a b c)) + ([a b c d] (mth/max a b c d)) + ([a b c d e] (mth/max a b c d e)) + ([a b c d e f] (mth/max a b c d e f)) + ([a b c d e f & other] + (reduce max (mth/max a b c d e f) other))) + +(defn min + ([a] a) + ([a b] (mth/min a b)) + ([a b c] (mth/min a b c)) + ([a b c d] (mth/min a b c d)) + ([a b c d e] (mth/min a b c d e)) + ([a b c d e f] (mth/min a b c d e f)) + ([a b c d e f & other] + (reduce min (mth/min a b c d e f) other))) (defn check-num "Function that checks if a number is nil or nan. Will return 0 when not diff --git a/common/src/app/common/data/macros.cljc b/common/src/app/common/data/macros.cljc index 836656d938..6eabeacc9f 100644 --- a/common/src/app/common/data/macros.cljc +++ b/common/src/app/common/data/macros.cljc @@ -7,7 +7,7 @@ #_:clj-kondo/ignore (ns app.common.data.macros "Data retrieval & manipulation specific macros." - (:refer-clojure :exclude [get-in select-keys str with-open]) + (:refer-clojure :exclude [get-in select-keys str with-open min max]) #?(:cljs (:require-macros [app.common.data.macros])) (:require #?(:clj [clojure.core :as c] @@ -120,13 +120,10 @@ "A macro based, optimized variant of `get` that access the property directly on CLJS, on CLJ works as get." [obj prop] - ;; `(do - ;; (when-not (record? ~obj) - ;; (js/console.trace (pr-str ~obj))) - ;; (c/get ~obj ~prop))) (if (:ns &env) (list (symbol ".") (with-meta obj {:tag 'js}) (symbol (str "-" (c/name prop)))) - `(c/get ~obj ~prop))) + (list `c/get obj prop))) + (def ^:dynamic *assert-context* nil) @@ -154,7 +151,7 @@ (defmacro verify! ([expr] - `(assert! nil ~expr)) + `(verify! nil ~expr)) ([hint expr] (let [hint (cond (vector? hint) diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index 2efdb30bd4..0f56082f72 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -32,11 +32,6 @@ [& params] `(throw (error ~@params))) -;; FIXME deprecate -(defn try* - [f on-error] - (try (f) (catch #?(:clj Throwable :cljs :default) e (on-error e)))) - ;; http://clj-me.cgrand.net/2013/09/11/macros-closures-and-unexpected-object-retention/ ;; Explains the use of ^:once metadata diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/files/builder.cljc similarity index 84% rename from common/src/app/common/file_builder.cljc rename to common/src/app/common/files/builder.cljc index f8faff2359..cc73c6dd22 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/files/builder.cljc @@ -4,14 +4,13 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.file-builder - "A version parsing helper." +(ns app.common.files.builder (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.pages.changes :as ch] [app.common.pprint :as pp] @@ -25,9 +24,9 @@ [app.common.uuid :as uuid] [cuerdas.core :as str])) -(def root-frame uuid/zero) -(def conjv (fnil conj [])) -(def conjs (fnil conj #{})) +(def ^:private root-id uuid/zero) +(def ^:private conjv (fnil conj [])) +(def ^:private conjs (fnil conj #{})) (defn- commit-change ([file change] @@ -38,35 +37,33 @@ :or {add-container? false fail-on-spec? false}}] (let [component-id (:current-component-id file) - change (cond-> change - (and add-container? (some? component-id)) - (cond-> - :always - (assoc :component-id component-id) + change (cond-> change + (and add-container? (some? component-id)) + (-> (assoc :component-id component-id) + (cond-> (some? (:current-frame-id file)) + (assoc :frame-id (:current-frame-id file)))) - (some? (:current-frame-id file)) - (assoc :frame-id (:current-frame-id file))) + (and add-container? (nil? component-id)) + (assoc :page-id (:current-page-id file) + :frame-id (:current-frame-id file))) + valid? (ch/valid-change? change)] - (and add-container? (nil? component-id)) - (assoc :page-id (:current-page-id file) - :frame-id (:current-frame-id file)))] + (when-not valid? + (let [explain (sm/explain ::ch/change change)] + (pp/pprint (sm/humanize-data explain)) + (when fail-on-spec? + (ex/raise :type :assertion + :code :data-validation + :hint "invalid change" + ::sm/explain explain)))) - (when fail-on-spec? - (dm/verify! (ch/change? change))) + (cond-> file + valid? + (-> (update :changes conjv change) + (update :data ch/process-changes [change] false)) - (let [valid? (ch/change? change)] - (when-not valid? - (pp/pprint change {:level 100}) - (sm/pretty-explain ::ch/change change)) - - - (cond-> file - valid? - (-> (update :changes conjv change) - (update :data ch/process-changes [change] false)) - - (not valid?) - (update :errors conjv change)))))) + (not valid?) + (update :errors conjv change))))) (defn- lookup-objects ([file] @@ -91,50 +88,6 @@ (commit-change file change {:add-container? true :fail-on-spec? fail-on-spec?}))) -(defn setup-rect-selrect [{:keys [x y width height transform] :as obj}] - (when-not (d/num? x y width height) - (ex/raise :type :assertion - :code :invalid-condition - :hint "Coords not valid for object")) - - (let [rect (gsh/make-rect x y width height) - center (gsh/center-rect rect) - selrect (gsh/rect->selrect rect) - - points (-> (gsh/rect->points rect) - (gsh/transform-points center transform))] - - (-> obj - (assoc :selrect selrect) - (assoc :points points)))) - -(defn- setup-path-selrect - [{:keys [content center transform transform-inverse] :as obj}] - - (when (or (empty? content) (nil? center)) - (ex/raise :type :assertion - :code :invalid-condition - :hint "Path not valid")) - - (let [transform (gmt/transform-in center transform) - transform-inverse (gmt/transform-in center transform-inverse) - - content' (gsh/transform-content content transform-inverse) - selrect (gsh/content->selrect content') - points (-> (gsh/rect->points selrect) - (gsh/transform-points transform))] - - (-> obj - (dissoc :center) - (assoc :selrect selrect) - (assoc :points points)))) - -(defn- setup-selrect - [obj] - (if (= (:type obj) :path) - (setup-path-selrect obj) - (setup-rect-selrect obj))) - (defn- generate-name [type data] (if (= type :svg-raw) @@ -203,10 +156,10 @@ (assoc :current-page-id page-id) ;; Current frame-id - (assoc :current-frame-id root-frame) + (assoc :current-frame-id root-id) ;; Current parent stack we'll be nesting - (assoc :parent-stack [root-frame]) + (assoc :parent-stack [root-id]) ;; Last object id added (assoc :last-id nil)))) @@ -220,11 +173,8 @@ (clear-names))) (defn add-artboard [file data] - (let [obj (-> (cts/make-minimal-shape :frame) - (merge data) - (check-name file :frame) - (setup-selrect) - (d/without-nils))] + (let [obj (-> (cts/setup-shape (assoc data :type :frame)) + (check-name file :frame))] (-> file (commit-shape obj) (assoc :current-frame-id (:id obj)) @@ -237,19 +187,15 @@ parent (lookup-shape file parent-id) current-frame-id (or (:frame-id parent) (when (nil? (:current-component-id file)) - root-frame))] + root-id))] (-> file (assoc :current-frame-id current-frame-id) (update :parent-stack pop)))) (defn add-group [file data] (let [frame-id (:current-frame-id file) - selrect cts/empty-selrect - name (:name data) - obj (-> (cts/make-minimal-group frame-id selrect name) - (merge data) - (check-name file :group) - (d/without-nils))] + obj (-> (cts/setup-shape (assoc data :type :group :frame-id frame-id)) + (check-name file :group))] (-> file (commit-shape obj) (assoc :last-id (:id obj)) @@ -271,7 +217,7 @@ :id group-id} {:add-container? true}) - (:masked-group? group) + (:masked-group group) (let [mask (first children)] (commit-change file @@ -309,15 +255,8 @@ (defn add-bool [file data] (let [frame-id (:current-frame-id file) - name (:name data) - obj (-> {:id (uuid/next) - :type :bool - :name name - :shapes [] - :frame-id frame-id} - (merge data) - (check-name file :bool) - (d/without-nils))] + obj (-> (cts/setup-shape (assoc data :type :bool :frame-id frame-id)) + (check-name file :bool))] (-> file (commit-shape obj) (assoc :last-id (:id obj)) @@ -360,11 +299,8 @@ (update :parent-stack pop)))) (defn create-shape [file type data] - (let [obj (-> (cts/make-minimal-shape type) - (merge data) - (check-name file :type) - (setup-selrect) - (d/without-nils))] + (let [obj (-> (cts/setup-shape (assoc data :type type)) + (check-name file :type))] (-> file (commit-shape obj) (assoc :last-id (:id obj)) @@ -556,23 +492,33 @@ {:type :del-media :id id})))) + (defn start-component ([file data] (start-component file data :group)) ([file data root-type] - (let [selrect (or (gsh/make-selrect (:x data) (:y data) (:width data) (:height data)) - cts/empty-selrect) + ;; FIXME: data probably can be a shape instance, then we can use gsh/shape->rect + (let [selrect (or (grc/make-rect (:x data) (:y data) (:width data) (:height data)) + grc/empty-rect) name (:name data) path (:path data) main-instance-id (:main-instance-id data) main-instance-page (:main-instance-page data) - obj (-> (cts/make-shape root-type selrect data) - (dissoc :path - :main-instance-id - :main-instance-page - :main-instance-x - :main-instance-y) - (check-name file root-type) - (d/without-nils))] + attrs (-> data + (assoc :type root-type) + (assoc :x (:x selrect)) + (assoc :y (:y selrect)) + (assoc :width (:width selrect)) + (assoc :height (:height selrect)) + (assoc :selrect selrect) + (dissoc :path) + (dissoc :main-instance-id) + (dissoc :main-instance-page) + (dissoc :main-instance-x) + (dissoc :main-instance-y)) + + obj (-> (cts/setup-shape attrs) + (check-name file root-type))] + (-> file (commit-change {:type :add-component @@ -604,7 +550,7 @@ :id component-id :skip-undelete? true}) - (:masked-group? component) + (:masked-group component) (let [mask (first children)] (commit-change file @@ -660,7 +606,7 @@ (gpt/point main-instance-x main-instance-y) true - {:main-instance? true + {:main-instance true :force-id main-instance-id})] (as-> file $ (reduce #(commit-change %1 @@ -703,7 +649,7 @@ (gpt/point x y) components-v2 - #_{:main-instance? true + #_{:main-instance true :force-id main-instance-id})] (as-> file $ @@ -734,8 +680,8 @@ (defn update-object [file old-obj new-obj] (let [page-id (:current-page-id file) - new-obj (setup-selrect new-obj) - attrs (d/concat-set (keys old-obj) (keys new-obj)) + new-obj (cts/setup-shape new-obj) + attrs (d/concat-set (keys old-obj) (keys new-obj)) generate-operation (fn [changes attr] (let [old-val (get old-obj attr) diff --git a/common/src/app/common/files/defaults.cljc b/common/src/app/common/files/defaults.cljc new file mode 100644 index 0000000000..ea4a2ada32 --- /dev/null +++ b/common/src/app/common/files/defaults.cljc @@ -0,0 +1,9 @@ +;; 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.common.files.defaults) + +(def version 23) diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc new file mode 100644 index 0000000000..6ebd89479f --- /dev/null +++ b/common/src/app/common/files/helpers.cljc @@ -0,0 +1,46 @@ +;; 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.common.files.helpers + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.schema :as sm])) + +(defn get-used-names + "Return a set with the all unique names used in the + elements (any entity thas has a :name)" + [elements] + (let [elements (if (map? elements) + (vals elements) + elements)] + (into #{} (keep :name) elements))) + +(defn- extract-numeric-suffix + [basename] + (if-let [[_ p1 p2] (re-find #"(.*) ([0-9]+)$" basename)] + [p1 (+ 1 (d/parse-integer p2))] + [basename 1])) + +(defn generate-unique-name + "A unique name generator" + [used basename] + (dm/assert! + "expected a set of strings" + (sm/set-of-strings? used)) + + (dm/assert! + "expected a string for `basename`." + (string? basename)) + + (if-not (contains? used basename) + basename + (let [[prefix initial] (extract-numeric-suffix basename)] + (loop [counter initial] + (let [candidate (str prefix " " counter)] + (if (contains? used candidate) + (recur (inc counter)) + candidate)))))) diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/files/migrations.cljc similarity index 86% rename from common/src/app/common/pages/migrations.cljc rename to common/src/app/common/files/migrations.cljc index 5104237102..11d9480435 100644 --- a/common/src/app/common/pages/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -4,30 +4,30 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.pages.migrations +(ns app.common.files.migrations (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.defaults :refer [version]] [app.common.geom.matrix :as gmt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.text :as gsht] [app.common.logging :as log] [app.common.math :as mth] - [app.common.pages :as cp] + [app.common.pages.changes :as cpc] [app.common.pages.helpers :as cph] [app.common.types.shape :as cts] [app.common.uuid :as uuid] [cuerdas.core :as str])) -;; TODO: revisit this and rename to file-migrations +#?(:cljs (log/set-level! :info)) (defmulti migrate :version) -(log/set-level! :info) - (defn migrate-data - ([data] (migrate-data data cp/file-version)) + ([data] (migrate-data data version)) ([data to-version] (if (= (:version data) to-version) data @@ -74,7 +74,7 @@ (if-not (contains? shape :content) (let [content (gsp/segments->content (:segments shape) (:close? shape)) selrect (gsh/content->selrect content) - points (gsh/rect->points selrect)] + points (grc/rect->points selrect)] (-> shape (dissoc :segments) (dissoc :close?) @@ -87,17 +87,17 @@ (fix-frames-selrects [frame] (if (= (:id frame) uuid/zero) frame - (let [frame-rect (select-keys frame [:x :y :width :height])] + (let [selrect (gsh/shape->rect frame)] (-> frame - (assoc :selrect (gsh/rect->selrect frame-rect)) - (assoc :points (gsh/rect->points frame-rect)))))) + (assoc :selrect selrect) + (assoc :points (grc/rect->points selrect)))))) (fix-empty-points [shape] (let [shape (cond-> shape - (empty? (:selrect shape)) (cts/setup-rect-selrect))] + (empty? (:selrect shape)) (cts/setup-rect))] (cond-> shape (empty? (:points shape)) - (assoc :points (gsh/rect->points (:selrect shape)))))) + (assoc :points (grc/rect->points (:selrect shape)))))) (update-object [object] (cond-> object @@ -141,10 +141,10 @@ ;; Fixes issues with selrect/points for shapes with width/height = 0 (line-like paths)" (letfn [(fix-line-paths [shape] (if (= (:type shape) :path) - (let [{:keys [width height]} (gsh/points->rect (:points shape))] + (let [{:keys [width height]} (grc/points->rect (:points shape))] (if (or (mth/almost-zero? width) (mth/almost-zero? height)) (let [selrect (gsh/content->selrect (:content shape)) - points (gsh/rect->points selrect) + points (grc/rect->points selrect) transform (gmt/matrix) transform-inv (gmt/matrix)] (assoc shape @@ -242,7 +242,7 @@ (loop [data data] (let [changes (mapcat calculate-changes (:pages-index data))] (if (seq changes) - (recur (cp/process-changes data changes)) + (recur (cpc/process-changes data changes)) data))))) (defmethod migrate 10 @@ -462,5 +462,58 @@ (update :pages-index update-vals update-container) (update :components update-vals update-container)))) -;; TODO: pending to do a migration for delete already not used fill -;; and stroke props. This should be done for >1.14.x version. +(defmethod migrate 21 + [data] + (letfn [(update-object [object] + (-> object + (d/update-when :selrect grc/make-rect) + (cts/map->Shape))) + (update-container [container] + (d/update-when container :objects update-vals update-object))] + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defmethod migrate 22 + [data] + (letfn [(update-object [object] + (cond-> object + (nil? (:transform object)) + (assoc :transform (gmt/matrix)) + + (nil? (:transform-inverse object)) + (assoc :transform-inverse (gmt/matrix)))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(defmethod migrate 23 + [data] + (letfn [(update-object [object] + (cond-> object + (contains? object :main-instance?) + (-> (assoc :main-instance (:main-instance? object)) + (dissoc :main-instance?)) + + (contains? object :component-root?) + (-> (assoc :component-root (:component-root? object)) + (dissoc :component-root?)) + + (contains? object :remote-synced?) + (-> (assoc :remote-synced (:remote-synced? object)) + (dissoc :remote-synced?)) + + (contains? object :masked-group?) + (-> (assoc :masked-group (:masked-group? object)) + (dissoc :masked-group?)))) + + (update-container [container] + (d/update-when container :objects update-vals update-object))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) diff --git a/common/src/app/common/fressian.clj b/common/src/app/common/fressian.clj index f42f9130ea..282620698a 100644 --- a/common/src/app/common/fressian.clj +++ b/common/src/app/common/fressian.clj @@ -7,12 +7,8 @@ (ns app.common.fressian (:require [app.common.data :as d] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] [clojure.data.fressian :as fres]) (:import - app.common.geom.matrix.Matrix - app.common.geom.point.Point clojure.lang.Ratio java.io.ByteArrayInputStream java.io.ByteArrayOutputStream @@ -297,39 +293,3 @@ [data] (with-open [^ByteArrayInputStream input (ByteArrayInputStream. ^bytes data)] (-> input reader read!))) - -;; --- ADDITIONAL - -(add-handlers! - {:name "penpot/point" - :class app.common.geom.point.Point - :wfn (fn [n w ^Point o] - (write-tag! w n 1) - (write-list! w (List/of (.-x o) (.-y o)))) - :rfn (fn [^Reader rdr] - (let [^List x (read-object! rdr)] - (Point. (.get x 0) (.get x 1))))} - - {:name "penpot/matrix" - :class app.common.geom.matrix.Matrix - :wfn (fn [^String n ^Writer w o] - (write-tag! w n 1) - (write-list! w (List/of (.-a ^Matrix o) - (.-b ^Matrix o) - (.-c ^Matrix o) - (.-d ^Matrix o) - (.-e ^Matrix o) - (.-f ^Matrix o)))) - :rfn (fn [^Reader rdr] - (let [^List x (read-object! rdr)] - (Matrix. (.get x 0) (.get x 1) (.get x 2) (.get x 3) (.get x 4) (.get x 5))))}) - - -;; Backward compatibility for 1.19 with v1.20; - -(add-handlers! - {:name "penpot/geom/rect" - :rfn read-map-like} - {:name "penpot/shape" - :rfn read-map-like}) - diff --git a/common/src/app/common/geom/align.cljc b/common/src/app/common/geom/align.cljc index 7c2091cb93..a69ab06cdf 100644 --- a/common/src/app/common/geom/align.cljc +++ b/common/src/app/common/geom/align.cljc @@ -6,6 +6,7 @@ (ns app.common.geom.align (:require + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :refer [get-children]])) @@ -30,7 +31,7 @@ the shape with the given rectangle. If the shape is a group, move also all of its recursive children." [shape rect axis objects] - (let [wrapper-rect (gsh/selection-rect [shape]) + (let [wrapper-rect (gsh/shapes->rect [shape]) align-pos (calc-align-pos wrapper-rect rect axis) delta {:x (- (:x align-pos) (:x wrapper-rect)) :y (- (:y align-pos) (:y wrapper-rect))}] @@ -78,11 +79,11 @@ other-coord (if (= axis :horizontal) :y :x) size (if (= axis :horizontal) :width :height) ; The rectangle that wraps the whole selection - wrapper-rect (gsh/selection-rect shapes) + wrapper-rect (gsh/shapes->rect shapes) ; Sort shapes by the center point in the given axis - sorted-shapes (sort-by #(coord (gsh/center-shape %)) shapes) + sorted-shapes (sort-by #(coord (gsh/shape->center %)) shapes) ; Each shape wrapped in its own rectangle - wrapped-shapes (map #(gsh/selection-rect [%]) sorted-shapes) + wrapped-shapes (map #(gsh/shapes->rect [%]) sorted-shapes) ; The total space between shapes space (reduce - (size wrapper-rect) (map size wrapped-shapes)) unit-space (/ space (- (count wrapped-shapes) 1)) @@ -111,28 +112,32 @@ (defn adjust-to-viewport ([viewport srect] (adjust-to-viewport viewport srect nil)) ([viewport srect {:keys [padding] :or {padding 0}}] - (let [gprop (/ (:width viewport) (:height viewport)) - srect (-> srect - (update :x #(- % padding)) - (update :y #(- % padding)) - (update :width #(+ % padding padding)) - (update :height #(+ % padding padding))) - width (:width srect) + (let [gprop (/ (:width viewport) + (:height viewport)) + srect (-> srect + (update :x #(- % padding)) + (update :y #(- % padding)) + (update :width #(+ % padding padding)) + (update :height #(+ % padding padding))) + width (:width srect) height (:height srect) - lprop (/ width height)] + lprop (/ width height)] (cond - (> gprop lprop) - (let [width' (* (/ width lprop) gprop) - padding (/ (- width' width) 2)] - (-> srect - (update :x #(- % padding)) - (assoc :width width'))) + (> gprop lprop) + (let [width' (* (/ width lprop) gprop) + padding (/ (- width' width) 2)] + (-> srect + (update :x #(- % padding)) + (assoc :width width') + (grc/update-rect :position))) - (< gprop lprop) - (let [height' (/ (* height lprop) gprop) - padding (/ (- height' height) 2)] - (-> srect - (update :y #(- % padding)) - (assoc :height height'))) + (< gprop lprop) + (let [height' (/ (* height lprop) gprop) + padding (/ (- height' height) 2)] + (-> srect + (update :y #(- % padding)) + (assoc :height height') + (grc/update-rect :position))) - :else srect)))) + :else + (grc/update-rect srect :position))))) diff --git a/frontend/src/app/util/geom/grid.cljs b/common/src/app/common/geom/grid.cljc similarity index 99% rename from frontend/src/app/util/geom/grid.cljs rename to common/src/app/common/geom/grid.cljc index dea9569ad8..ef66418f04 100644 --- a/frontend/src/app/util/geom/grid.cljs +++ b/common/src/app/common/geom/grid.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.util.geom.grid +(ns app.common.geom.grid (:require [app.common.data :as d] [app.common.geom.point :as gpt] diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index c298154b08..a784b19427 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -8,34 +8,41 @@ (:require #?(:cljs [cljs.pprint :as pp] :clj [clojure.pprint :as pp]) + #?(:clj [app.common.fressian :as fres]) [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.math :as mth] + [app.common.record :as cr] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.schema.openapi :as-alias oapi] [app.common.spec :as us] - [clojure.spec.alpha :as s])) + [app.common.transit :as t] + [clojure.spec.alpha :as s]) + #?(:clj + (:import + java.util.List))) + (def precision 6) ;; --- Matrix Impl -(defrecord Matrix [^double a - ^double b - ^double c - ^double d - ^double e - ^double f] +(cr/defrecord Matrix [^double a + ^double b + ^double c + ^double d + ^double e + ^double f] Object - (toString [_] + (toString [this] (dm/fmt "matrix(%, %, %, %, %, %)" - (mth/to-fixed a precision) - (mth/to-fixed b precision) - (mth/to-fixed c precision) - (mth/to-fixed d precision) - (mth/to-fixed e precision) - (mth/to-fixed f precision)))) + (mth/to-fixed (.-a this) precision) + (mth/to-fixed (.-b this) precision) + (mth/to-fixed (.-c this) precision) + (mth/to-fixed (.-d this) precision) + (mth/to-fixed (.-e this) precision) + (mth/to-fixed (.-f this) precision)))) (defn matrix? "Return true if `v` is Matrix instance." @@ -45,9 +52,9 @@ (defn matrix "Create a new matrix instance." ([] - (Matrix. 1 0 0 1 0 0)) + (pos->Matrix 1 0 0 1 0 0)) ([a b c d e f] - (Matrix. a b c d e f))) + (pos->Matrix a b c d e f))) (def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?") @@ -94,7 +101,7 @@ (sg/small-double) (sg/small-double) (sg/small-double) ) - (sg/fmap #(apply ->Matrix %))) + (sg/fmap #(apply pos->Matrix %))) ::oapi/type "string" ::oapi/format "matrix" ::oapi/decode decode @@ -114,24 +121,54 @@ (s/def ::matrix (s/and ::matrix-attrs matrix?)) - (defn close? [^Matrix m1 ^Matrix m2] - (and (mth/close? (.-a m1) (.-a m2)) - (mth/close? (.-b m1) (.-b m2)) - (mth/close? (.-c m1) (.-c m2)) - (mth/close? (.-d m1) (.-d m2)) - (mth/close? (.-e m1) (.-e m2)) - (mth/close? (.-f m1) (.-f m2)))) + (and ^boolean (mth/close? (.-a m1) (.-a m2)) + ^boolean (mth/close? (.-b m1) (.-b m2)) + ^boolean (mth/close? (.-c m1) (.-c m2)) + ^boolean (mth/close? (.-d m1) (.-d m2)) + ^boolean (mth/close? (.-e m1) (.-e m2)) + ^boolean (mth/close? (.-f m1) (.-f m2)))) (defn unit? [^Matrix m1] - (and (some? m1) - (mth/close? (.-a m1) 1) - (mth/close? (.-b m1) 0) - (mth/close? (.-c m1) 0) - (mth/close? (.-d m1) 1) - (mth/close? (.-e m1) 0) - (mth/close? (.-f m1) 0))) + (and ^boolean (some? m1) + ^boolean (mth/close? (.-a m1) 1) + ^boolean (mth/close? (.-b m1) 0) + ^boolean (mth/close? (.-c m1) 0) + ^boolean (mth/close? (.-d m1) 1) + ^boolean (mth/close? (.-e m1) 0) + ^boolean (mth/close? (.-f m1) 0))) + +(defn multiply! + [^Matrix m1 ^Matrix m2] + (let [m1a (.-a m1) + m1b (.-b m1) + m1c (.-c m1) + m1d (.-d m1) + m1e (.-e m1) + m1f (.-f m1) + m2a (.-a m2) + m2b (.-b m2) + m2c (.-c m2) + m2d (.-d m2) + m2e (.-e m2) + m2f (.-f m2)] + #?@(:cljs + [(set! (.-a m1) (+ (* m1a m2a) (* m1c m2b))) + (set! (.-b m1) (+ (* m1b m2a) (* m1d m2b))) + (set! (.-c m1) (+ (* m1a m2c) (* m1c m2d))) + (set! (.-d m1) (+ (* m1b m2c) (* m1d m2d))) + (set! (.-e m1) (+ (* m1a m2e) (* m1c m2f) m1e)) + (set! (.-f m1) (+ (* m1b m2e) (* m1d m2f) m1f)) + m1] + :clj + [(pos->Matrix + (+ (* m1a m2a) (* m1c m2b)) + (+ (* m1b m2a) (* m1d m2b)) + (+ (* m1a m2c) (* m1c m2d)) + (+ (* m1b m2c) (* m1d m2d)) + (+ (* m1a m2e) (* m1c m2f) m1e) + (+ (* m1b m2e) (* m1d m2f) m1f))]))) (defn multiply ([^Matrix m1 ^Matrix m2] @@ -156,7 +193,7 @@ m2e (.-e m2) m2f (.-f m2)] - (Matrix. + (pos->Matrix (+ (* m1a m2a) (* m1c m2b)) (+ (* m1b m2a) (* m1d m2b)) (+ (* m1a m2c) (* m1c m2d)) @@ -165,51 +202,28 @@ (+ (* m1b m2e) (* m1d m2f) m1f))))) ([m1 m2 & others] - (reduce multiply (multiply m1 m2) others))) - -(defn multiply! - [^Matrix m1 ^Matrix m2] - (let [m1a (.-a m1) - m1b (.-b m1) - m1c (.-c m1) - m1d (.-d m1) - m1e (.-e m1) - m1f (.-f m1) - m2a (.-a m2) - m2b (.-b m2) - m2c (.-c m2) - m2d (.-d m2) - m2e (.-e m2) - m2f (.-f m2)] - #?@(:cljs [(set! (.-a m1) (+ (* m1a m2a) (* m1c m2b))) - (set! (.-b m1) (+ (* m1b m2a) (* m1d m2b))) - (set! (.-c m1) (+ (* m1a m2c) (* m1c m2d))) - (set! (.-d m1) (+ (* m1b m2c) (* m1d m2d))) - (set! (.-e m1) (+ (* m1a m2e) (* m1c m2f) m1e)) - (set! (.-f m1) (+ (* m1b m2e) (* m1d m2f) m1f)) - m1] - :clj [(Matrix. - (+ (* m1a m2a) (* m1c m2b)) - (+ (* m1b m2a) (* m1d m2b)) - (+ (* m1a m2c) (* m1c m2d)) - (+ (* m1b m2c) (* m1d m2d)) - (+ (* m1a m2e) (* m1c m2f) m1e) - (+ (* m1b m2e) (* m1d m2f) m1f))]))) + (reduce multiply! (multiply m1 m2) others))) (defn add-translate "Given two TRANSLATE matrixes (only e and f have significative values), combine them. Quicker than multiplying them, for this precise case." - ([{m1e :e m1f :f} {m2e :e m2f :f}] - (Matrix. 1 0 0 1 (+ m1e m2e) (+ m1f m2f))) + ([^Matrix m1 ^Matrix m2] + (let [m1e (dm/get-prop m1 :e) + m1f (dm/get-prop m1 :f) + m2e (dm/get-prop m2 :e) + m2f (dm/get-prop m2 :f)] + (pos->Matrix 1 0 0 1 (+ m1e m2e) (+ m1f m2f)))) ([m1 m2 & others] (reduce add-translate (add-translate m1 m2) others))) +;; FIXME: optimize? + (defn substract [{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f} {m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}] - (Matrix. + (pos->Matrix (- m1a m2a) (- m1b m2b) (- m1c m2c) (- m1d m2d) (- m1e m2e) (- m1f m2f))) @@ -221,13 +235,24 @@ (defn translate-matrix ([pt] - (assert (gpt/point? pt)) - (Matrix. 1 0 0 1 - (dm/get-prop pt :x) - (dm/get-prop pt :y))) + (dm/assert! (gpt/point? pt)) + (pos->Matrix 1 0 0 1 + (dm/get-prop pt :x) + (dm/get-prop pt :y))) ([x y] - (Matrix. 1 0 0 1 x y))) + (pos->Matrix 1 0 0 1 x y))) + + +(defn translate-matrix-neg + ([pt] + (dm/assert! (gpt/point? pt)) + (pos->Matrix 1 0 0 1 + (- (dm/get-prop pt :x)) + (- (dm/get-prop pt :y)))) + + ([x y] + (pos->Matrix 1 0 0 1 (- x) (- y)))) (defn scale-matrix ([pt center] @@ -235,10 +260,10 @@ sy (dm/get-prop pt :y) cx (dm/get-prop center :x) cy (dm/get-prop center :y)] - (Matrix. sx 0 0 sy (- cx (* cx sx)) (- cy (* cy sy))))) + (pos->Matrix sx 0 0 sy (- cx (* cx sx)) (- cy (* cy sy))))) ([pt] - (assert (gpt/point? pt)) - (Matrix. (dm/get-prop pt :x) 0 0 (dm/get-prop pt :y) 0 0))) + (dm/assert! (gpt/point? pt)) + (pos->Matrix (dm/get-prop pt :x) 0 0 (dm/get-prop pt :y) 0 0))) (defn rotate-matrix ([angle point] @@ -252,15 +277,15 @@ ns (- s) tx (+ (* c nx) (* ns ny) cx) ty (+ (* s nx) (* c ny) cy)] - (Matrix. c s ns c tx ty))) + (pos->Matrix c s ns c tx ty))) ([angle] (let [a (mth/radians angle)] - (Matrix. (mth/cos a) - (mth/sin a) - (- (mth/sin a)) - (mth/cos a) - 0 - 0)))) + (pos->Matrix (mth/cos a) + (mth/sin a) + (- (mth/sin a)) + (mth/cos a) + 0 + 0)))) (defn skew-matrix ([angle-x angle-y point] @@ -270,7 +295,7 @@ ([angle-x angle-y] (let [m1 (mth/tan (mth/radians angle-x)) m2 (mth/tan (mth/radians angle-y))] - (Matrix. 1 m2 m1 1 0 0)))) + (pos->Matrix 1 m2 m1 1 0 0)))) (defn rotate "Apply rotation transformation to the matrix." @@ -331,6 +356,7 @@ (translate (gpt/negate pt))) mtx)) +;; FIXME: performance (defn determinant "Determinant for the affinity transform" [{:keys [a b c d _ _]}] @@ -340,14 +366,14 @@ "Gets the inverse of the affinity transform `mtx`" [{:keys [a b c d e f] :as mtx}] (let [det (determinant mtx)] - (when-not (mth/almost-zero? det) + (when-not ^boolean (mth/almost-zero? det) (let [a' (/ d det) b' (/ (- b) det) c' (/ (- c) det) d' (/ a det) e' (/ (- (* c f) (* d e)) det) f' (/ (- (* b e) (* a f)) det)] - (Matrix. a' b' c' d' e' f'))))) + (pos->Matrix a' b' c' d' e' f'))))) (defn round [mtx] @@ -371,8 +397,41 @@ point)) (defn move? - [{:keys [a b c d _ _]}] - (and (mth/almost-zero? (- a 1)) - (mth/almost-zero? b) - (mth/almost-zero? c) - (mth/almost-zero? (- d 1)))) + [m] + (and ^boolean (mth/almost-zero? (- (dm/get-prop m :a) 1)) + ^boolean (mth/almost-zero? (dm/get-prop m :b)) + ^boolean (mth/almost-zero? (dm/get-prop m :c)) + ^boolean (mth/almost-zero? (- (dm/get-prop m :d) 1)))) + +#?(:clj + (fres/add-handlers! + {:name "penpot/matrix" + :class Matrix + :wfn (fn [n w o] + (fres/write-tag! w n 1) + (fres/write-list! w (List/of (.-a ^Matrix o) + (.-b ^Matrix o) + (.-c ^Matrix o) + (.-d ^Matrix o) + (.-e ^Matrix o) + (.-f ^Matrix o)))) + :rfn (fn [rdr] + (let [^List x (fres/read-object! rdr)] + (pos->Matrix (.get x 0) + (.get x 1) + (.get x 2) + (.get x 3) + (.get x 4) + (.get x 5))))})) + +(t/add-handlers! + {:id "matrix" + :class Matrix + :wfn #(into {} %) + :rfn (fn [m] + (pos->Matrix (get m :a) + (get m :b) + (get m :c) + (get m :d) + (get m :e) + (get m :f)))}) diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc index 949ae2ba4d..20680e8166 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -11,20 +11,26 @@ :clj [clojure.pprint :as pp]) #?(:cljs [cljs.core :as c] :clj [clojure.core :as c]) + #?(:clj [app.common.fressian :as fres]) [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.math :as mth] + [app.common.record :as cr] [app.common.schema :as sm] [app.common.schema.generators :as sg] [app.common.schema.openapi :as-alias oapi] [app.common.spec :as us] + [app.common.transit :as t] [clojure.spec.alpha :as s] - [cuerdas.core :as str])) + [cuerdas.core :as str]) + #?(:clj + (:import + java.util.List))) ;; --- Point Impl -(defrecord Point [x y]) +(cr/defrecord Point [x y]) (defn s [pt] @@ -57,7 +63,7 @@ (map->Point p) (if (string? p) (let [[x y] (->> (str/split p #",") (mapv parse-double))] - (Point. x y)) + (pos->Point x y)) p))) (encode [p] @@ -71,7 +77,7 @@ :description "Point" :error/message "expected a valid point" :gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int)) - (sg/fmap #(apply ->Point %))) + (sg/fmap #(apply pos->Point %))) ::oapi/type "string" ::oapi/format "point" ::oapi/decode decode @@ -85,7 +91,7 @@ (defn point "Create a Point instance." - ([] (Point. 0 0)) + ([] (pos->Point 0 0)) ([v] (cond (point? v) @@ -95,12 +101,12 @@ (point v v) (point-like? v) - (Point. (:x v) (:y v)) + (pos->Point (:x v) (:y v)) :else (ex/raise :hint "invalid arguments (on pointer constructor)" :value v))) ([x y] - (Point. x y))) + (pos->Point x y))) (defn close? [p1 p2] @@ -119,25 +125,29 @@ "Returns the addition of the supplied value to both coordinates of the point as a new point." [p1 p2] - (assert (and (point? p1) - (point? p2)) - "arguments should be pointer instance") - (Point. (+ (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (+ (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))) + (dm/assert! + "arguments should be point instance" + (and (point? p1) + (point? p2))) + + (pos->Point (+ (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (+ (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))) (defn subtract "Returns the subtraction of the supplied value to both coordinates of the point as a new point." [p1 p2] - (assert (and (point? p1) - (point? p2)) - "arguments should be pointer instance") - (Point. (- (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (- (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))) + (dm/assert! + "arguments should be pointer instance" + (and (point? p1) + (point? p2))) + + (pos->Point (- (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (- (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))) (defn multiply "Returns the subtraction of the supplied value to both @@ -146,20 +156,20 @@ (assert (and (point? p1) (point? p2)) "arguments should be pointer instance") - (Point. (* (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (* (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))) + (pos->Point (* (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (* (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))) (defn divide [p1 p2] (assert (and (point? p1) (point? p2)) "arguments should be pointer instance") - (Point. (/ (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (/ (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))) + (pos->Point (/ (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (/ (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))) (defn min ([] nil) @@ -168,10 +178,10 @@ (cond (nil? p1) p2 (nil? p2) p1 - :else (Point. (c/min (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (c/min (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))))) + :else (pos->Point (c/min (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (c/min (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))))) (defn max ([] nil) ([p1] p1) @@ -179,21 +189,21 @@ (cond (nil? p1) p2 (nil? p2) p1 - :else (Point. (c/max (dm/get-prop p1 :x) - (dm/get-prop p2 :x)) - (c/max (dm/get-prop p1 :y) - (dm/get-prop p2 :y)))))) + :else (pos->Point (c/max (dm/get-prop p1 :x) + (dm/get-prop p2 :x)) + (c/max (dm/get-prop p1 :y) + (dm/get-prop p2 :y)))))) (defn inverse [pt] (assert (point? pt) "point instance expected") - (Point. (/ 1.0 (dm/get-prop pt :x)) - (/ 1.0 (dm/get-prop pt :y)))) + (pos->Point (/ 1.0 (dm/get-prop pt :x)) + (/ 1.0 (dm/get-prop pt :y)))) (defn negate [pt] (assert (point? pt) "point instance expected") - (Point. (- (dm/get-prop pt :x)) - (- (dm/get-prop pt :y)))) + (pos->Point (- (dm/get-prop pt :x)) + (- (dm/get-prop pt :y)))) (defn distance "Calculate the distance between two points." @@ -217,8 +227,8 @@ (dm/get-prop p2 :x)) dy (- (dm/get-prop p1 :y) (dm/get-prop p2 :y))] - (Point. (mth/abs dx) - (mth/abs dy)))) + (pos->Point (mth/abs dx) + (mth/abs dy)))) (defn length [pt] @@ -285,8 +295,8 @@ (assert (number? angle) "expected number") (let [len (length p) angle (mth/radians angle)] - (Point. (* (mth/cos angle) len) - (* (mth/sin angle) len)))) + (pos->Point (* (mth/cos angle) len) + (* (mth/sin angle) len)))) (defn quadrant "Return the quadrant of the angle of the point." @@ -306,22 +316,21 @@ ([pt decimals] (assert (point? pt) "expected point instance") (assert (number? decimals) "expected number instance") - (Point. (mth/precision (dm/get-prop pt :x) decimals) - (mth/precision (dm/get-prop pt :y) decimals)))) + (pos->Point (mth/precision (dm/get-prop pt :x) decimals) + (mth/precision (dm/get-prop pt :y) decimals)))) (defn round-step "Round the coordinates to the closest half-point" [pt step] (assert (point? pt) "expected point instance") - (Point. (mth/round (dm/get-prop pt :x) step) - (mth/round (dm/get-prop pt :y) step))) + (pos->Point (mth/round (dm/get-prop pt :x) step) + (mth/round (dm/get-prop pt :y) step))) (defn transform "Transform a point applying a matrix transformation." [p m] (when (point? p) - (if (nil? m) - p + (if (some? m) (let [x (dm/get-prop p :x) y (dm/get-prop p :y) a (dm/get-prop m :a) @@ -330,18 +339,51 @@ d (dm/get-prop m :d) e (dm/get-prop m :e) f (dm/get-prop m :f)] - (Point. (+ (* x a) (* y c) e) - (+ (* x b) (* y d) f)))))) + (pos->Point (+ (* x a) (* y c) e) + (+ (* x b) (* y d) f))) + p))) +(defn transform! + [p m] + + (dm/assert! + "expected valid rect and matrix instances" + (and (some? p) (some? m))) + + (let [x (dm/get-prop p :x) + y (dm/get-prop p :y) + a (dm/get-prop m :a) + b (dm/get-prop m :b) + c (dm/get-prop m :c) + d (dm/get-prop m :d) + e (dm/get-prop m :e) + f (dm/get-prop m :f)] + #?(:clj + (pos->Point (+ (* x a) (* y c) e) + (+ (* x b) (* y d) f)) + :cljs + (do + (set! (.-x p) (+ (* x a) (* y c) e)) + (set! (.-y p) (+ (* x b) (* y d) f)) + p)))) + +(defn matrix->point + "Returns a result of transform an identity point with the provided + matrix instance" + [m] + (let [e (dm/get-prop m :e) + f (dm/get-prop m :f)] + (pos->Point e f))) + ;; Vector functions (defn to-vec [p1 p2] (subtract p2 p1)) (defn scale [p scalar] - (Point. (* (dm/get-prop p :x) scalar) - (* (dm/get-prop p :y) scalar))) + (pos->Point (* (dm/get-prop p :x) scalar) + (* (dm/get-prop p :y) scalar))) (defn dot [p1 p2] @@ -354,14 +396,14 @@ [p1] (let [p-length (length p1)] (if (mth/almost-zero? p-length) - (Point. 0 0) - (Point. (/ (dm/get-prop p1 :x) p-length) - (/ (dm/get-prop p1 :y) p-length))))) + (pos->Point 0 0) + (pos->Point (/ (dm/get-prop p1 :x) p-length) + (/ (dm/get-prop p1 :y) p-length))))) (defn perpendicular [pt] - (Point. (- (dm/get-prop pt :y)) - (dm/get-prop pt :x))) + (pos->Point (- (dm/get-prop pt :y)) + (dm/get-prop pt :x))) (defn project "V1 perpendicular projection on vector V2" @@ -412,7 +454,7 @@ [p1 p2 t] (let [x (mth/lerp (dm/get-prop p1 :x) (dm/get-prop p2 :x) t) y (mth/lerp (dm/get-prop p1 :y) (dm/get-prop p2 :y) t)] - (Point. x y))) + (pos->Point x y))) (defn rotate "Rotates the point around center with an angle" @@ -434,7 +476,7 @@ y (+ (* sa (- px cx)) (* ca (- py cy)) cy)] - (Point. x y))) + (pos->Point x y))) (defn scale-from "Moves a point in the vector that creates with center with a scale @@ -450,10 +492,10 @@ [p] (let [x (dm/get-prop p :x) y (dm/get-prop p :y)] - (Point. (if (mth/almost-zero? x) 0.001 x) - (if (mth/almost-zero? y) 0.001 y)))) - + (pos->Point (if (mth/almost-zero? x) 0.001 x) + (if (mth/almost-zero? y) 0.001 y)))) +;; FIXME: perfromance (defn abs [point] (-> point @@ -464,3 +506,19 @@ (defmethod pp/simple-dispatch Point [obj] (pr obj)) +#?(:clj + (fres/add-handlers! + {:name "penpot/point" + :class Point + :wfn (fn [n w ^Point o] + (fres/write-tag! w n 1) + (fres/write-list! w (List/of (.-x o) (.-y o)))) + :rfn (fn [rdr] + (let [^List x (fres/read-object! rdr)] + (pos->Point (.get x 0) (.get x 1))))})) + +(t/add-handlers! + {:id "point" + :class Point + :wfn #(into {} %) + :rfn map->Point}) diff --git a/common/src/app/common/geom/rect.cljc b/common/src/app/common/geom/rect.cljc new file mode 100644 index 0000000000..cd9bf6f066 --- /dev/null +++ b/common/src/app/common/geom/rect.cljc @@ -0,0 +1,355 @@ +;; 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.common.geom.rect + (:require + #?(:clj [app.common.fressian :as fres]) + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.point :as gpt] + [app.common.math :as mth] + [app.common.record :as rc] + [app.common.transit :as t])) + +(rc/defrecord Rect [x y width height x1 y1 x2 y2]) + +(defn rect? + [o] + (instance? Rect o)) + +#?(:clj + (fres/add-handlers! + {:name "penpot/geom/rect" + :class Rect + :wfn fres/write-map-like + :rfn (comp map->Rect fres/read-map-like)})) + +(t/add-handlers! + {:id "rect" + :class Rect + :wfn #(into {} %) + :rfn map->Rect}) + +(defn make-rect + ([] (make-rect 0 0 0.01 0.01)) + ([data] + (if (rect? data) + data + (let [{:keys [x y width height]} data] + (make-rect (d/nilv x 0) + (d/nilv y 0) + (d/nilv width 0.01) + (d/nilv height 0.01))))) + + ([p1 p2] + (dm/assert! + "expected `p1` and `p2` to be points" + (and (gpt/point? p1) + (gpt/point? p2))) + + (let [xp1 (dm/get-prop p1 :x) + yp1 (dm/get-prop p1 :y) + xp2 (dm/get-prop p2 :x) + yp2 (dm/get-prop p2 :y) + x1 (mth/min xp1 xp2) + y1 (mth/min yp1 yp2) + x2 (mth/max xp1 xp2) + y2 (mth/max yp1 yp2)] + (make-rect x1 y1 (- x2 x1) (- y2 y1)))) + + ([x y width height] + (when (d/num? x y width height) + (let [w (mth/max width 0.01) + h (mth/max height 0.01)] + (pos->Rect x y w h x y (+ x w) (+ y h)))))) + +(def empty-rect + (make-rect 0 0 0.01 0.01)) + +(defn update-rect + [rect type] + (case type + :size + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (assoc rect + :x2 (+ x w) + :y2 (+ y h))) + + :corners + (let [x1 (dm/get-prop rect :x1) + y1 (dm/get-prop rect :y1) + x2 (dm/get-prop rect :x2) + y2 (dm/get-prop rect :y2)] + (assoc rect + :x (mth/min x1 x2) + :y (mth/min y1 y2) + :width (mth/abs (- x2 x1)) + :height (mth/abs (- y2 y1)))) + + :position + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (assoc rect + :x1 x + :y1 y + :x2 (+ x w) + :y2 (+ y h))))) + +(defn update-rect! + [rect type] + (case type + (:size :position) + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (rc/assoc! rect + :x1 x + :y1 y + :x2 (+ x w) + :y2 (+ y h))) + + :corners + (let [x1 (dm/get-prop rect :x1) + y1 (dm/get-prop rect :y1) + x2 (dm/get-prop rect :x2) + y2 (dm/get-prop rect :y2)] + (rc/assoc! rect + :x (mth/min x1 x2) + :y (mth/min y1 y2) + :width (mth/abs (- x2 x1)) + :height (mth/abs (- y2 y1)))))) + +(defn close-rect? + [rect1 rect2] + + (dm/assert! + "expected two rects" + (and (rect? rect1) + (rect? rect2))) + + (and ^boolean (mth/close? (dm/get-prop rect1 :x) + (dm/get-prop rect2 :x)) + ^boolean (mth/close? (dm/get-prop rect1 :y) + (dm/get-prop rect2 :y)) + ^boolean (mth/close? (dm/get-prop rect1 :width) + (dm/get-prop rect2 :width)) + ^boolean (mth/close? (dm/get-prop rect1 :height) + (dm/get-prop rect2 :height)))) + +(defn rect->points + [rect] + (dm/assert! + "expected rect instance" + (rect? rect)) + + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (when (d/num? x y) + (let [w (mth/max w 0.01) + h (mth/max h 0.01)] + [(gpt/point x y) + (gpt/point (+ x w) y) + (gpt/point (+ x w) (+ y h)) + (gpt/point x (+ y h))])))) + +(defn rect->point + "Extract the position part of the rect" + [rect] + (gpt/point (dm/get-prop rect :x) + (dm/get-prop rect :y))) + +(defn rect->center + [rect] + (dm/assert! (rect? rect)) + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (when (d/num? x y w h) + (gpt/point (+ x (/ w 2.0)) + (+ y (/ h 2.0)))))) + +(defn rect->lines + [rect] + (dm/assert! (rect? rect)) + + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (when (d/num? x y) + (let [w (mth/max w 0.01) + h (mth/max h 0.01)] + [[(gpt/point x y) (gpt/point (+ x w) y)] + [(gpt/point (+ x w) y) (gpt/point (+ x w) (+ y h))] + [(gpt/point (+ x w) (+ y h)) (gpt/point x (+ y h))] + [(gpt/point x (+ y h)) (gpt/point x y)]])))) + +(defn points->rect + [points] + (when-let [points (seq points)] + (loop [minx ##Inf + miny ##Inf + maxx ##-Inf + maxy ##-Inf + pts points] + (if-let [pt (first pts)] + (let [x (dm/get-prop pt :x) + y (dm/get-prop pt :y)] + (recur (mth/min minx x) + (mth/min miny y) + (mth/max maxx x) + (mth/max maxy y) + (rest pts))) + (when (d/num? minx miny maxx maxy) + (make-rect minx miny (- maxx minx) (- maxy miny))))))) + +;; FIXME: measure performance +(defn bounds->rect + [[pa pb pc pd]] + (let [ax (dm/get-prop pa :x) + ay (dm/get-prop pa :y) + bx (dm/get-prop pb :x) + by (dm/get-prop pb :y) + cx (dm/get-prop pc :x) + cy (dm/get-prop pc :y) + dx (dm/get-prop pd :x) + dy (dm/get-prop pd :y) + minx (mth/min ax bx cx dx) + miny (mth/min ay by cy dy) + maxx (mth/max ax bx cx dx) + maxy (mth/max ay by cy dy)] + (when (d/num? minx miny maxx maxy) + (make-rect minx miny (- maxx minx) (- maxy miny))))) + +(def ^:private xf-keep-x (keep #(dm/get-prop % :x))) +(def ^:private xf-keep-y (keep #(dm/get-prop % :y))) +(def ^:private xf-keep-x2 (keep #(dm/get-prop % :x2))) +(def ^:private xf-keep-y2 (keep #(dm/get-prop % :y2))) + +(defn squared-points + [points] + (when (d/not-empty? points) + (let [minx (transduce xf-keep-x d/min ##Inf points) + miny (transduce xf-keep-y d/min ##Inf points) + maxx (transduce xf-keep-x2 d/max ##-Inf points) + maxy (transduce xf-keep-y2 d/max ##-Inf points)] + (when (d/num? minx miny maxx maxy) + [(gpt/point minx miny) + (gpt/point maxx miny) + (gpt/point maxx maxy) + (gpt/point minx maxy)])))) + +(defn join-rects [rects] + (when (seq rects) + (let [minx (transduce xf-keep-x d/min ##Inf rects) + miny (transduce xf-keep-y d/min ##Inf rects) + maxx (transduce xf-keep-x2 d/max ##-Inf rects) + maxy (transduce xf-keep-y2 d/max ##-Inf rects)] + (when (d/num? minx miny maxx maxy) + (make-rect minx miny (- maxx minx) (- maxy miny)))))) + +(defn center->rect + [point w h] + (when (some? point) + (let [x (dm/get-prop point :x) + y (dm/get-prop point :y)] + (when (d/num? x y w h) + (make-rect (- x (/ w 2)) + (- y (/ h 2)) + w + h))))) + +(defn s= + [a b] + (mth/almost-zero? (- a b))) + +;; FIXME: performance +(defn overlaps-rects? + "Check for two rects to overlap. Rects won't overlap only if + one of them is fully to the left or the top" + [rect-a rect-b] + + (let [x1a (:x rect-a) + y1a (:y rect-a) + x2a (+ (:x rect-a) (:width rect-a)) + y2a (+ (:y rect-a) (:height rect-a)) + + x1b (:x rect-b) + y1b (:y rect-b) + x2b (+ (:x rect-b) (:width rect-b)) + y2b (+ (:y rect-b) (:height rect-b))] + + (and (or (> x2a x1b) (s= x2a x1b)) + (or (>= x2b x1a) (s= x2b x1a)) + (or (<= y1b y2a) (s= y1b y2a)) + (or (<= y1a y2b) (s= y1a y2b))))) + +(defn contains-point? + [rect point] + (assert (gpt/point? point)) + (let [x1 (:x rect) + y1 (:y rect) + x2 (+ (:x rect) (:width rect)) + y2 (+ (:y rect) (:height rect)) + + px (:x point) + py (:y point)] + + (and (or (> px x1) (s= px x1)) + (or (< px x2) (s= px x2)) + (or (> py y1) (s= py y1)) + (or (< py y2) (s= py y2))))) + +(defn contains-rect? + "Check if a rect srb is contained inside sra" + [sra srb] + (let [ax1 (dm/get-prop sra :x1) + ax2 (dm/get-prop sra :x2) + ay1 (dm/get-prop sra :y1) + ay2 (dm/get-prop sra :y2) + bx1 (dm/get-prop srb :x1) + bx2 (dm/get-prop srb :x2) + by1 (dm/get-prop srb :y1) + by2 (dm/get-prop srb :y2)] + (and (>= bx1 ax1) + (<= bx2 ax2) + (>= by1 ay1) + (<= by2 ay2)))) + +(defn corners->rect + ([p1 p2] + (corners->rect (:x p1) (:y p1) (:x p2) (:y p2))) + ([xp1 yp1 xp2 yp2] + (make-rect (mth/min xp1 xp2) + (mth/min yp1 yp2) + (abs (- xp1 xp2)) + (abs (- yp1 yp2))))) + +(defn clip-rect + [selrect bounds] + (when (rect? selrect) + (dm/assert! (rect? bounds)) + (let [x1 (dm/get-prop selrect :x1) + y1 (dm/get-prop selrect :y1) + x2 (dm/get-prop selrect :x2) + y2 (dm/get-prop selrect :y2) + bx1 (dm/get-prop bounds :x1) + by1 (dm/get-prop bounds :y1) + bx2 (dm/get-prop bounds :x2) + by2 (dm/get-prop bounds :y2)] + (corners->rect (mth/max bx1 x1) + (mth/max by1 y1) + (mth/min bx2 x2) + (mth/min by2 y2))))) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index fabefe36b1..af4f491940 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.constraints :as gct] @@ -16,28 +17,30 @@ [app.common.geom.shapes.intersect :as gsi] [app.common.geom.shapes.modifiers :as gsm] [app.common.geom.shapes.path :as gsp] - [app.common.geom.shapes.rect :as gpr] - [app.common.geom.shapes.text :as gst] [app.common.geom.shapes.transforms :as gtr] [app.common.math :as mth])) ;; --- Outer Rect -(defn selection-rect - "Returns a rect that contains all the shapes and is aware of the - rotation of each shape. Mainly used for multiple selection." - [shapes] - (->> shapes - (map (comp gpr/points->selrect :points)) - (gpr/join-selrects))) - (defn translate-to-frame - [shape {:keys [x y]}] - (gtr/move shape (gpt/negate (gpt/point x y))) ) + [shape frame] + (->> (gpt/point (- (dm/get-prop frame :x)) + (- (dm/get-prop frame :y))) + (gtr/move shape))) (defn translate-from-frame - [shape {:keys [x y]}] - (gtr/move shape (gpt/point x y)) ) + [shape frame] + (gtr/move shape (gpt/point (dm/get-prop frame :x) + (dm/get-prop frame :y)))) + +(defn shape->rect + [shape] + (let [x (dm/get-prop shape :x) + y (dm/get-prop shape :y) + w (dm/get-prop shape :width) + h (dm/get-prop shape :height)] + (when (d/num? x y w h) + (grc/make-rect x y w h)))) ;; --- Helpers @@ -45,7 +48,7 @@ "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))) + (grc/points->rect (:points shape))) (defn left-bound "Returns the lowest x coord of the shape BEFORE applying transformations." @@ -82,21 +85,38 @@ (update :width (comp inc inc)) (update :height (comp inc inc)))))) -(defn selrect->areas [bounds selrect] - (let [{bound-x1 :x1 bound-x2 :x2 bound-y1 :y1 bound-y2 :y2} bounds - {sr-x1 :x1 sr-x2 :x2 sr-y1 :y1 sr-y2 :y2} selrect] - {:left (gpr/corners->selrect bound-x1 sr-y1 sr-x1 sr-y2) - :top (gpr/corners->selrect sr-x1 bound-y1 sr-x2 sr-y1) - :right (gpr/corners->selrect sr-x2 sr-y1 bound-x2 sr-y2) - :bottom (gpr/corners->selrect sr-x1 sr-y2 sr-x2 bound-y2)})) +(defn get-areas + [bounds selrect] + (let [bound-x1 (dm/get-prop bounds :x1) + bound-x2 (dm/get-prop bounds :x2) + bound-y1 (dm/get-prop bounds :y1) + bound-y2 (dm/get-prop bounds :y2) + sr-x1 (dm/get-prop selrect :x1) + sr-x2 (dm/get-prop selrect :x2) + sr-y1 (dm/get-prop selrect :y1) + sr-y2 (dm/get-prop selrect :y2)] + {:left (grc/corners->rect bound-x1 sr-y1 sr-x1 sr-y2) + :top (grc/corners->rect sr-x1 bound-y1 sr-x2 sr-y1) + :right (grc/corners->rect sr-x2 sr-y1 bound-x2 sr-y2) + :bottom (grc/corners->rect sr-x1 sr-y2 sr-x2 bound-y2)})) -(defn distance-selrect [selrect other] - (let [{:keys [x1 y1]} other - {:keys [x2 y2]} selrect] +(defn distance-selrect + [selrect other] + + (dm/assert! + (and (grc/rect? selrect) + (grc/rect? other))) + + (let [x1 (dm/get-prop other :x1) + y1 (dm/get-prop other :y1) + x2 (dm/get-prop selrect :x2) + y2 (dm/get-prop selrect :y2)] (gpt/point (- x1 x2) (- y1 y2)))) (defn distance-shapes [shape other] - (distance-selrect (:selrect shape) (:selrect other))) + (distance-selrect + (dm/get-prop shape :selrect) + (dm/get-prop other :selrect))) (defn close-attrs? "Compares two shapes attributes to see if they are equal or almost @@ -131,27 +151,11 @@ (= val1 val2))))) ;; EXPORTS -(dm/export gco/center-shape) -(dm/export gco/center-selrect) -(dm/export gco/center-rect) -(dm/export gco/center-points) +(dm/export gco/shape->center) +(dm/export gco/shapes->rect) +(dm/export gco/points->center) (dm/export gco/transform-points) -(dm/export gpr/make-rect) -(dm/export gpr/make-selrect) -(dm/export gpr/rect->selrect) -(dm/export gpr/rect->points) -(dm/export gpr/points->selrect) -(dm/export gpr/points->rect) -(dm/export gpr/center->rect) -(dm/export gpr/center->selrect) -(dm/export gpr/join-rects) -(dm/export gpr/join-selrects) -(dm/export gpr/contains-selrect?) -(dm/export gpr/contains-point?) -(dm/export gpr/close-selrect?) -(dm/export gpr/clip-selrect) - (dm/export gtr/move) (dm/export gtr/absolute-move) (dm/export gtr/transform-matrix) @@ -173,6 +177,7 @@ (dm/export gct/calc-child-modifiers) ;; PATHS +;; FIXME: rename (dm/export gsp/content->selrect) (dm/export gsp/transform-content) (dm/export gsp/open-path?) @@ -196,6 +201,3 @@ ;; Modifiers (dm/export gsm/set-objects-modifiers) - -;; Text -(dm/export gst/position-data-selrect) diff --git a/common/src/app/common/geom/shapes/bounds.cljc b/common/src/app/common/geom/shapes/bounds.cljc index 9bb6686f69..125122761f 100644 --- a/common/src/app/common/geom/shapes/bounds.cljc +++ b/common/src/app/common/geom/shapes/bounds.cljc @@ -7,32 +7,29 @@ (ns app.common.geom.shapes.bounds (:require [app.common.data :as d] - [app.common.geom.shapes.rect :as gsr] + [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.math :as mth] [app.common.pages.helpers :as cph])) (defn shape-stroke-margin [shape stroke-width] - (if (= (:type shape) :path) + (if (cph/path-shape? shape) ;; TODO: Calculate with the stroke offset (not implemented yet (mth/sqrt (* 2 stroke-width stroke-width)) (- (mth/sqrt (* 2 stroke-width stroke-width)) stroke-width))) -(defn blur-filters [type value] - (->> [value] - (remove :hidden) - (filter #(= (:type %) type)) - (map #(hash-map :id (str "filter_" (:id %)) - :type (:type %) - :params %)))) - -(defn shadow-filters [type filters] - (->> filters - (remove :hidden) - (filter #(= (:style %) type)) - (map #(hash-map :id (str "filter_" (:id %)) - :type (:style %) - :params %)))) +(defn- apply-filters + [type filters] + (sequence + (comp + (remove :hidden) + (filter #(= (:style %) type)) + (map (fn [item] + {:id (dm/str "filter_" (:id item)) + :type type + :params item}))) + filters)) (defn shape->filters [shape] @@ -41,93 +38,112 @@ ;; Background blur won't work in current SVG specification ;; We can revisit this in the future - #_(->> shape :blur (blur-filters :background-blur)) + #_(->> shape :blur (into []) (blur-filters :background-blur)) - (->> shape :shadow (shadow-filters :drop-shadow)) + (->> shape :shadow (apply-filters :drop-shadow)) [{:id "shape" :type :blend-filters}] - (->> shape :shadow (shadow-filters :inner-shadow)) - (->> shape :blur (blur-filters :layer-blur)))) + (->> shape :shadow (apply-filters :inner-shadow)) + (->> shape :blur (into []) (apply-filters :layer-blur)))) -(defn calculate-filter-bounds [{:keys [x y width height]} filter-entry] - (let [{:keys [offset-x offset-y blur spread] :or {offset-x 0 offset-y 0 blur 0 spread 0}} (:params filter-entry) - filter-x (min x (+ x offset-x (- spread) (- blur) -5)) - filter-y (min y (+ y offset-y (- spread) (- blur) -5)) - filter-width (+ width (mth/abs offset-x) (* spread 2) (* blur 2) 10) - filter-height (+ height (mth/abs offset-y) (* spread 2) (* blur 2) 10)] - (gsr/make-selrect filter-x filter-y filter-width filter-height))) +(defn- calculate-filter-bounds + [selrect filter-entry] + (let [x (dm/get-prop selrect :x) + y (dm/get-prop selrect :y) + w (dm/get-prop selrect :width) + h (dm/get-prop selrect :height) + + {:keys [offset-x offset-y blur spread] + :or {offset-x 0 offset-y 0 blur 0 spread 0}} + (:params filter-entry) + + filter-x (mth/min x (+ x offset-x (- spread) (- blur) -5)) + filter-y (mth/min y (+ y offset-y (- spread) (- blur) -5)) + filter-w (+ w (mth/abs offset-x) (* spread 2) (* blur 2) 10) + filter-h (+ h (mth/abs offset-y) (* spread 2) (* blur 2) 10)] + (grc/make-rect filter-x filter-y filter-w filter-h))) (defn get-rect-filter-bounds [selrect filters blur-value] - (let [filter-bounds (->> filters - (filter #(= :drop-shadow (:type %))) - (map (partial calculate-filter-bounds selrect)) - (concat [selrect]) - (gsr/join-selrects)) - delta-blur (* blur-value 2) - - result - (-> filter-bounds - (update :x - delta-blur) - (update :y - delta-blur) - (update :x1 - delta-blur) - (update :y1 - delta-blur) - (update :x2 + delta-blur) - (update :y2 + delta-blur) - (update :width + (* delta-blur 2)) - (update :height + (* delta-blur 2)))] - - result)) + (let [bounds-xf (comp + (filter #(= :drop-shadow (:type %))) + (map (partial calculate-filter-bounds selrect))) + delta-blur (* blur-value 2)] + (-> (into [selrect] bounds-xf filters) + (grc/join-rects) + (update :x - delta-blur) + (update :y - delta-blur) + (update :x1 - delta-blur) + (update :y1 - delta-blur) + (update :x2 + delta-blur) + (update :y2 + delta-blur) + (update :width + (* delta-blur 2)) + (update :height + (* delta-blur 2))))) (defn get-shape-filter-bounds - ([shape] - (let [svg-root? (and (= :svg-raw (:type shape)) (not= :svg (get-in shape [:content :tag])))] - (if svg-root? - (:selrect shape) - - (let [filters (shape->filters shape) - blur-value (or (-> shape :blur :value) 0)] - (get-rect-filter-bounds (-> shape :points gsr/points->selrect) filters blur-value)))))) + [shape] + (if (and (cph/svg-raw-shape? shape) + (not= :svg (dm/get-in shape [:content :tag]))) + (dm/get-prop shape :selrect) + (let [filters (shape->filters shape) + blur-value (or (-> shape :blur :value) 0) + srect (-> (dm/get-prop shape :points) + (grc/points->rect))] + (get-rect-filter-bounds srect filters blur-value)))) (defn calculate-padding ([shape] (calculate-padding shape false)) - ([shape ignore-margin?] - (let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center) - :center (/ (:stroke-width % 0) 2) - :outer (:stroke-width % 0) - 0) (:strokes shape))) + (let [strokes (:strokes shape) - margin (if ignore-margin? - 0 - (apply max 0 (map #(shape-stroke-margin % stroke-width) (:strokes shape)))) + stroke-width + (->> strokes + (map #(case (get % :stroke-alignment :center) + :center (/ (:stroke-width % 0) 2) + :outer (:stroke-width % 0) + 0)) + (reduce d/max 0)) - shadow-width (apply max 0 (map #(case (:style % :drop-shadow) - :drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10) - 0) (:shadow shape))) + margin + (if ignore-margin? + 0 + (->> strokes + (map #(shape-stroke-margin % stroke-width)) + (reduce d/max 0))) - shadow-height (apply max 0 (map #(case (:style % :drop-shadow) - :drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10) - 0) (:shadow shape)))] + shadow-width + (->> (:shadow shape) + (map #(case (:style % :drop-shadow) + :drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10) + 0)) + (reduce d/max 0)) + + shadow-height + (->> (:shadow shape) + (map #(case (:style % :drop-shadow) + :drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10) + 0)) + (reduce d/max 0))] {:horizontal (+ stroke-width margin shadow-width) :vertical (+ stroke-width margin shadow-height)}))) (defn- add-padding [bounds padding] - (-> bounds - (update :x - (:horizontal padding)) - (update :x1 - (:horizontal padding)) - (update :x2 + (:horizontal padding)) - (update :y - (:vertical padding)) - (update :y1 - (:vertical padding)) - (update :y2 + (:vertical padding)) - (update :width + (* 2 (:horizontal padding))) - (update :height + (* 2 (:vertical padding))))) + (let [h-padding (:horizontal padding) + v-padding (:vertical padding)] + (-> bounds + (update :x - h-padding) + (update :x1 - h-padding) + (update :x2 + h-padding) + (update :y - v-padding) + (update :y1 - v-padding) + (update :y2 + v-padding) + (update :width + (* 2 h-padding)) + (update :height + (* 2 v-padding))))) (defn get-object-bounds [objects shape] - (let [calculate-base-bounds (fn [shape] (-> (get-shape-filter-bounds shape) @@ -138,7 +154,7 @@ (empty? (:shapes shape)) [(calculate-base-bounds shape)] - (:masked-group? shape) + (:masked-group shape) [(calculate-base-bounds shape)] (and (cph/frame-shape? shape) (not (:show-content shape))) @@ -153,17 +169,15 @@ (:show-content shape)) (or (not (cph/group-shape? shape)) - (not (:masked-group? shape))))) - + (not (:masked-group shape))))) (:id shape) - (fn [result child] (conj result (calculate-base-bounds child))) [(calculate-base-bounds shape)])) children-bounds - (cond->> (gsr/join-selrects bounds) + (cond->> (grc/join-rects bounds) (not (cph/frame-shape? shape)) (or (:children-bounds shape))) filters (shape->filters shape) diff --git a/common/src/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc index a0fcdb4b5d..35b5000364 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -7,80 +7,83 @@ (ns app.common.geom.shapes.common (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes.rect :as gpr] - [app.common.math :as mth])) + [app.common.geom.rect :as grc] + [app.common.math :as mth] + [app.common.record :as cr])) -(defn center-rect - [{:keys [x y width height]}] - (when (d/num? x y width height) - (gpt/point (+ x (/ width 2.0)) - (+ y (/ height 2.0))))) +(def ^:private xf-keep-x (keep #(dm/get-prop % :x))) +(def ^:private xf-keep-y (keep #(dm/get-prop % :y))) -(defn center-selrect - "Calculate the center of the selrect." - [selrect] - (center-rect selrect)) +(defn shapes->rect + "Returns a rect that contains all the shapes and is aware of the + rotation of each shape. Mainly used for multiple selection." + [shapes] + (->> shapes + (keep (fn [shape] + (-> (dm/get-prop shape :points) + (grc/points->rect)))) + (grc/join-rects))) -(defn center-points [points] - (let [ptx (into [] (keep :x) points) - pty (into [] (keep :y) points) - minx (reduce min ##Inf ptx) - miny (reduce min ##Inf pty) - maxx (reduce max ##-Inf ptx) - maxy (reduce max ##-Inf pty)] +(defn points->center + [points] + (let [ptx (into [] xf-keep-x points) + pty (into [] xf-keep-y points) + minx (reduce d/min ##Inf ptx) + miny (reduce d/min ##Inf pty) + maxx (reduce d/max ##-Inf ptx) + maxy (reduce d/max ##-Inf pty)] (gpt/point (/ (+ minx maxx) 2.0) (/ (+ miny maxy) 2.0)))) -(defn center-bounds [[a b c d]] - (let [xa (:x a) - ya (:y a) - xb (:x b) - yb (:y b) - xc (:x c) - yc (:y c) - xd (:x d) - yd (:y d) - minx (min xa xb xc xd) - miny (min ya yb yc yd) - maxx (max xa xb xc xd) - maxy (max ya yb yc yd)] - (gpt/point (/ (+ minx maxx) 2.0) - (/ (+ miny maxy) 2.0)))) - -(defn center-shape +(defn shape->center "Calculate the center of the shape." [shape] - (center-rect (:selrect shape))) + (grc/rect->center (dm/get-prop shape :selrect))) (defn transform-points ([points matrix] (transform-points points nil matrix)) ([points center matrix] - (if (and (d/not-empty? points) (gmt/matrix? matrix)) - (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) - post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) - - tr-point (fn [point] - (gpt/transform point (gmt/multiply prev matrix post)))] - (mapv tr-point points)) + (if (and ^boolean (gmt/matrix? matrix) + ^boolean (seq points)) + (let [prev (if (some? center) (gmt/translate-matrix center) (cr/clone gmt/base)) + post (if (some? center) (gmt/translate-matrix-neg center) gmt/base) + mtx (-> prev + (gmt/multiply! matrix) + (gmt/multiply! post))] + (mapv #(gpt/transform % mtx) points)) points))) (defn transform-selrect - [{:keys [x1 y1 x2 y2] :as sr} matrix] - (let [[c1 c2] (transform-points [(gpt/point x1 y1) (gpt/point x2 y2)] matrix)] - (gpr/corners->selrect c1 c2))) + [selrect matrix] + + (dm/assert! + "expected valid rect and matrix instances" + (and (grc/rect? selrect) + (gmt/matrix? matrix))) + + (let [x1 (dm/get-prop selrect :x1) + y1 (dm/get-prop selrect :y1) + x2 (dm/get-prop selrect :x2) + y2 (dm/get-prop selrect :y2) + p1 (gpt/point x1 y1) + p2 (gpt/point x2 y2) + c1 (gpt/transform! p1 matrix) + c2 (gpt/transform! p2 matrix)] + (grc/corners->rect c1 c2))) (defn invalid-geometry? [{:keys [points selrect]}] - (or (mth/nan? (:x selrect)) - (mth/nan? (:y selrect)) - (mth/nan? (:width selrect)) - (mth/nan? (:height selrect)) - (some (fn [p] - (or (mth/nan? (:x p)) - (mth/nan? (:y p)))) - points))) + (or ^boolean (mth/nan? (:x selrect)) + ^boolean (mth/nan? (:y selrect)) + ^boolean (mth/nan? (:width selrect)) + ^boolean (mth/nan? (:height selrect)) + ^boolean (some (fn [p] + (or ^boolean (mth/nan? (:x p)) + ^boolean (mth/nan? (:y p)))) + points))) diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc index 6a9885e69e..5aeb5e3d44 100644 --- a/common/src/app/common/geom/shapes/constraints.cljc +++ b/common/src/app/common/geom/shapes/constraints.cljc @@ -6,7 +6,9 @@ (ns app.common.geom.shapes.constraints (:require + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.intersect :as gsi] [app.common.geom.shapes.points :as gpo] [app.common.geom.shapes.transforms :as gtr] @@ -204,19 +206,22 @@ disp-start (displacement start-before start-after before-side-vector after-side-vector) ;; We get the current axis side and grow it on both side by the end+start displacements - before-vec (side-vector axis child-points-after) - after-vec (side-vector-resize axis child-points-after disp-start disp-end) + before-vec (side-vector axis child-points-after) + after-vec (side-vector-resize axis child-points-after disp-start disp-end) ;; after-vec will contain the side length of the grown side ;; we scale the shape by the diference and translate it by the start ;; displacement (so its left+top position is constant) - scale (/ (gpt/length after-vec) (max 0.01 (gpt/length before-vec))) + scale (/ (gpt/length after-vec) (mth/max 0.01 (gpt/length before-vec))) - resize-origin (gpo/origin child-points-after) + resize-origin (gpo/origin child-points-after) - [_ transform transform-inverse] (gtr/calculate-geometry parent-points-after) + center (gco/points->center parent-points-after) + selrect (gtr/calculate-selrect parent-points-after center) + transform (gtr/calculate-transform parent-points-after center selrect) + transform-inverse (when (some? transform) (gmt/inverse transform)) + resize-vector (get-scale axis scale)] - resize-vector (get-scale axis scale)] (-> (ctm/empty) (ctm/resize resize-vector resize-origin transform transform-inverse) (ctm/move disp-start)))) @@ -276,10 +281,13 @@ resize-vector (gpt/point scale-x scale-y) resize-origin (gpo/origin transformed-child-bounds) - [_ transform transform-inverse] (gtr/calculate-geometry transformed-parent-bounds)] - (-> modifiers - (ctm/resize resize-vector resize-origin transform transform-inverse)))) + center (gco/points->center transformed-child-bounds) + selrect (gtr/calculate-selrect transformed-child-bounds center) + transform (gtr/calculate-transform transformed-child-bounds center selrect) + transform-inverse (when (some? transform) (gmt/inverse transform))] + + (ctm/resize modifiers resize-vector resize-origin transform transform-inverse))) (defn calc-child-modifiers [parent child modifiers ignore-constraints child-bounds parent-bounds transformed-parent-bounds] diff --git a/common/src/app/common/geom/shapes/flex_layout/drop_area.cljc b/common/src/app/common/geom/shapes/flex_layout/drop_area.cljc index f1c019184a..8a050b0f04 100644 --- a/common/src/app/common/geom/shapes/flex_layout/drop_area.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/drop_area.cljc @@ -9,10 +9,10 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.flex-layout.lines :as fli] [app.common.geom.shapes.points :as gpo] - [app.common.geom.shapes.rect :as gsr] [app.common.geom.shapes.transforms :as gtr] [app.common.pages.helpers :as cph] [app.common.types.modifiers :as ctm] @@ -59,16 +59,16 @@ (if row? (let [half-point-width (+ (- box-x x) (/ box-width 2))] - [(gsr/make-rect x y width height) - (-> (gsr/make-rect x y half-point-width height) + [(grc/make-rect x y width height) + (-> (grc/make-rect x y half-point-width height) (assoc :index (if reverse? (inc index) index))) - (-> (gsr/make-rect (+ x half-point-width) y (- width half-point-width) height) + (-> (grc/make-rect (+ x half-point-width) y (- width half-point-width) height) (assoc :index (if reverse? index (inc index))))]) (let [half-point-height (+ (- box-y y) (/ box-height 2))] - [(gsr/make-rect x y width height) - (-> (gsr/make-rect x y width half-point-height) + [(grc/make-rect x y width height) + (-> (grc/make-rect x y width half-point-height) (assoc :index (if reverse? (inc index) index))) - (-> (gsr/make-rect x (+ y half-point-height) width (- height half-point-height)) + (-> (grc/make-rect x (+ y half-point-height) width (- height half-point-height)) (assoc :index (if reverse? index (inc index))))])))) (defn drop-line-area @@ -83,7 +83,7 @@ v-center? (and col? (ctl/v-center? frame)) v-end? (and row? (ctl/v-end? frame)) - center (gco/center-shape frame) + center (gco/shape->center frame) start-p (gmt/transform-point-center start-p center transform-inverse) line-width @@ -136,7 +136,7 @@ :else (+ line-height (- box-y prev-y) (/ layout-gap-row 2)))] - (gsr/make-rect x y width height))) + (grc/make-rect x y width height))) (defn layout-drop-areas "Retrieve the layout drop areas to move shapes inside layouts" @@ -190,7 +190,7 @@ (-> (ctm/empty) (ctm/resize (gpt/point (if flip-x -1.0 1.0) (if flip-y -1.0 1.0)) - (gco/center-shape shape) + (gco/shape->center shape) transform transform-inverse))] [(gtr/transform-shape shape modifiers) modifiers]) @@ -212,6 +212,6 @@ [frame-id objects position] (let [frame (get objects frame-id) drop-areas (get-drop-areas frame objects) - position (gmt/transform-point-center position (gco/center-shape frame) (:transform-inverse frame)) - area (d/seek #(gsr/contains-point? % position) drop-areas)] + position (gmt/transform-point-center position (gco/shape->center frame) (:transform-inverse frame)) + area (d/seek #(grc/contains-point? % position) drop-areas)] (:index area))) diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 12b64f8978..665ac1e594 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -9,9 +9,9 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.path :as gpp] - [app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.text :as gte] [app.common.math :as mth])) @@ -163,7 +163,7 @@ "Checks if the given rect intersects with the selrect" [rect points] - (let [rect-points (gpr/rect->points rect) + (let [rect-points (grc/rect->points rect) rect-lines (points->lines rect-points) points-lines (points->lines points)] @@ -182,7 +182,7 @@ ;; TODO: Look for ways to optimize this operation simple? (> (count (:content shape)) 100) - rect-points (gpr/rect->points rect) + rect-points (grc/rect->points rect) rect-lines (points->lines rect-points) path-lines (if simple? (points->lines (:points shape)) @@ -268,7 +268,7 @@ "Checks if the given rect overlaps with an ellipse" [shape rect] - (let [rect-points (gpr/rect->points rect) + (let [rect-points (grc/rect->points rect) rect-lines (points->lines rect-points) {:keys [x y width height]} shape @@ -289,7 +289,7 @@ [{:keys [position-data] :as shape} rect] (if (and (some? position-data) (d/not-empty? position-data)) - (let [center (gco/center-shape shape) + (let [center (gco/shape->center shape) transform-rect (fn [rect-points] @@ -297,7 +297,7 @@ (->> position-data (map (comp transform-rect - gpr/rect->points + grc/rect->points gte/position-data->rect)) (some #(overlaps-rect-points? rect %)))) (overlaps-rect-points? rect (:points shape)))) @@ -332,7 +332,7 @@ (defn has-point-rect? [rect point] - (let [lines (gpr/rect->lines rect)] + (let [lines (grc/rect->lines rect)] (is-point-inside-evenodd? point lines))) (defn has-point? diff --git a/common/src/app/common/geom/shapes/modifiers.cljc b/common/src/app/common/geom/shapes/modifiers.cljc index f0eb8f320c..315df975e6 100644 --- a/common/src/app/common/geom/shapes/modifiers.cljc +++ b/common/src/app/common/geom/shapes/modifiers.cljc @@ -29,7 +29,7 @@ ;; [(get-in objects [k :name]) v])) ;; modif-tree)))) -(defn children-sequence +(defn- get-children-seq "Given an id returns a sequence of its children" [id objects] @@ -39,61 +39,63 @@ id) (map #(get objects %)))) -(defn resolve-tree-sequence +(defn- resolve-tree "Given the ids that have changed search for layout roots to recalculate" [ids objects] (dm/assert! (or (nil? ids) (set? ids))) - (let [get-tree-root - (fn ;; Finds the tree root for the current id - [id] - + (let [;; Finds the tree root for the current id + get-tree-root + (fn [id] (loop [current id result id] - (let [shape (get objects current) - parent (get objects (:parent-id shape))] - (cond - (or (not shape) (= uuid/zero current)) + (let [shape (get objects current)] + (if (or (not ^boolean shape) (= uuid/zero current)) result + (let [parent-id (dm/get-prop shape :parent-id) + parent (get objects parent-id)] + (cond + ;; Frame found, but not layout we return the last layout found (or the id) + (and ^boolean (cph/frame-shape? parent) + (not ^boolean (ctl/any-layout? parent))) + result - ;; Frame found, but not layout we return the last layout found (or the id) - (and (= :frame (:type parent)) - (not (ctl/any-layout? parent))) - result + ;; Layout found. We continue upward but we mark this layout + (ctl/any-layout? parent) + (recur parent-id parent-id) - ;; Layout found. We continue upward but we mark this layout - (ctl/any-layout? parent) - (recur (:id parent) (:id parent)) + ;; If group or boolean or other type of group we continue with the last result + :else + (recur parent-id result))))))) - ;; If group or boolean or other type of group we continue with the last result - :else - (recur (:id parent) result))))) - - is-child? #(cph/is-child? objects %1 %2) - - calculate-common-roots - (fn ;; Given some roots retrieves the minimum number of tree roots - [result id] + ;; Given some roots retrieves the minimum number of tree roots + search-common-roots + (fn [result id] (if (= id uuid/zero) result (let [root (get-tree-root id) ;; Remove the children from the current root result - (if (cph/has-children? objects root) - (into #{} (remove #(is-child? root %)) result) + (if ^boolean (cph/has-children? objects root) + (into #{} (remove (partial cph/is-child? objects root)) result) result) - root-parents (cph/get-parent-ids objects root) - contains-parent? (some #(contains? result %) root-parents)] - (cond-> result - (not contains-parent?) - (conj root))))) + contains-parent? + (->> (cph/get-parent-ids objects root) + (some (partial contains? result)))] - roots (->> ids (reduce calculate-common-roots #{}))] - (concat - (when (contains? ids uuid/zero) [(get objects uuid/zero)]) - (mapcat #(children-sequence % objects) roots)))) + (if (not contains-parent?) + (conj result root) + result)))) + + result + (->> (reduce search-common-roots #{} ids) + (mapcat #(get-children-seq % objects)))] + + (if (contains? ids uuid/zero) + (cons (get objects uuid/zero) result) + result))) (defn- set-children-modifiers "Propagates the modifiers from a parent too its children applying constraints if necesary" @@ -371,7 +373,7 @@ (defn reflow-layout [objects old-modif-tree bounds ignore-constraints id] - (let [tree-seq (children-sequence id objects) + (let [tree-seq (get-children-seq id objects) [modif-tree _] (reduce @@ -416,7 +418,7 @@ (let [resize-modif-tree {current {:modifiers auto-resize-modifiers}} - tree-seq (children-sequence current objects) + tree-seq (get-children-seq current objects) [resize-modif-tree _] (reduce @@ -440,7 +442,7 @@ ;; Step-2: After resizing we still need to reflow the layout parents that are not auto-width/height - tree-seq (resolve-tree-sequence to-reflow objects) + tree-seq (resolve-tree to-reflow objects) [reflow-modif-tree _] (reduce @@ -476,7 +478,7 @@ (some? old-modif-tree) (transform-bounds objects old-modif-tree)) - shapes-tree (resolve-tree-sequence (-> modif-tree keys set) objects) + shapes-tree (resolve-tree (-> modif-tree keys set) objects) ;; Calculate the input transformation and constraints modif-tree (reduce #(propagate-modifiers-constraints objects bounds ignore-constraints %1 %2) modif-tree shapes-tree) diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index bce132b4bb..7497ef87e0 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -9,8 +9,8 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gsc] - [app.common.geom.shapes.rect :as gpr] + [app.common.geom.rect :as grc] + [app.common.geom.shapes.common :as gco] [app.common.math :as mth] [app.common.path.commands :as upc] [app.common.path.subpaths :as sp])) @@ -46,11 +46,14 @@ (defn content->points "Returns the points in the given content" [content] - (->> content - (map #(when (-> % :params :x) - (gpt/point (-> % :params :x) (-> % :params :y)))) - (remove nil?) - (into []))) + (letfn [(segment->point [seg] + (let [params (get seg :params) + x (get params :x) + y (get params :y)] + (when (d/num? x y) + (gpt/point x y))))] + (some->> (seq content) + (into [] (keep segment->point))))) (defn line-values [[from-p to-p] t] @@ -334,7 +337,7 @@ (->> (curve-extremities curve) (mapv #(curve-values curve %))))) [])] - (gpr/points->selrect points)))) + (grc/points->rect points)))) (defn content->selrect [content] (let [calc-extremities @@ -360,7 +363,7 @@ extremities (mapcat calc-extremities content (concat [nil] content))] - (gpr/points->selrect extremities))) + (grc/points->rect extremities))) (defn move-content [content move-vec] (let [dx (:x move-vec) @@ -591,7 +594,7 @@ (let [[from-p to-p :as curve] (subcurve-range curve from-t to-t) extremes (->> (curve-extremities curve) (mapv #(curve-values curve %)))] - (gpr/points->rect (into [from-p to-p] extremes)))) + (grc/points->rect (into [from-p to-p] extremes)))) (defn line-has-point? "Using the line equation we put the x value and check if matches with @@ -623,7 +626,7 @@ [point curve] (letfn [(check-range [from-t to-t] (let [r (curve-range->rect curve from-t to-t)] - (when (gpr/contains-point? r point) + (when (grc/contains-point? r point) (if (s= from-t to-t) (< (gpt/distance (curve-values curve from-t) point) 0.1) @@ -760,7 +763,7 @@ (let [r1 (curve-range->rect c1 c1-from c1-to) r2 (curve-range->rect c2 c2-from c2-to)] - (when (gpr/overlaps-rects? r1 r2) + (when (grc/overlaps-rects? r1 r2) (let [p1 (curve-values c1 c1-from) p2 (curve-values c2 c2-from)] @@ -811,7 +814,7 @@ [[from-p to-p :as curve]] (let [extremes (->> (curve-extremities curve) (mapv #(curve-values curve %)))] - (gpr/points->rect (into [from-p to-p] extremes)))) + (grc/points->rect (into [from-p to-p] extremes)))) (defn is-point-in-border? @@ -943,7 +946,7 @@ [content] (-> content content->selrect - gsc/center-selrect)) + grc/rect->center)) (defn content->points+selrect "Given the content of a shape, calculate its points and selrect" @@ -960,7 +963,7 @@ flip-y (gmt/scale (gpt/point 1 -1)) :always (gmt/multiply (:transform-inverse shape (gmt/matrix)))) - center (or (gsc/center-shape shape) + center (or (gco/shape->center shape) (content-center content)) base-content (transform-content @@ -969,16 +972,16 @@ ;; Calculates the new selrect with points given the old center points (-> (content->selrect base-content) - (gpr/rect->points) - (gsc/transform-points center transform)) + (grc/rect->points) + (gco/transform-points center transform)) - points-center (gsc/center-points points) + points-center (gco/points->center points) ;; Points is now the selrect but the center is different so we can create the selrect ;; through points selrect (-> points - (gsc/transform-points points-center transform-inverse) - (gpr/points->selrect))] + (gco/transform-points points-center transform-inverse) + (grc/points->rect))] [points selrect])) (defn open-path? diff --git a/common/src/app/common/geom/shapes/pixel_precision.cljc b/common/src/app/common/geom/shapes/pixel_precision.cljc index 9994b09788..3702ddec69 100644 --- a/common/src/app/common/geom/shapes/pixel_precision.cljc +++ b/common/src/app/common/geom/shapes/pixel_precision.cljc @@ -8,10 +8,11 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.points :as gpo] - [app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.transforms :as gtr] [app.common.math :as mth] [app.common.pages.helpers :as cph] @@ -19,28 +20,32 @@ (defn size-pixel-precision [modifiers shape points precision] - (let [origin (gpo/origin points) - curr-width (gpo/width-points points) - curr-height (gpo/height-points points) + (let [origin (gpo/origin points) + curr-width (gpo/width-points points) + curr-height (gpo/height-points points) - [_ transform transform-inverse] (gtr/calculate-geometry points) + center (gco/points->center points) + selrect (gtr/calculate-selrect points center) - path? (cph/path-shape? shape) - vertical-line? (and path? (<= curr-width 0.01)) - horizontal-line? (and path? (<= curr-height 0.01)) + transform (gtr/calculate-transform points center selrect) + transform-inverse (when (some? transform) (gmt/inverse transform)) - target-width (if vertical-line? curr-width (max 1 (mth/round curr-width precision))) - target-height (if horizontal-line? curr-height (max 1 (mth/round curr-height precision))) + path? (cph/path-shape? shape) + vertical-line? (and path? (<= curr-width 0.01)) + horizontal-line? (and path? (<= curr-height 0.01)) - ratio-width (/ target-width curr-width) - ratio-height (/ target-height curr-height) - scalev (gpt/point ratio-width ratio-height)] - (-> modifiers - (ctm/resize scalev origin transform transform-inverse {:precise? true})))) + target-width (if vertical-line? curr-width (mth/max 1 (mth/round curr-width precision))) + target-height (if horizontal-line? curr-height (mth/max 1 (mth/round curr-height precision))) + + ratio-width (/ target-width curr-width) + ratio-height (/ target-height curr-height) + scalev (gpt/point ratio-width ratio-height)] + + (ctm/resize modifiers scalev origin transform transform-inverse {:precise? true}))) (defn position-pixel-precision [modifiers _ points precision ignore-axis] - (let [bounds (gpr/bounds->rect points) + (let [bounds (grc/bounds->rect points) corner (gpt/point bounds) target-corner (cond-> corner diff --git a/common/src/app/common/geom/shapes/points.cljc b/common/src/app/common/geom/shapes/points.cljc index e2403b76b0..dbd1bfcf70 100644 --- a/common/src/app/common/geom/shapes/points.cljc +++ b/common/src/app/common/geom/shapes/points.cljc @@ -8,9 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.intersect :as gsi] - [app.common.geom.shapes.rect :as gre] [app.common.math :as mth])) (defn origin @@ -104,7 +102,6 @@ (defn parent-coords-bounds [child-bounds [p1 p2 _ p4 :as parent-bounds]] - (if (empty? child-bounds) parent-bounds @@ -121,10 +118,10 @@ (fn [[th-min th-max tv-min tv-max] current-point] (let [cth (project-t current-point rh vv) ctv (project-t current-point rv hv)] - [(min th-min cth) - (max th-max cth) - (min tv-min ctv) - (max tv-max ctv)])) + [(mth/min th-min cth) + (mth/max th-max cth) + (mth/min tv-min ctv) + (mth/max tv-max ctv)])) [th-min th-max tv-min tv-max] (->> child-bounds @@ -152,13 +149,6 @@ [bounds parent-bounds] (parent-coords-bounds (flatten bounds) parent-bounds)) -(defn points->selrect - [points] - (let [width (width-points points) - height (height-points points) - center (gco/center-points points)] - (gre/center->selrect center width height))) - (defn move [bounds vector] (->> bounds diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index c304b99f47..adacddb73e 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -4,221 +4,4 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.geom.shapes.rect - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.point :as gpt] - [app.common.math :as mth])) - -(defn make-rect - ([p1 p2] - (let [xp1 (:x p1) - yp1 (:y p1) - xp2 (:x p2) - yp2 (:y p2) - x1 (min xp1 xp2) - y1 (min yp1 yp2) - x2 (max xp1 xp2) - y2 (max yp1 yp2)] - (make-rect x1 y1 (- x2 x1) (- y2 y1)))) - - ([x y width height] - (when (d/num? x y width height) - (let [width (max width 0.01) - height (max height 0.01)] - {:x x - :y y - :width width - :height height})))) - -(defn make-selrect - [x y width height] - (when (d/num? x y width height) - (let [width (max width 0.01) - height (max height 0.01)] - {:x x - :y y - :x1 x - :y1 y - :x2 (+ x width) - :y2 (+ y height) - :width width - :height height}))) - -(defn close-rect? - [rect1 rect2] - (and (mth/close? (:x rect1) (:x rect2)) - (mth/close? (:y rect1) (:y rect2)) - (mth/close? (:width rect1) (:width rect2)) - (mth/close? (:height rect1) (:height rect2)))) - -(defn close-selrect? - [selrect1 selrect2] - (and (mth/close? (:x selrect1) (:x selrect2)) - (mth/close? (:y selrect1) (:y selrect2)) - (mth/close? (:x1 selrect1) (:x1 selrect2)) - (mth/close? (:y1 selrect1) (:y1 selrect2)) - (mth/close? (:x2 selrect1) (:x2 selrect2)) - (mth/close? (:y2 selrect1) (:y2 selrect2)) - (mth/close? (:width selrect1) (:width selrect2)) - (mth/close? (:height selrect1) (:height selrect2)))) - -(defn rect->points [{:keys [x y width height]}] - (when (d/num? x y) - (let [width (max width 0.01) - height (max height 0.01)] - [(gpt/point x y) - (gpt/point (+ x width) y) - (gpt/point (+ x width) (+ y height)) - (gpt/point x (+ y height))]))) - -(defn rect->lines [{:keys [x y width height]}] - (when (d/num? x y) - (let [width (max width 0.01) - height (max height 0.01)] - [[(gpt/point x y) (gpt/point (+ x width) y)] - [(gpt/point (+ x width) y) (gpt/point (+ x width) (+ y height))] - [(gpt/point (+ x width) (+ y height)) (gpt/point x (+ y height))] - [(gpt/point x (+ y height)) (gpt/point x y)]]))) - -(defn points->rect - [points] - (when-let [points (seq points)] - (loop [minx ##Inf - miny ##Inf - maxx ##-Inf - maxy ##-Inf - pts points] - (if-let [pt (first pts)] - (let [x (dm/get-prop pt :x) - y (dm/get-prop pt :y)] - (recur (min minx x) - (min miny y) - (max maxx x) - (max maxy y) - (rest pts))) - (when (d/num? minx miny maxx maxy) - (make-rect minx miny (- maxx minx) (- maxy miny))))))) - -(defn bounds->rect - [[{ax :x ay :y} {bx :x by :y} {cx :x cy :y} {dx :x dy :y}]] - (let [minx (min ax bx cx dx) - miny (min ay by cy dy) - maxx (max ax bx cx dx) - maxy (max ay by cy dy)] - (when (d/num? minx miny maxx maxy) - (make-rect minx miny (- maxx minx) (- maxy miny))))) - -(defn squared-points - [points] - (when (d/not-empty? points) - (let [minx (transduce (keep :x) min ##Inf points) - miny (transduce (keep :y) min ##Inf points) - maxx (transduce (keep :x) max ##-Inf points) - maxy (transduce (keep :y) max ##-Inf points)] - (when (d/num? minx miny maxx maxy) - [(gpt/point minx miny) - (gpt/point maxx miny) - (gpt/point maxx maxy) - (gpt/point minx maxy)])))) - -(defn points->selrect [points] - (when-let [rect (points->rect points)] - (let [{:keys [x y width height]} rect] - (make-selrect x y width height)))) - -(defn rect->selrect [rect] - (-> rect rect->points points->selrect)) - -(defn join-rects [rects] - (when (d/not-empty? rects) - (let [minx (transduce (keep :x) min ##Inf rects) - miny (transduce (keep :y) min ##Inf rects) - maxx (transduce (keep #(when (and (:x %) (:width %)) (+ (:x %) (:width %)))) max ##-Inf rects) - maxy (transduce (keep #(when (and (:y %) (:height %))(+ (:y %) (:height %)))) max ##-Inf rects)] - (when (d/num? minx miny maxx maxy) - (make-rect minx miny (- maxx minx) (- maxy miny)))))) - -(defn join-selrects [selrects] - (when (d/not-empty? selrects) - (let [minx (transduce (keep :x1) min ##Inf selrects) - miny (transduce (keep :y1) min ##Inf selrects) - maxx (transduce (keep :x2) max ##-Inf selrects) - maxy (transduce (keep :y2) max ##-Inf selrects)] - (when (d/num? minx miny maxx maxy) - (make-selrect minx miny (- maxx minx) (- maxy miny)))))) - -(defn center->rect [{:keys [x y]} width height] - (when (d/num? x y width height) - (make-rect (- x (/ width 2)) - (- y (/ height 2)) - width - height))) - -(defn center->selrect [{:keys [x y]} width height] - (when (d/num? x y width height) - (make-selrect (- x (/ width 2)) - (- y (/ height 2)) - width - height))) - -(defn s= - [a b] - (mth/almost-zero? (- a b))) - -(defn overlaps-rects? - "Check for two rects to overlap. Rects won't overlap only if - one of them is fully to the left or the top" - [rect-a rect-b] - - (let [x1a (:x rect-a) - y1a (:y rect-a) - x2a (+ (:x rect-a) (:width rect-a)) - y2a (+ (:y rect-a) (:height rect-a)) - - x1b (:x rect-b) - y1b (:y rect-b) - x2b (+ (:x rect-b) (:width rect-b)) - y2b (+ (:y rect-b) (:height rect-b))] - - (and (or (> x2a x1b) (s= x2a x1b)) - (or (>= x2b x1a) (s= x2b x1a)) - (or (<= y1b y2a) (s= y1b y2a)) - (or (<= y1a y2b) (s= y1a y2b))))) - -(defn contains-point? - [rect point] - (assert (gpt/point? point)) - (let [x1 (:x rect) - y1 (:y rect) - x2 (+ (:x rect) (:width rect)) - y2 (+ (:y rect) (:height rect)) - - px (:x point) - py (:y point)] - - (and (or (> px x1) (s= px x1)) - (or (< px x2) (s= px x2)) - (or (> py y1) (s= py y1)) - (or (< py y2) (s= py y2))))) - -(defn contains-selrect? - "Check if a selrect sr2 is contained inside sr1" - [sr1 sr2] - (and (>= (:x1 sr2) (:x1 sr1)) - (<= (:x2 sr2) (:x2 sr1)) - (>= (:y1 sr2) (:y1 sr1)) - (<= (:y2 sr2) (:y2 sr1)))) - -(defn corners->selrect - ([p1 p2] - (corners->selrect (:x p1) (:y p1) (:x p2) (:y p2))) - ([xp1 yp1 xp2 yp2] - (make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2))))) - -(defn clip-selrect - [{:keys [x1 y1 x2 y2] :as sr} clip-rect] - (when (some? sr) - (let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2 :as sr2} (rect->selrect clip-rect)] - (corners->selrect (max bx1 x1) (max by1 y1) (min bx2 x2) (min by2 y2))))) +(ns app.common.geom.shapes.rect) diff --git a/common/src/app/common/geom/shapes/text.cljc b/common/src/app/common/geom/shapes/text.cljc index 1b6739187f..1d08d6ab23 100644 --- a/common/src/app/common/geom/shapes/text.cljc +++ b/common/src/app/common/geom/shapes/text.cljc @@ -6,41 +6,36 @@ (ns app.common.geom.shapes.text (:require + [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] - [app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.transforms :as gtr])) (defn position-data->rect [{:keys [x y width height]}] - {:x x - :y (- y height) - :width width - :height height}) + (grc/make-rect x (- y height) width height)) -(defn position-data-selrect +(defn shape->rect [shape] - (let [points (->> shape - :position-data - (mapcat (comp gpr/rect->points position-data->rect)))] - (if (empty? points) - (:selrect shape) - (-> points (gpr/points->selrect))))) + (let [points (->> (:position-data shape) + (mapcat (comp grc/rect->points position-data->rect)))] + (if (seq points) + (grc/points->rect points) + (dm/get-prop shape :selrect)))) -(defn position-data-bounding-box +(defn shape->bounds [shape] - (let [points (->> shape - :position-data - (mapcat (comp gpr/rect->points position-data->rect))) - transform (gtr/transform-matrix shape)] + (let [points (->> (:position-data shape) + (mapcat (comp grc/rect->points position-data->rect)))] (-> points - (gco/transform-points transform) - (gpr/points->selrect )))) + (gco/transform-points (gtr/transform-matrix shape)) + (grc/points->rect)))) (defn overlaps-position-data? "Checks if the given position data is inside the shape" [{:keys [points]} position-data] - (let [bounding-box (gpr/points->selrect points) + (let [bounding-box (grc/points->rect points) fix-rect #(assoc % :y (- (:y %) (:height %)))] (->> position-data - (some #(gpr/overlaps-rects? bounding-box (fix-rect %))) + (some #(grc/overlaps-rects? bounding-box (fix-rect %))) (boolean)))) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 1c7bc9b8ea..98b78be5aa 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -5,97 +5,114 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.geom.shapes.transforms - #?(:clj (:import (org.la4j Matrix LinearAlgebra)) - :cljs (:import goog.math.Matrix)) (:require - #?(:clj [app.common.exceptions :as ex]) [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.bool :as gshb] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.path :as gpa] - [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] [app.common.pages.helpers :as cph] - [app.common.types.modifiers :as ctm] - [app.common.uuid :as uuid])) + [app.common.record :as cr] + [app.common.types.modifiers :as ctm])) #?(:clj (set! *warn-on-reflection* true)) +(defn- valid-point? + [o] + (and ^boolean (gpt/point? o) + ^boolean (d/num? (dm/get-prop o :x) + (dm/get-prop o :y)))) + ;; --- Relative Movement -(defn- move-selrect [{:keys [x y x1 y1 x2 y2 width height] :as selrect} {dx :x dy :y :as pt}] - (if (and (some? selrect) (some? pt) (d/num? dx dy)) - {:x (if (d/num? x) (+ dx x) x) - :y (if (d/num? y) (+ dy y) y) - :x1 (if (d/num? x1) (+ dx x1) x1) - :y1 (if (d/num? y1) (+ dy y1) y1) - :x2 (if (d/num? x2) (+ dx x2) x2) - :y2 (if (d/num? y2) (+ dy y2) y2) - :width width - :height height} +(defn- move-selrect + [selrect pt] + (if (and ^boolean (some? selrect) + ^boolean (valid-point? pt)) + (let [x (dm/get-prop selrect :x) + y (dm/get-prop selrect :y) + w (dm/get-prop selrect :width) + h (dm/get-prop selrect :height) + dx (dm/get-prop pt :x) + dy (dm/get-prop pt :y)] + + (grc/make-rect + (if ^boolean (d/num? x) (+ dx x) x) + (if ^boolean (d/num? y) (+ dy y) y) + w + h)) selrect)) -(defn- move-points [points move-vec] - (cond->> points - (d/num? (:x move-vec) (:y move-vec)) - (mapv #(gpt/add % move-vec)))) +(defn- move-points + [points move-vec] + (if (valid-point? move-vec) + (mapv #(gpt/add % move-vec) points) + points)) +;; FIXME: deprecated (defn move-position-data - ([position-data {:keys [x y]}] - (move-position-data position-data x y)) - - ([position-data dx dy] - (when (some? position-data) - (cond->> position-data - (d/num? dx dy) - (mapv #(-> % + [position-data delta] + (when (some? position-data) + (let [dx (dm/get-prop delta :x) + dy (dm/get-prop delta :y)] + (if (d/num? dx dy) + (mapv #(-> % (update :x + dx) - (update :y + dy))))))) + (update :y + dy)) + position-data) + position-data)))) +(defn transform-position-data + [position-data transform] + (when (some? position-data) + (let [dx (dm/get-prop transform :e) + dy (dm/get-prop transform :f)] + (if (d/num? dx dy) + (mapv #(-> % + (update :x + dx) + (update :y + dy)) + position-data) + position-data)))) + +;; FIXME: revist usage of mutability (defn move "Move the shape relatively to its current position applying the provided delta." - [{:keys [type] :as shape} {dx :x dy :y}] - (let [dx (d/check-num dx 0) - dy (d/check-num dy 0) - move-vec (gpt/point dx dy)] + [shape point] + (let [type (dm/get-prop shape :type) + dx (dm/get-prop point :x) + dy (dm/get-prop point :y) + dx (d/check-num dx 0) + dy (d/check-num dy 0) + mvec (gpt/point dx dy)] (-> shape - (update :selrect move-selrect move-vec) - (update :points move-points move-vec) - (d/update-when :x + dx) - (d/update-when :y + dy) - (d/update-when :position-data move-position-data dx dy) - (cond-> (= :bool type) (update :bool-content gpa/move-content move-vec)) - (cond-> (= :path type) (update :content gpa/move-content move-vec))))) + (update :selrect move-selrect mvec) + (update :points move-points mvec) + (d/update-when :x d/safe+ dx) + (d/update-when :y d/safe+ dy) + (d/update-when :position-data move-position-data mvec) + (cond-> (= :bool type) (update :bool-content gpa/move-content mvec)) + (cond-> (= :path type) (update :content gpa/move-content mvec))))) ;; --- Absolute Movement (defn absolute-move "Move the shape to the exactly specified position." - [shape {:keys [x y]}] - (let [dx (- (d/check-num x) (-> shape :selrect :x)) - dy (- (d/check-num y) (-> shape :selrect :y))] + [shape pos] + (let [x (dm/get-prop pos :x) + y (dm/get-prop pos :y) + sr (dm/get-prop shape :selrect) + px (dm/get-prop sr :x) + py (dm/get-prop sr :y) + dx (- (d/check-num x) px) + dy (- (d/check-num y) py)] (move shape (gpt/point dx dy)))) -; ---- Geometric operations - -(defn- calculate-height - "Calculates the height of a parallelogram given by the points" - [[p1 _ _ p4]] - - (-> (gpt/to-vec p4 p1) - (gpt/length))) - -(defn- calculate-width - "Calculates the width of a parallelogram given by the points" - [[p1 p2 _ _]] - (-> (gpt/to-vec p1 p2) - (gpt/length))) - ;; --- Transformation matrix operations (defn transform-matrix @@ -105,7 +122,7 @@ (transform-matrix shape nil)) ([shape params] - (transform-matrix shape params (or (gco/center-shape shape) (gpt/point 0 0)))) + (transform-matrix shape params (or (gco/shape->center shape) (gpt/point 0 0)))) ([{:keys [flip-x flip-y transform] :as shape} {:keys [no-flip]} shape-center] (-> (gmt/matrix) @@ -134,9 +151,10 @@ (dm/str (transform-matrix shape params)) ""))) +;; FIXME: performance (defn inverse-transform-matrix ([shape] - (let [shape-center (or (gco/center-shape shape) + (let [shape-center (or (gco/shape->center shape) (gpt/point 0 0))] (inverse-transform-matrix shape shape-center))) ([{:keys [flip-x flip-y] :as shape} center] @@ -148,217 +166,214 @@ (gmt/multiply (:transform-inverse shape (gmt/matrix))) (gmt/translate (gpt/negate center))))) +;; FIXME: move to geom rect? (defn transform-rect "Transform a rectangles and changes its attributes" [rect matrix] - (let [points (-> (gpr/rect->points rect) + (let [points (-> (grc/rect->points rect) (gco/transform-points matrix))] - (gpr/points->rect points))) + (grc/points->rect points))) (defn transform-points-matrix - "Calculate the transform matrix to convert from the selrect to the points bounds - TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)" - [{:keys [x1 y1 x2 y2]} [d1 d2 _ d4]] + [selrect [d1 d2 _ d4]] ;; If the coordinates are very close to zero (but not zero) the rounding can mess with the ;; transforms. So we round to zero the values - (let [x1 (mth/round-to-zero x1) - y1 (mth/round-to-zero y1) - x2 (mth/round-to-zero x2) - y2 (mth/round-to-zero y2) - d1x (mth/round-to-zero (:x d1)) - d1y (mth/round-to-zero (:y d1)) - d2x (mth/round-to-zero (:x d2)) - d2y (mth/round-to-zero (:y d2)) - d4x (mth/round-to-zero (:x d4)) - d4y (mth/round-to-zero (:y d4))] - #?(:clj - ;; NOTE: the source matrix may not be invertible we can't - ;; calculate the transform, so on exception we return `nil` - (ex/ignoring - (let [target-points-matrix - (->> (list d1x d2x d4x - d1y d2y d4y - 1 1 1) - (into-array Double/TYPE) - (Matrix/from1DArray 3 3)) + (let [x1 (mth/round-to-zero (dm/get-prop selrect :x1)) + y1 (mth/round-to-zero (dm/get-prop selrect :y1)) + x2 (mth/round-to-zero (dm/get-prop selrect :x2)) + y2 (mth/round-to-zero (dm/get-prop selrect :y2)) - source-points-matrix - (->> (list x1 x2 x1 - y1 y1 y2 - 1 1 1) - (into-array Double/TYPE) - (Matrix/from1DArray 3 3)) + det (+ (- (* (- y1 y2) x1) + (* (- y1 y2) x2)) + (* (- y1 y1) x1))] - ;; May throw an exception if the matrix is not invertible - source-points-matrix-inv - (.. source-points-matrix - (withInverter LinearAlgebra/GAUSS_JORDAN) - (inverse)) + (when-not (zero? det) + (let [ma0 (mth/round-to-zero (dm/get-prop d1 :x)) + ma1 (mth/round-to-zero (dm/get-prop d2 :x)) + ma2 (mth/round-to-zero (dm/get-prop d4 :x)) + ma3 (mth/round-to-zero (dm/get-prop d1 :y)) + ma4 (mth/round-to-zero (dm/get-prop d2 :y)) + ma5 (mth/round-to-zero (dm/get-prop d4 :y)) - transform-jvm - (.. target-points-matrix - (multiply source-points-matrix-inv))] + mb0 (/ (- y1 y2) det) + mb1 (/ (- x1 x2) det) + mb2 (/ (- (* x2 y2) (* x1 y1)) det) + mb3 (/ (- y2 y1) det) + mb4 (/ (- x1 x1) det) + mb5 (/ (- (* x1 y1) (* x1 y2)) det) + mb6 (/ (- y1 y1) det) + mb7 (/ (- x2 x1) det) + mb8 (/ (- (* x1 y1) (* x2 y1)) det)] - (gmt/matrix (.get transform-jvm 0 0) - (.get transform-jvm 1 0) - (.get transform-jvm 0 1) - (.get transform-jvm 1 1) - (.get transform-jvm 0 2) - (.get transform-jvm 1 2)))) + (gmt/matrix (+ (* ma0 mb0) + (* ma1 mb3) + (* ma2 mb6)) + (+ (* ma3 mb0) + (* ma4 mb3) + (* ma5 mb6)) + (+ (* ma0 mb1) + (* ma1 mb4) + (* ma2 mb7)) + (+ (* ma3 mb1) + (* ma4 mb4) + (* ma5 mb7)) + (+ (* ma0 mb2) + (* ma1 mb5) + (* ma2 mb8)) + (+ (* ma3 mb2) + (* ma4 mb5) + (* ma5 mb8))))))) - :cljs - (let [target-points-matrix - (Matrix. #js [#js [d1x d2x d4x] - #js [d1y d2y d4y] - #js [ 1 1 1]]) +(defn calculate-selrect + [points center] - source-points-matrix - (Matrix. #js [#js [x1 x2 x1] - #js [y1 y1 y2] - #js [ 1 1 1]]) + (let [p1 (nth points 0) + p2 (nth points 1) + p4 (nth points 3) - ;; returns nil if not invertible - source-points-matrix-inv (.getInverse source-points-matrix) + width (mth/hypot + (- (dm/get-prop p2 :x) + (dm/get-prop p1 :x)) + (- (dm/get-prop p2 :y) + (dm/get-prop p1 :y))) - ;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM) - transform-js - (when source-points-matrix-inv - (.multiply target-points-matrix source-points-matrix-inv))] + height (mth/hypot + (- (dm/get-prop p1 :x) + (dm/get-prop p4 :x)) + (- (dm/get-prop p1 :y) + (dm/get-prop p4 :y)))] - (when transform-js - (gmt/matrix (.getValueAt transform-js 0 0) - (.getValueAt transform-js 1 0) - (.getValueAt transform-js 0 1) - (.getValueAt transform-js 1 1) - (.getValueAt transform-js 0 2) - (.getValueAt transform-js 1 2))))))) + (grc/center->rect center width height))) -(defn calculate-geometry - [points] - (let [width (calculate-width points) - height (calculate-height points) - center (gco/center-points points) - sr (gpr/center->selrect center width height) - - points-transform-mtx (transform-points-matrix sr points) +(defn calculate-transform + [points center selrect] + (let [transform (transform-points-matrix selrect points) ;; Calculate the transform by move the transformation to the center transform - (when points-transform-mtx - (gmt/multiply - (gmt/translate-matrix (gpt/negate center)) - points-transform-mtx - (gmt/translate-matrix center))) + (when (some? transform) + (-> (gmt/translate-matrix-neg center) + (gmt/multiply! transform) + (gmt/multiply! (gmt/translate-matrix center))))] - transform-inverse (when transform (gmt/inverse transform)) + ;; There is a rounding error when the matrix returned have float point values + ;; when the matrix is unit we return a "pure" matrix so we don't accumulate + ;; rounding problems + (when ^boolean (gmt/matrix? transform) + (if ^boolean (gmt/unit? transform) + gmt/base + transform)))) - ;; There is a rounding error when the matrix returned have float point values - ;; when the matrix is unit we return a "pure" matrix so we don't accumulate - ;; rounding problems - [transform transform-inverse] - (if (gmt/unit? transform) - [(gmt/matrix) (gmt/matrix)] - [transform transform-inverse])] +(defn calculate-geometry + [points] + (let [center (gco/points->center points) + selrect (calculate-selrect points center) + transform (calculate-transform points center selrect)] + [selrect transform (when (some? transform) (gmt/inverse transform))])) - [sr transform transform-inverse])) - -(defn- adjust-shape-flips +(defn- adjust-shape-flips! "After some tranformations the flip-x/flip-y flags can change we need to check this before adjusting the selrect" [shape points] + (let [points' (dm/get-prop shape :points) + p0' (nth points' 0) + p0 (nth points 0) - (let [points' (:points shape) + ;; FIXME: unroll and remove point allocation here + xv1 (gpt/to-vec p0' (nth points' 1)) + xv2 (gpt/to-vec p0 (nth points 1)) + dot-x (gpt/dot xv1 xv2) - xv1 (gpt/to-vec (nth points' 0) (nth points' 1)) - xv2 (gpt/to-vec (nth points 0) (nth points 1)) - dot-x (gpt/dot xv1 xv2) - - yv1 (gpt/to-vec (nth points' 0) (nth points' 3)) - yv2 (gpt/to-vec (nth points 0) (nth points 3)) - dot-y (gpt/dot yv1 yv2)] + yv1 (gpt/to-vec p0' (nth points' 3)) + yv2 (gpt/to-vec p0 (nth points 3)) + dot-y (gpt/dot yv1 yv2)] (cond-> shape (neg? dot-x) - (-> (update :flip-x not) - (update :rotation -)) + (-> (cr/update! :flip-x not) + (cr/update! :rotation -)) (neg? dot-y) - (-> (update :flip-y not) - (update :rotation -))))) + (-> (cr/update! :flip-y not) + (cr/update! :rotation -))))) (defn- apply-transform-move "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" [shape transform-mtx] - (let [bool? (= (:type shape) :bool) - path? (= (:type shape) :path) - text? (= (:type shape) :text) - {dx :x dy :y} (gpt/transform (gpt/point) transform-mtx) - points (gco/transform-points (:points shape) transform-mtx) - selrect (gco/transform-selrect (:selrect shape) transform-mtx)] + (let [type (dm/get-prop shape :type) + points (gco/transform-points (dm/get-prop shape :points) transform-mtx) + selrect (gco/transform-selrect (dm/get-prop shape :selrect) transform-mtx) + + ;; NOTE: ensure we start with a fresh copy of shape for mutabilty + shape (cr/clone shape) + + shape (if (= type :bool) + (update shape :bool-content gpa/transform-content transform-mtx) + shape) + shape (if (= type :text) + (update shape :position-data move-position-data transform-mtx) + shape) + shape (if (= type :path) + (update shape :content gpa/transform-content transform-mtx) + (cr/assoc! shape + :x (dm/get-prop selrect :x) + :y (dm/get-prop selrect :y) + :width (dm/get-prop selrect :width) + :height (dm/get-prop selrect :height)))] (-> shape - (cond-> bool? - (update :bool-content gpa/transform-content transform-mtx)) - (cond-> path? - (update :content gpa/transform-content transform-mtx)) - (cond-> text? - (update :position-data move-position-data dx dy)) - (cond-> (not path?) - (assoc :x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect))) - (assoc :selrect selrect) - (assoc :points points)))) + (cr/assoc! :selrect selrect) + (cr/assoc! :points points)))) + (defn- apply-transform-generic "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" [shape transform-mtx] + (let [points (-> (dm/get-prop shape :points) + (gco/transform-points transform-mtx)) - (let [points' (:points shape) - points (gco/transform-points points' transform-mtx) - shape (-> shape (adjust-shape-flips points)) - bool? (= (:type shape) :bool) - path? (= (:type shape) :path) + ;; NOTE: ensure we have a fresh shallow copy of shape + shape (cr/clone shape) + shape (adjust-shape-flips! shape points) - [selrect transform transform-inverse] (calculate-geometry points) + center (gco/points->center points) + selrect (calculate-selrect points center) + transform (calculate-transform points center selrect) + inverse (when (some? transform) (gmt/inverse transform)) - base-rotation (or (:rotation shape) 0) - modif-rotation (or (get-in shape [:modifiers :rotation]) 0) - rotation (mod (+ base-rotation modif-rotation) 360)] + ] - (if-not (and transform transform-inverse) - ;; When we cannot calculate the transformation we leave the shape as it was + (if-not (and (some? inverse) (some? transform)) shape - (-> shape - (cond-> bool? - (update :bool-content gpa/transform-content transform-mtx)) - (cond-> path? - (update :content gpa/transform-content transform-mtx)) - (cond-> (not path?) - (assoc :x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect))) - (cond-> transform - (-> (assoc :transform transform) - (assoc :transform-inverse transform-inverse))) - (cond-> (not transform) - (dissoc :transform :transform-inverse)) - (cond-> (some? selrect) - (assoc :selrect selrect)) + (let [type (dm/get-prop shape :type) + rotation (mod (+ (d/nilv (:rotation shape) 0) + (d/nilv (dm/get-in shape [:modifiers :rotation]) 0)) + 360) + shape (if (= type :bool) + (update shape :bool-content gpa/transform-content transform-mtx) + shape) - (cond-> (d/not-empty? points) - (assoc :points points)) - (assoc :rotation rotation))))) + shape (if (= type :path) + (update shape :content gpa/transform-content transform-mtx) + (cr/assoc! shape + :x (dm/get-prop selrect :x) + :y (dm/get-prop selrect :y) + :width (dm/get-prop selrect :width) + :height (dm/get-prop selrect :height)))] + (-> shape + (cr/assoc! :transform transform) + (cr/assoc! :transform-inverse inverse) + (cr/assoc! :selrect selrect) + (cr/assoc! :points points) + (cr/assoc! :rotation rotation)))))) (defn- apply-transform "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" [shape transform-mtx] - (if (gmt/move? transform-mtx) + (if ^boolean (gmt/move? transform-mtx) (apply-transform-move shape transform-mtx) (apply-transform-generic shape transform-mtx))) @@ -385,7 +400,7 @@ (let [;; Points for every shape inside the group points (->> children (mapcat :points)) - shape-center (gco/center-points points) + shape-center (gco/points->center points) ;; Fixed problem with empty groups. Should not happen (but it does) points (if (empty? points) (:points group) points) @@ -393,13 +408,14 @@ ;; Invert to get the points minus the transforms applied to the group base-points (gco/transform-points points shape-center (:transform-inverse group (gmt/matrix))) + ;; FIXME: looks redundant operation points -> rect -> points ;; Defines the new selection rect with its transformations - new-points (-> (gpr/points->selrect base-points) - (gpr/rect->points) + new-points (-> (grc/points->rect base-points) + (grc/rect->points) (gco/transform-points shape-center (:transform group (gmt/matrix)))) ;; Calculate the new selrect - new-selrect (gpr/points->selrect base-points)] + new-selrect (grc/points->rect base-points)] ;; Updates the shape and the applytransform-rect will update the other properties (-> group @@ -448,21 +464,16 @@ (transform-shape modifiers)))) ([shape modifiers] - (letfn [(apply-modifiers - [shape modifiers] - (if (ctm/empty? modifiers) - shape - (let [transform (ctm/modifiers->transform modifiers)] - (cond-> shape - (and (some? transform) (not= uuid/zero (:id shape))) ;; Never transform the root frame - (apply-transform transform) + (if (and (some? modifiers) (not (ctm/empty? modifiers))) + (let [transform (ctm/modifiers->transform modifiers)] + (cond-> shape + (and (some? transform) + (not (cph/root? shape))) + (apply-transform transform) - (ctm/has-structure? modifiers) - (ctm/apply-structure-modifiers modifiers)))))] - - (cond-> shape - (and (some? modifiers) (not (ctm/empty? modifiers))) - (apply-modifiers modifiers))))) + (ctm/has-structure? modifiers) + (ctm/apply-structure-modifiers modifiers))) + shape))) (defn apply-objects-modifiers ([objects modifiers] @@ -492,24 +503,16 @@ (defn transform-selrect [selrect modifiers] (-> selrect - (gpr/rect->points) + (grc/rect->points) (transform-bounds modifiers) - (gpr/points->selrect))) + (grc/points->rect))) (defn transform-selrect-matrix [selrect mtx] (-> selrect - (gpr/rect->points) + (grc/rect->points) (gco/transform-points mtx) - (gpr/points->selrect))) - -(defn selection-rect - "Returns a rect that contains all the shapes and is aware of the - rotation of each shape. Mainly used for multiple selection." - [shapes] - (->> shapes - (map (comp gpr/points->selrect :points transform-shape)) - (gpr/join-selrects))) + (grc/points->rect))) (declare apply-group-modifiers) diff --git a/common/src/app/common/geom/snap.cljc b/common/src/app/common/geom/snap.cljc new file mode 100644 index 0000000000..04e19c11b2 --- /dev/null +++ b/common/src/app/common/geom/snap.cljc @@ -0,0 +1,61 @@ +;; 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.common.geom.snap + (:require + [app.common.data.macros :as dm] + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph] + [app.common.types.shape-tree :as ctst])) + +(defn rect->snap-points + [rect] + (let [x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + #{(gpt/point x y) + (gpt/point (+ x w) y) + (gpt/point (+ x w) (+ y h)) + (gpt/point x (+ y h)) + (grc/rect->center rect)})) + +(defn- frame->snap-points + [frame] + (let [points (dm/get-prop frame :points) + rect (grc/points->rect points) + x (dm/get-prop rect :x) + y (dm/get-prop rect :y) + w (dm/get-prop rect :width) + h (dm/get-prop rect :height)] + (into (rect->snap-points rect) + #{(gpt/point (+ x (/ w 2)) y) + (gpt/point (+ x w) (+ y (/ h 2))) + (gpt/point (+ x (/ w 2)) (+ y h)) + (gpt/point x (+ y (/ h 2)))}))) + +(defn shape->snap-points + [shape] + (if ^boolean (cph/frame-shape? shape) + (frame->snap-points shape) + (->> (dm/get-prop shape :points) + (into #{(gsh/shape->center shape)})))) + +(defn guide->snap-points + [guide frame] + (cond + (and (some? frame) + (not ^boolean (ctst/rotated-frame? frame)) + (not ^boolean (cph/root-frame? frame))) + #{} + + (= :x (:axis guide)) + #{(gpt/point (:position guide) 0)} + + :else + #{(gpt/point 0 (:position guide))})) diff --git a/common/src/app/common/math.cljc b/common/src/app/common/math.cljc index 0adc340beb..688872c16a 100644 --- a/common/src/app/common/math.cljc +++ b/common/src/app/common/math.cljc @@ -6,9 +6,24 @@ (ns app.common.math "A collection of math utils." - (:refer-clojure :exclude [abs]) + (:refer-clojure :exclude [abs min max]) #?(:cljs - (:require [goog.math :as math]))) + (:require-macros [app.common.math :refer [min max]])) + (:require + #?(:cljs [goog.math :as math]) + [clojure.core :as c])) + +(defmacro min + [& params] + (if (:ns &env) + `(js/Math.min ~@params) + `(c/min ~@params))) + +(defmacro max + [& params] + (if (:ns &env) + `(js/Math.max ~@params) + `(c/max ~@params))) (def PI #?(:cljs (.-PI js/Math) @@ -177,7 +192,7 @@ (defn round-to-zero "Given a number if it's close enough to zero round to the zero to avoid precision problems" [num] - (if (almost-zero? num) + (if (< (abs num) 1e-4) 0 num)) @@ -198,10 +213,12 @@ (defn max-abs [a b] - (max (abs a) (abs b))) + (max (abs a) + (abs b))) (defn sign "Get the sign (+1 / -1) for the number" [n] (if (neg? n) -1 1)) + diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index 2d52f03c47..05d9e12c43 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -9,34 +9,11 @@ (:require [app.common.data.macros :as dm] [app.common.pages.changes :as changes] - [app.common.pages.common :as common] - [app.common.pages.focus :as focus] - [app.common.pages.indices :as indices] - [app.common.types.file :as ctf])) - -;; Common -(dm/export common/root) -(dm/export common/file-version) -(dm/export common/default-color) -(dm/export common/component-sync-attrs) -(dm/export common/retrieve-used-names) -(dm/export common/generate-unique-name) - -;; Focus -(dm/export focus/focus-objects) -(dm/export focus/filter-not-focus) -(dm/export focus/is-in-focus?) + [app.common.pages.indices :as indices])) ;; Indices -#_(dm/export indices/calculate-z-index) -#_(dm/export indices/update-z-index) (dm/export indices/generate-child-all-parents-index) -(dm/export indices/generate-child-parent-index) (dm/export indices/create-clip-index) ;; Process changes (dm/export changes/process-changes) - -;; Initialization -(dm/export ctf/make-file-data) -(dm/export ctf/empty-file-data) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 45854b9375..5b20daf5b6 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -12,7 +12,6 @@ [app.common.exceptions :as ex] [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.pages.common :refer [component-sync-attrs]] [app.common.pages.helpers :as cph] [app.common.schema :as sm] [app.common.schema.desc-native :as smd] @@ -20,6 +19,7 @@ [app.common.types.colors-list :as ctcl] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] + [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.file :as ctf] [app.common.types.page :as ctp] @@ -50,7 +50,7 @@ [:set-remote-synced [:map {:title "SetRemoteSyncedOperation"} [:type [:= :set-remote-synced]] - [:remote-synced? [:maybe :boolean]]]]]) + [:remote-synced {:optional true} [:maybe :boolean]]]]]) (sm/def! ::change [:schema @@ -68,11 +68,11 @@ [:map {:title "AddObjChange"} [:type [:= :add-obj]] [:id ::sm/uuid] - [:obj [:map-of {:gen/max 10} :keyword :any]] + [:obj :map] [:page-id {:optional true} ::sm/uuid] [:component-id {:optional true} ::sm/uuid] - [:frame-id {:optional true} ::sm/uuid] - [:parent-id {:optional true} ::sm/uuid] + [:frame-id ::sm/uuid] + [:parent-id {:optional true} [:maybe ::sm/uuid]] [:index {:optional true} [:maybe :int]] [:ignore-touched {:optional true} :boolean]]] @@ -227,11 +227,11 @@ (sm/def! ::changes [:sequential {:gen/max 2} ::change]) - -(def change? + +(def valid-change? (sm/pred-fn ::change)) -(def changes? +(def valid-changes? (sm/pred-fn [:sequential ::change])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -258,7 +258,8 @@ ;; If object has changed or is new verify is correct (when (and (some? shape-new) (not= shape-old shape-new)) - (dm/verify! (cts/shape? shape-new)))))] + (dm/verify! (and (cts/shape? shape-new) + (cts/valid-shape? shape-new))))))] (->> (into #{} (map :page-id) items) (mapcat (fn [page-id] @@ -283,7 +284,7 @@ ;; When verify? false we spec the schema validation. Currently used to make just ;; 1 validation even if the changes are applied twice (when verify? - (dm/verify! (changes? items))) + (dm/verify! (valid-changes? items))) (let [result (reduce #(or (process-change %1 %2) %1) data items)] ;; Validate result shapes (only on the backend) @@ -430,7 +431,7 @@ (= :bool (:type group)) (gsh/update-bool-selrect group children objects) - (:masked-group? group) + (:masked-group group) (set-mask-selrect group children) :else @@ -474,7 +475,7 @@ (#{:group :frame} (:type parent)) (not ignore-touched)) (-> (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?))))) + (dissoc :remote-synced))))) (remove-from-old-parent [old-objects objects shape-id] (let [prev-parent-id (dm/get-in old-objects [shape-id :parent-id])] @@ -493,7 +494,7 @@ (d/update-in-when [pid :shapes] d/vec-without-nils) (cond-> component? (d/update-when pid #(-> % (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?))))))))) + (dissoc :remote-synced))))))))) (update-parent-id [objects id] (-> objects (d/update-when id assoc :parent-id parent-id))) @@ -639,7 +640,7 @@ (defmethod process-operation :set [on-changed shape op] (let [attr (:attr op) - group (get component-sync-attrs attr) + group (get ctk/sync-attrs attr) val (:val op) shape-val (get shape attr) ignore (:ignore-touched op) @@ -675,7 +676,7 @@ (and in-copy? group (not ignore) (not equal?) (not (and ignore-geometry is-geometry?))) (-> (update :touched cph/set-touched-group group) - (dissoc :remote-synced?)) + (dissoc :remote-synced)) (nil? val) (dissoc attr) @@ -693,11 +694,11 @@ (defmethod process-operation :set-remote-synced [_ shape op] - (let [remote-synced? (:remote-synced? op) + (let [remote-synced (:remote-synced op) in-copy? (ctk/in-component-copy? shape)] - (if (or (not in-copy?) (not remote-synced?)) - (dissoc shape :remote-synced?) - (assoc shape :remote-synced? true)))) + (if (or (not in-copy?) (not remote-synced)) + (dissoc shape :remote-synced) + (assoc shape :remote-synced true)))) (defmethod process-operation :default [_ _ op] @@ -725,19 +726,18 @@ ; We need to trigger a sync if the shape has changed any ; attribute that participates in components synchronization. (and (= (:type operation) :set) - (component-sync-attrs (:attr operation)))) + (get ctk/sync-attrs (:attr operation)))) any-sync? (some need-sync? operations)] (when any-sync? - (let [xform (comp (filter :main-instance?) ; Select shapes that are main component instances + (let [xform (comp (filter :main-instance) ; Select shapes that are main component instances (map :component-id))] (into #{} xform shape-and-parents)))))) (defmethod components-changed :mov-objects [file-data {:keys [page-id _component-id parent-id shapes] :as change}] (when page-id - (let [page (ctpl/get-page file-data page-id) - - xform (comp (filter :main-instance?) + (let [page (ctpl/get-page file-data page-id) + xform (comp (filter :main-instance) (map :component-id)) check-shape @@ -756,7 +756,7 @@ (let [page (ctpl/get-page file-data page-id) shape-and-parents (map (partial ctn/get-shape page) (cons id (cph/get-parent-ids (:objects page) id))) - xform (comp (filter :main-instance?) + xform (comp (filter :main-instance) (map :component-id))] (into #{} xform shape-and-parents)))) diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index 8e2588d4e5..19959f6833 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -11,6 +11,7 @@ [app.common.files.features :as ffeat] [app.common.geom.matrix :as gmt] [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.pages :as cp] @@ -217,6 +218,9 @@ (add-object changes obj nil)) ([changes obj {:keys [index ignore-touched] :or {index ::undefined ignore-touched false}}] + + ;; FIXME: add shape validation + (assert-page-id changes) (assert-objects changes) (let [obj (cond-> obj @@ -234,7 +238,7 @@ :frame-id (:frame-id obj) :index (::index obj) :ignore-touched ignore-touched - :obj (dissoc obj ::index :parent-id)} + :obj (dissoc obj ::index)} del-change {:type :del-obj @@ -469,7 +473,7 @@ (every? #(apply gpt/close? %) (d/zip old-val new-val)) (= attr :selrect) - (gsh/close-selrect? old-val new-val) + (grc/close-rect? old-val new-val) :else (= old-val new-val))] @@ -491,7 +495,7 @@ (gsh/update-bool-selrect parent children objects) (= (:type parent) :group) - (if (:masked-group? parent) + (if (:masked-group parent) (gsh/update-mask-selrect parent children) (gsh/update-group-selrect parent children)))] (if resized-parent @@ -624,11 +628,11 @@ :attr :component-file :val (:component-file shape)} {:type :set - :attr :component-root? - :val (:component-root? shape)} + :attr :component-root + :val (:component-root shape)} {:type :set - :attr :main-instance? - :val (:main-instance? shape)} + :attr :main-instance + :val (:main-instance shape)} {:type :set :attr :shape-ref :val (:shape-ref shape)} diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 2f7fdcdd2e..299fd26243 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -22,8 +22,9 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn root? - [{:keys [id type]}] - (and (= type :frame) (= id uuid/zero))) + [shape] + (and (= (dm/get-prop shape :type) :frame) + (= (dm/get-prop shape :id) uuid/zero))) (defn root-frame? ([objects id] @@ -35,60 +36,71 @@ (defn frame-shape? ([objects id] (frame-shape? (get objects id))) - ([{:keys [type]}] - (= type :frame))) + ([shape] + (and (some? shape) + (= :frame (dm/get-prop shape :type))))) (defn group-shape? ([objects id] (group-shape? (get objects id))) - ([{:keys [type]}] - (= type :group))) + ([shape] + (and (some? shape) + (= :group (dm/get-prop shape :type))))) (defn mask-shape? + ([shape] + (and ^boolean (group-shape? shape) + ^boolean (:masked-group shape))) ([objects id] - (mask-shape? (get objects id))) - ([{:keys [type masked-group?]}] - (and (= type :group) masked-group?))) + (mask-shape? (get objects id)))) (defn bool-shape? - [{:keys [type]}] - (= type :bool)) + [shape] + (and (some? shape) + (= :bool (dm/get-prop shape :type)))) (defn group-like-shape? - [{:keys [type]}] - (or (= :group type) (= :bool type))) + [shape] + (or ^boolean (group-shape? shape) + ^boolean (bool-shape? shape))) (defn text-shape? - [{:keys [type]}] - (= type :text)) + [shape] + (and (some? shape) + (= :text (dm/get-prop shape :type)))) (defn rect-shape? - [{:keys [type]}] - (= type :rect)) + [shape] + (and (some? shape) + (= :rect (dm/get-prop shape :type)))) (defn circle-shape? [{:keys [type]}] (= type :circle)) (defn image-shape? - [{:keys [type]}] - (= type :image)) + [shape] + (and (some? shape) + (= :image (dm/get-prop shape :type)))) (defn svg-raw-shape? - [{:keys [type]}] - (= type :svg-raw)) + [shape] + (and (some? shape) + (= :svg-raw (dm/get-prop shape :type)))) (defn path-shape? ([objects id] (path-shape? (get objects id))) - ([{:keys [type]}] - (= type :path))) + ([shape] + (and (some? shape) + (= :path (dm/get-prop shape :type))))) (defn unframed-shape? "Checks if it's a non-frame shape in the top level." [shape] - (and (not (frame-shape? shape)) - (= (:frame-id shape) uuid/zero))) + (and (some? shape) + (not (frame-shape? shape)) + (= (dm/get-prop shape :frame-id) uuid/zero))) (defn has-children? ([objects id] @@ -96,10 +108,11 @@ ([shape] (d/not-empty? (:shapes shape)))) +;; ---- ACCESSORS + (defn get-children-ids [objects id] - (letfn [(get-children-ids-rec - [id processed] + (letfn [(get-children-ids-rec [id processed] (when (not (contains? processed id)) (when-let [shapes (-> (get objects id) :shapes (some-> vec))] (into shapes (mapcat #(get-children-ids-rec % (conj processed id))) shapes))))] @@ -120,19 +133,21 @@ (defn get-parent "Retrieve the id of the parent for the shape-id (if exists)" [objects id] - (let [lookup (d/getf objects)] - (-> id lookup :parent-id lookup))) + (when-let [shape (get objects id)] + (get objects (dm/get-prop shape :parent-id)))) (defn get-parent-id "Retrieve the id of the parent for the shape-id (if exists)" [objects id] - (-> objects (get id) :parent-id)) + (when-let [shape (get objects id)] + (dm/get-prop shape :parent-id))) (defn get-parent-ids "Returns a vector of parents of the specified shape." [objects shape-id] - (loop [result [] id shape-id] - (let [parent-id (dm/get-in objects [id :parent-id])] + (loop [result [] + id shape-id] + (let [parent-id (get-parent-id objects id)] (if (and (some? parent-id) (not= parent-id id)) (recur (conj result parent-id) parent-id) result)))) @@ -154,12 +169,12 @@ (defn hidden-parent? "Checks the parent for the hidden property" [objects shape-id] - (let [parent-id (dm/get-in objects [shape-id :parent-id])] - (cond - (or (nil? parent-id) (nil? shape-id) (= shape-id uuid/zero) (= parent-id uuid/zero)) false - (dm/get-in objects [parent-id :hidden]) true - :else - (recur objects parent-id)))) + (let [parent-id (get-parent-id objects shape-id)] + (if (or (nil? parent-id) (nil? shape-id) (= shape-id uuid/zero) (= parent-id uuid/zero)) + false + (if ^boolean (dm/get-in objects [parent-id :hidden]) + true + (recur objects parent-id))))) (defn get-parent-ids-with-index "Returns a tuple with the list of parents and a map with the position within each parent" @@ -167,10 +182,10 @@ (loop [parent-list [] parent-indices {} current shape-id] - (let [parent-id (dm/get-in objects [current :parent-id]) - parent (get objects parent-id)] + (let [parent-id (get-parent-id objects current) + parent (get objects parent-id)] (if (and (some? parent) (not= parent-id current)) - (let [parent-list (conj parent-list parent-id) + (let [parent-list (conj parent-list parent-id) parent-indices (assoc parent-indices parent-id (d/index-of (:shapes parent) current))] (recur parent-list parent-indices parent-id)) [parent-list parent-indices])))) @@ -178,7 +193,7 @@ (defn get-siblings-ids [objects id] (let [parent (get-parent objects id)] - (into [] (->> (:shapes parent) (remove #(= % id)))))) + (into [] (remove #(= % id)) (:shapes parent)))) (defn get-frame "Get the frame that contains the shape. If the shape is already a @@ -190,7 +205,7 @@ (map? shape-or-id) (if (frame-shape? shape-or-id) shape-or-id - (get objects (:frame-id shape-or-id))) + (get objects (dm/get-prop shape-or-id :frame-id))) (= uuid/zero shape-or-id) (get objects uuid/zero) diff --git a/common/src/app/common/pages/indices.cljc b/common/src/app/common/pages/indices.cljc index 7a5e8ef1ae..e8a0b274ca 100644 --- a/common/src/app/common/pages/indices.cljc +++ b/common/src/app/common/pages/indices.cljc @@ -9,13 +9,6 @@ [app.common.pages.helpers :as cph] [app.common.uuid :as uuid])) -(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" @@ -42,7 +35,7 @@ (not= uuid/zero (:id shape))) (conj shape) - (:masked-group? shape) + (:masked-group shape) (conj (get objects (->> shape :shapes first))) (= :bool (:type shape)) diff --git a/common/src/app/common/path/bool.cljc b/common/src/app/common/path/bool.cljc index 97333e7f02..5a72e687f9 100644 --- a/common/src/app/common/path/bool.cljc +++ b/common/src/app/common/path/bool.cljc @@ -8,8 +8,8 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.path :as gsp] - [app.common.geom.shapes.rect :as gpr] [app.common.path.commands :as upc] [app.common.path.subpaths :as ups])) @@ -101,7 +101,7 @@ (if (= :move-to (:command segment)) false (let [r1 (command->selrect segment)] - (gpr/overlaps-rects? r1 selrect)))) + (grc/overlaps-rects? r1 selrect)))) (overlap-segments? [seg-1 seg-2] @@ -110,7 +110,7 @@ false (let [r1 (command->selrect seg-1) r2 (command->selrect seg-2)] - (gpr/overlaps-rects? r1 r2)))) + (grc/overlaps-rects? r1 r2)))) (split [seg-1 seg-2] @@ -156,7 +156,7 @@ :curve-to (-> (gsp/command->bezier segment) (gsp/curve-values 0.5)))] - (and (gpr/contains-point? content-sr point) + (and (grc/contains-point? content-sr point) (or (gsp/is-point-in-geom-data? point content-geom) (gsp/is-point-in-border? point content))))) @@ -170,7 +170,7 @@ :curve-to (-> (gsp/command->bezier segment) (gsp/curve-values 0.5)))] - (and (gpr/contains-point? content-sr point) + (and (grc/contains-point? content-sr point) (gsp/is-point-in-geom-data? point content-geom)))) (defn overlap-segment? diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc index 99caf6356a..6f43e0a24e 100644 --- a/common/src/app/common/path/shapes_to_path.cljc +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -10,7 +10,7 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gsc] + [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.corners :as gso] [app.common.geom.shapes.path :as gsp] [app.common.path.bool :as pb] @@ -231,7 +231,7 @@ new-content (cond-> new-content (some? transform) - (gsp/transform-content (gmt/transform-in (gsc/center-shape shape) transform)))] + (gsp/transform-content (gmt/transform-in (gco/shape->center shape) transform)))] (-> shape (assoc :type :path) diff --git a/common/src/app/common/record.cljc b/common/src/app/common/record.cljc new file mode 100644 index 0000000000..e8abbfc700 --- /dev/null +++ b/common/src/app/common/record.cljc @@ -0,0 +1,267 @@ +;; 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.common.record + "A collection of helpers and macros for defien a penpot customized record types." + (:refer-clojure :exclude [defrecord assoc! clone]) + #?(:cljs (:require-macros [app.common.record]))) + +#_:clj-kondo/ignore +(defmacro caching-hash + [coll hash-fn hash-key] + `(let [h# ~hash-key] + (if-not (nil? h#) + h# + (let [h# (~hash-fn ~coll)] + (set! ~hash-key h#) + h#)))) + +#?(:clj + (defn- property-symbol + [sym] + (symbol (str "-" (name sym))))) + +#?(:clj + (defn- generate-field-access + [this-sym val-sym fields] + (map (fn [field] + (cond + (nil? field) nil + (identical? field val-sym) val-sym + :else `(. ~this-sym ~(property-symbol field)))) + fields))) + +#?(:clj + (defn emit-extend + [env tagname fields impls] + (let [base-fields (mapv #(with-meta % nil) fields) + fields (conj base-fields '$meta '$extmap (with-meta '$hash {:mutable true})) + key-sym (gensym "key-") + val-sym (gensym "val-") + this-sym (with-meta (gensym "this-") {:tag tagname}) + other-sym (gensym "other-") + pr-open (str "#" (-> env :ns :name) "." (name tagname) "{")] + (concat impls + ['cljs.core/ICloneable + `(~'-clone [~this-sym] + (new ~tagname ~@(generate-field-access this-sym val-sym fields))) + + 'IHash + `(~'-hash [~this-sym] + (caching-hash ~this-sym + (fn [coll#] + (bit-xor + ~(hash (str tagname)) + (cljs.core/hash-unordered-coll coll#))) + (. ~this-sym ~'-$hash))) + + 'cljs.core/IEquiv + `(~'-equiv [~this-sym ~other-sym] + (and (some? ~other-sym) + (identical? (.-constructor ~this-sym) + (.-constructor ~other-sym)) + ~@(map (fn [field] + `(= (.. ~this-sym ~(property-symbol field)) + (.. ~(with-meta other-sym {:tag tagname}) ~(property-symbol field)))) + base-fields) + (= (. ~this-sym ~'-$extmap) + (. ~(with-meta other-sym {:tag tagname}) ~'-$extmap)))) + + 'cljs.core/IMeta + `(~'-meta [~this-sym] (. ~this-sym ~'-$meta)) + + 'cljs.core/IWithMeta + `(~'-with-meta [~this-sym ~val-sym] + (new ~tagname ~@(->> (replace {'$meta val-sym} fields) + (generate-field-access this-sym val-sym)))) + + 'cljs.core/ILookup + `(~'-lookup + ([~this-sym k#] + (cljs.core/-lookup ~this-sym k# nil)) + ([~this-sym ~key-sym else#] + (case ~key-sym + ~@(mapcat (fn [f] [(keyword f) `(. ~this-sym ~(property-symbol f))]) + base-fields) + (cljs.core/get (. ~this-sym ~'-$extmap) ~key-sym else#)))) + + 'cljs.core/ICounted + `(~'-count [~this-sym] + (+ ~(count base-fields) (count (. ~this-sym ~'-$extmap)))) + + 'cljs.core/ICollection + `(~'-conj [~this-sym ~val-sym] + (if (vector? ~val-sym) + (cljs.core/-assoc ~this-sym (cljs.core/-nth ~val-sym 0) (cljs.core/-nth ~val-sym 1)) + (reduce cljs.core/-conj ~this-sym ~val-sym))) + + 'cljs.core/IAssociative + `(~'-contains-key? [~this-sym ~key-sym] + ~(if (seq base-fields) + `(case ~key-sym + (~@(map keyword base-fields)) true + (contains? (. ~this-sym ~'-$extmap) ~key-sym)) + `(contains? (. ~this-sym ~'-$extmap) ~key-sym))) + + `(~'-assoc [~this-sym ~key-sym ~val-sym] + (case ~key-sym + ~@(mapcat (fn [fld] + [(keyword fld) `(new ~tagname ~@(->> (replace {fld val-sym '$hash nil} fields) + (generate-field-access this-sym val-sym)))]) + base-fields) + (new ~tagname ~@(->> (remove #{'$extmap '$hash} fields) + (generate-field-access this-sym val-sym)) + (assoc (. ~this-sym ~'-$extmap) ~key-sym ~val-sym) nil))) + + 'cljs.core/ITransientAssociative + `(~'-assoc! [~this-sym ~key-sym ~val-sym] + (let [key# (if (keyword? ~key-sym) + (.-fqn ~(with-meta key-sym {:tag `cljs.core/Keyword})) + ~key-sym)] + (case ~key-sym + ~@(mapcat + (fn [f] + [(keyword f) `(set! (. ~this-sym ~(property-symbol f)) ~val-sym)]) + base-fields) + + (set! (. ~this-sym ~'-$extmap) (cljs.core/assoc (. ~this-sym ~'-$extmap) ~key-sym ~val-sym))) + + ~this-sym)) + + 'cljs.core/IMap + `(~'-dissoc [~this-sym ~key-sym] + (case ~key-sym + (~@(map keyword base-fields)) + (cljs.core/-assoc ~this-sym ~key-sym nil) + + (let [extmap1# (. ~this-sym ~'-$extmap) + extmap2# (dissoc extmap1# ~key-sym)] + (if (identical? extmap1# extmap2#) + ~this-sym + (new ~tagname ~@(->> (remove #{'$extmap '$hash} fields) + (generate-field-access this-sym val-sym)) + (not-empty extmap2#) + nil))))) + + 'cljs.core/ISeqable + `(~'-seq [~this-sym] + (seq (concat [~@(map (fn [f] + `(cljs.core/MapEntry. + ~(keyword f) + (. ~this-sym ~(property-symbol f)) + nil)) + base-fields)] + (. ~this-sym ~'-$extmap)))) + + 'cljs.core/IIterable + `(~'-iterator [~this-sym] + (cljs.core/RecordIter. 0 ~this-sym ~(count base-fields) + [~@(map keyword base-fields)] + (if (. ~this-sym ~'-$extmap) + (cljs.core/-iterator (. ~this-sym ~'-$extmap)) + (cljs.core/nil-iter)))) + + 'cljs.core/IKVReduce + `(~'-kv-reduce [~this-sym f# init#] + (reduce (fn [ret# [~key-sym v#]] (f# ret# ~key-sym v#)) init# ~this-sym)) + + 'cljs.core/IPrintWithWriter + `(~'-pr-writer [~this-sym writer# opts#] + (let [pr-pair# (fn [keyval#] + (cljs.core/pr-sequential-writer writer# (~'js* "cljs.core.pr_writer") + "" " " "" opts# keyval#))] + (cljs.core/pr-sequential-writer + writer# pr-pair# ~pr-open ", " "}" opts# + (concat [~@(for [f base-fields] + `(vector ~(keyword f) (. ~this-sym ~(property-symbol f))))] + (. ~this-sym ~'-$extmap))))) + + ])))) + +(defmacro defrecord + [rsym fields & impls] + (let [param (gensym "param-") + ks (map keyword fields)] + (if (:ns &env) + `(do + (deftype ~rsym ~(into fields ['$meta '$extmap '$hash])) + (extend-type ~rsym ~@(emit-extend &env rsym fields impls)) + + (defn ~(with-meta (symbol (str "pos->" rsym)) + (assoc (meta rsym) :factory :positional)) + [~@fields] + (new ~rsym ~@(conj fields nil nil nil))) + + (defn ~(with-meta (symbol (str 'map-> rsym)) + (assoc (meta rsym) :factory :map)) + [~param] + (let [exclude# #{~@ks} + extmap# (reduce-kv (fn [acc# k# v#] + (if (contains? exclude# k#) + acc# + (assoc acc# k# v#))) + {} + ~param)] + (new ~rsym + ~@(for [k ks] + `(get ~param ~k)) + nil + (not-empty extmap#) + nil))) + ~rsym) + + `(do + (clojure.core/defrecord ~rsym ~fields ~@impls) + (defn ~(with-meta (symbol (str "pos->" rsym)) + (assoc (meta rsym) :factory :positional)) + [~@(map (fn [f] (vary-meta f dissoc :tag)) fields)] + (new ~rsym ~@(conj fields nil nil))))))) + +(defmacro clone + [ssym] + (if (:ns &env) + `(cljs.core/clone ~ssym) + ssym)) + +(defmacro assoc! + "A record specific update operation" + [ssym & pairs] + (if (:ns &env) + (let [pairs (partition-all 2 pairs)] + `(-> ~ssym + ~@(map (fn [[ks vs]] + `(cljs.core/-assoc! ~ks ~vs)) + pairs))) + `(assoc ~ssym ~@pairs))) + +(defmacro update! + "A record specific update operation" + [ssym ksym f & params] + (if (:ns &env) + (let [ssym (with-meta ssym {:tag 'js})] + `(cljs.core/assoc! ~ssym ~ksym (~f (. ~ssym ~(property-symbol ksym)) ~@params))) + `(update ~ssym ~ksym ~f ~@params))) + +(defmacro define-properties! + [rsym & properties] + (let [rsym (with-meta rsym {:tag 'js})] + `(do + ~@(for [params properties + :let [pname (get params :name) + get-fn (get params :get) + set-fn (get params :set)]] + `(.defineProperty js/Object + (.-prototype ~rsym) + ~pname + (cljs.core/js-obj + "enumerable" true + "configurable" true + ~@(concat + (when get-fn + ["get" get-fn]) + (when set-fn + ["set" set-fn])))))))) + diff --git a/common/src/app/common/schema/generators.cljc b/common/src/app/common/schema/generators.cljc index f9955b49d8..cbe541edd6 100644 --- a/common/src/app/common/schema/generators.cljc +++ b/common/src/app/common/schema/generators.cljc @@ -82,7 +82,6 @@ ext (tg/elements ["net" "com" "org" "app" "io"])] (u/uri (str scheme "://" domain "." ext)))) -;; FIXME: revisit (defn uuid [] (->> tg/small-integer diff --git a/common/src/app/common/transit.cljc b/common/src/app/common/transit.cljc index 9d698cd3ac..057a1593d2 100644 --- a/common/src/app/common/transit.cljc +++ b/common/src/app/common/transit.cljc @@ -7,8 +7,6 @@ (ns app.common.transit (:require [app.common.data :as d] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] [app.common.uri :as uri] [cognitect.transit :as t] [lambdaisland.uri :as luri] @@ -18,8 +16,6 @@ #?(:cljs ["luxon" :as lxn])) #?(:clj (:import - app.common.geom.matrix.Matrix - app.common.geom.point.Point java.io.ByteArrayInputStream java.io.ByteArrayOutputStream java.io.File @@ -122,23 +118,6 @@ {:id "u" :rfn parse-uuid}) - {:id "point" - :class #?(:clj Point :cljs gpt/Point) - :wfn #(into {} %) - :rfn gpt/map->Point} - - {:id "matrix" - :class #?(:clj Matrix :cljs gmt/Matrix) - :wfn #(into {} %) - :rfn #?(:cljs gmt/map->Matrix - :clj (fn [{:keys [a b c d e f]}] - (gmt/matrix (double a) - (double b) - (double c) - (double d) - (double e) - (double f))))} - {:id "ordered-set" :class #?(:clj LinkedSet :cljs lks/LinkedSet) :wfn vec diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 1101e21366..b94ecf25e6 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -6,10 +6,97 @@ (ns app.common.types.component) +;; Attributes that may be synced in components, and the group they belong to. +;; When one attribute is modified in a shape inside a component, the corresponding +;; group is marked as :touched. Then, if the shape is synced with the remote shape +;; in the main component, none of the attributes of the same group is changed. + +(def sync-attrs + {:name :name-group + :fills :fill-group + :hide-fill-on-export :fill-group + :content :content-group + :position-data :content-group + :hidden :visibility-group + :blocked :modifiable-group + :grow-type :text-font-group + :font-family :text-font-group + :font-size :text-font-group + :font-style :text-font-group + :font-weight :text-font-group + :letter-spacing :text-display-group + :line-height :text-display-group + :text-align :text-display-group + :strokes :stroke-group + + ;; DEPRECATED: FIXME: this attrs are deprecated for a long time but + ;; we still have tests that uses this attribute for synchronization + :stroke-width :stroke-group + :fill-color :fill-group + :fill-opacity :fill-group + + :rx :radius-group + :ry :radius-group + :r1 :radius-group + :r2 :radius-group + :r3 :radius-group + :r4 :radius-group + :type :geometry-group + :selrect :geometry-group + :points :geometry-group + :locked :geometry-group + :proportion :geometry-group + :proportion-lock :geometry-group + :x :geometry-group + :y :geometry-group + :width :geometry-group + :height :geometry-group + :rotation :geometry-group + :transform :geometry-group + :transform-inverse :geometry-group + :opacity :layer-effects-group + :blend-mode :layer-effects-group + :shadow :shadow-group + :blur :blur-group + :masked-group :mask-group + :constraints-h :constraints-group + :constraints-v :constraints-group + :fixed-scroll :constraints-group + :exports :exports-group + + :layout :layout-container + :layout-align-content :layout-container + :layout-align-items :layout-container + :layout-flex-dir :layout-container + :layout-gap :layout-container + :layout-gap-type :layout-container + :layout-justify-content :layout-container + :layout-justify-items :layout-container + :layout-wrap-type :layout-container + :layout-padding-type :layout-container + :layout-padding :layout-container + :layout-h-orientation :layout-container + :layout-v-orientation :layout-container + :layout-grid-dir :layout-container + :layout-grid-rows :layout-container + :layout-grid-columns :layout-container + :layout-grid-cells :layout-container + + :layout-item-margin :layout-item + :layout-item-margin-type :layout-item + :layout-item-h-sizing :layout-item + :layout-item-v-sizing :layout-item + :layout-item-max-h :layout-item + :layout-item-min-h :layout-item + :layout-item-max-w :layout-item + :layout-item-min-w :layout-item + :layout-item-align-self :layout-item}) + + (defn instance-root? "Check if this shape is the head of a top instance." [shape] - (some? (:component-root? shape))) + (some? (:component-root shape))) (defn instance-head? "Check if this shape is the head of a top instance or a subinstance." @@ -36,9 +123,10 @@ (= (:shape-ref shape-inst) (:shape-ref shape-main))))) (defn main-instance? - "Check if this shape is the root of the main instance of some component." + "Check if this shape is the root of the main instance of some + component." [shape] - (some? (:main-instance? shape))) + (some? (:main-instance shape))) (defn in-component-copy? "Check if the shape is inside a component non-main instance." @@ -63,7 +151,7 @@ (if (some? (:main-instance-id component)) (get-in component [:objects (:main-instance-id component)]) (get-in component [:objects (:id component)]))) - + (defn uses-library-components? "Check if the shape uses any component in the given library." [shape library-id] @@ -76,7 +164,7 @@ (dissoc shape :component-id :component-file - :component-root? - :remote-synced? + :component-root + :remote-synced :shape-ref :touched)) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 1cb7f98bf7..a54dbac099 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -7,9 +7,10 @@ (ns app.common.types.container (:require [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.common :as common] + [app.common.pages.helpers :as cph] [app.common.schema :as sm] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] @@ -100,7 +101,7 @@ (nil? shape) nil - (= uuid/zero (:id shape)) + (cph/root-frame? shape) nil (and (not (ctk/in-component-copy? shape)) (not allow-main?)) @@ -119,7 +120,7 @@ a main component have not any discriminating attribute." [objects shape] (let [component-shape (get-component-shape objects shape {:allow-main? true})] - (:main-instance? component-shape))) + (:main-instance component-shape))) (defn in-any-component? "Check if the shape is part of any component (main or copy), wether it's @@ -146,7 +147,7 @@ (cond-> new-shape true - (dissoc :component-root?) + (dissoc :component-root) (nil? (:parent-id new-shape)) (dissoc :component-id @@ -165,13 +166,13 @@ (nil? (:parent-id new-shape)) (assoc :component-id (:id new-shape) :component-file file-id - :component-root? true) + :component-root true) (and (nil? (:parent-id new-shape)) components-v2) - (assoc :main-instance? true) + (assoc :main-instance true) (some? (:parent-id new-shape)) - (dissoc :component-root?))) + (dissoc :component-root))) [new-root-shape new-shapes updated-shapes] (ctst/clone-object shape nil objects update-new-shape update-original-shape) @@ -186,9 +187,10 @@ (defn make-component-instance "Generate a new instance of the component inside the given container. - Clone the shapes of the component, generating new names and ids, and linking - each new shape to the corresponding one of the component. Place the new instance - coordinates in the given position." + Clone the shapes of the component, generating new names and ids, and + linking each new shape to the corresponding one of the + component. Place the new instance coordinates in the given + position." ([container component library-data position components-v2] (make-component-instance container component library-data position components-v2 {})) @@ -197,17 +199,19 @@ :or {main-instance? false force-id nil force-frame-id nil keep-ids? false}}] (let [component-page (when components-v2 (ctpl/get-page library-data (:main-instance-page component))) + component-shape (if components-v2 (-> (get-shape component-page (:main-instance-id component)) (assoc :parent-id nil) (assoc :frame-id uuid/zero)) (get-shape component (:id component))) + orig-pos (gpt/point (:x component-shape) (:y component-shape)) delta (gpt/subtract position orig-pos) objects (:objects container) - unames (volatile! (common/retrieve-used-names objects)) + unames (volatile! (cfh/get-used-names objects)) frame-id (or force-frame-id (ctst/frame-id-by-position objects @@ -231,10 +235,10 @@ (dissoc :touched)) main-instance? - (assoc :main-instance? true) + (assoc :main-instance true) (not main-instance?) - (dissoc :main-instance?) + (dissoc :main-instance) (and (not main-instance?) (nil? (:shape-ref original-shape))) (assoc :shape-ref (:id original-shape)) @@ -242,14 +246,14 @@ (nil? (:parent-id original-shape)) (assoc :component-id (:id component) :component-file (:id library-data) - :component-root? true + :component-root true :name new-name) (and (nil? (:parent-id original-shape)) main-instance? components-v2) - (assoc :main-instance? true) + (assoc :main-instance true) (some? (:parent-id original-shape)) - (dissoc :component-root?)))) + (dissoc :component-root)))) [new-shape new-shapes _] (ctst/clone-object component-shape diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 5c3a5d12a2..79d7b651f1 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -8,10 +8,10 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.defaults :refer [version]] [app.common.files.features :as ffeat] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.pages.common :refer [file-version]] [app.common.pages.helpers :as cph] [app.common.schema :as sm] [app.common.types.color :as ctc] @@ -68,7 +68,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def empty-file-data - {:version file-version + {:version version :pages [] :pages-index {}}) @@ -79,9 +79,8 @@ ([file-id page-id] (let [page (when (some? page-id) (ctp/make-empty-page page-id "Page 1"))] - (cond-> (-> empty-file-data - (assoc :id file-id)) + (cond-> (assoc empty-file-data :id file-id) (some? page-id) (ctpl/add-page page) @@ -291,7 +290,7 @@ been modified after the given date." [file-data library since-date] (letfn [(used-assets-shape [shape] - (concat + (concat (ctkl/used-components-changed-since shape library since-date) (ctcl/used-colors-changed-since shape library since-date) (ctyl/used-typographies-changed-since shape library since-date))) @@ -299,7 +298,7 @@ (used-assets-container [container] (->> (mapcat used-assets-shape (ctn/shapes-seq container)) (map #(cons (:id container) %))))] - + (mapcat used-assets-container (containers-seq file-data)))) (defn get-or-add-library-page @@ -347,7 +346,7 @@ file-data position false - {:main-instance? true + {:main-instance true :force-frame-id uuid/zero :keep-ids? true}) add-shapes @@ -430,7 +429,7 @@ library-data position (dm/get-in file-data [:options :components-v2]) - {:main-instance? true}) + {:main-instance true}) main-instance-shapes (map #(cond-> % @@ -590,9 +589,9 @@ (letfn [(show-shape [shape-id level objects] (let [shape (get objects shape-id)] (println (str/pad (str (str/repeat " " level) - (when (:main-instance? shape) "{") + (when (:main-instance shape) "{") (:name shape) - (when (:main-instance? shape) "}") + (when (:main-instance shape) "}") (when (seq (:touched shape)) "*") (when show-ids (str/format " <%s>" (:id shape)))) {:length 20 @@ -603,7 +602,7 @@ (println (str (str/repeat " " level) " " (str (:touched shape))))) - (when (:remote-synced? shape) + (when (:remote-synced shape) (println (str (str/repeat " " level) " (remote-synced)")))) (when (:shapes shape) @@ -612,7 +611,7 @@ (show-component-info [shape objects] (if (nil? (:shape-ref shape)) - (if (:component-root? shape) " #" "") + (if (:component-root shape) " #" "") (let [root-shape (ctn/get-component-shape objects shape) component-id (when root-shape (:component-id root-shape)) component-file-id (when root-shape (:component-file root-shape)) @@ -627,7 +626,7 @@ (get-ref-shape file-data component shape)))] (str/format " %s--> %s%s%s" - (cond (:component-root? shape) "#" + (cond (:component-root shape) "#" (:component-id shape) "@" :else "-") @@ -635,7 +634,7 @@ (or (:name component-shape) "?") - (if (or (:component-root? shape) + (if (or (:component-root shape) (nil? (:component-id shape)) true) "" @@ -672,4 +671,3 @@ (show-shape (:id component) 0 (:objects component))) (when (:main-instance-page component) (show-component-instance component))))))))) - diff --git a/common/src/app/common/types/modifiers.cljc b/common/src/app/common/types/modifiers.cljc index 2dc84309cb..8db511edd0 100644 --- a/common/src/app/common/types/modifiers.cljc +++ b/common/src/app/common/types/modifiers.cljc @@ -19,8 +19,7 @@ [app.common.pages.helpers :as cph] [app.common.text :as txt] [app.common.types.shape.layout :as ctl] - #?(:cljs [cljs.core :as c] - :clj [clojure.core :as c]))) + [clojure.core :as c])) ;; --- Modifiers @@ -106,18 +105,17 @@ [property value] (StructureOperation. :change-property property value nil)) - ;; Private aux functions (defn- move-vec? [vector] - (or (not (mth/almost-zero? (dm/get-prop vector :x))) - (not (mth/almost-zero? (dm/get-prop vector :y))))) + (or (not ^boolean (mth/almost-zero? (dm/get-prop vector :x))) + (not ^boolean (mth/almost-zero? (dm/get-prop vector :y))))) (defn- resize-vec? [vector] - (or (not (mth/almost-zero? (- (dm/get-prop vector :x) 1))) - (not (mth/almost-zero? (- (dm/get-prop vector :y) 1))))) + (or (not ^boolean (mth/almost-zero? (- (dm/get-prop vector :x) 1))) + (not ^boolean (mth/almost-zero? (- (dm/get-prop vector :y) 1))))) (defn- mergeable-move? [op1 op2] @@ -128,22 +126,24 @@ (defn- mergeable-resize? [op1 op2] (let [type-op1 (dm/get-prop op1 :type) - transform-op1 (or (dm/get-prop op1 :transform) (gmt/matrix)) - transform-inv-op1 (or (dm/get-prop op1 :transform-inverse) (gmt/matrix)) + transform-op1 (d/nilv (dm/get-prop op1 :transform) gmt/base) + transform-inv-op1 (d/nilv (dm/get-prop op1 :transform-inverse) gmt/base) origin-op1 (dm/get-prop op1 :origin) type-op2 (dm/get-prop op2 :type) - transform-op2 (or (dm/get-prop op2 :transform) (gmt/matrix)) - transform-inv-op2 (or (dm/get-prop op2 :transform-inverse) (gmt/matrix)) + transform-op2 (d/nilv (dm/get-prop op2 :transform) gmt/base) + transform-inv-op2 (d/nilv (dm/get-prop op2 :transform-inverse) gmt/base) origin-op2 (dm/get-prop op2 :origin)] - (and (= :resize type-op1) (= :resize type-op2) + + (and (= :resize type-op1) + (= :resize type-op2) ;; Same origin - (gpt/close? origin-op1 origin-op2) + ^boolean (gpt/close? origin-op1 origin-op2) ;; Same transforms - (gmt/close? transform-op1 transform-op2) - (gmt/close? transform-inv-op1 transform-inv-op2)))) + ^boolean (gmt/close? transform-op1 transform-op2) + ^boolean (gmt/close? transform-inv-op1 transform-inv-op2)))) (defn- merge-move [op1 op2] @@ -155,14 +155,15 @@ (defn- merge-resize [op1 op2] (let [op1-vector (dm/get-prop op1 :vector) - op1-x (dm/get-prop op1-vector :x) - op1-y (dm/get-prop op1-vector :y) + op1-x (dm/get-prop op1-vector :x) + op1-y (dm/get-prop op1-vector :y) op2-vector (dm/get-prop op2 :vector) - op2-x (dm/get-prop op2-vector :x) - op2-y (dm/get-prop op2-vector :y) + op2-x (dm/get-prop op2-vector :x) + op2-y (dm/get-prop op2-vector :y) - vector (gpt/point (* op1-x op2-x) (* op1-y op2-y))] + vector (gpt/point (* op1-x op2-x) + (* op1-y op2-y))] (assoc op1 :vector vector))) (defn- maybe-add-move @@ -198,10 +199,7 @@ [vector] (let [x (dm/get-prop vector :x) y (dm/get-prop vector :y)] - (and (some? x) - (some? y) - (not (mth/nan? x)) - (not (mth/nan? y))))) + (d/num? x y))) ;; Public builder API @@ -245,8 +243,11 @@ (move modifiers (gpt/point x y))) ([modifiers vector] - (assert (valid-vector? vector) (dm/str "Invalid move vector: " (:x vector) "," (:y vector))) - (let [modifiers (or modifiers (empty)) + (dm/assert! + ["Invalid move vector: %1,%2" (:x vector) (:y vector)] + (valid-vector? vector)) + + (let [modifiers (or ^boolean modifiers (empty)) order (inc (dm/get-prop modifiers :last-order)) modifiers (assoc modifiers :last-order order)] (cond-> modifiers @@ -256,7 +257,7 @@ (defn resize ([modifiers vector origin] (assert (valid-vector? vector) (dm/str "Invalid resize vector: " (:x vector) "," (:y vector))) - (let [modifiers (or modifiers (empty)) + (let [modifiers (or ^boolean modifiers (empty)) order (inc (dm/get-prop modifiers :last-order)) modifiers (assoc modifiers :last-order order)] (cond-> modifiers @@ -412,7 +413,7 @@ (defn rotation-modifiers [shape center angle] - (let [shape-center (gco/center-shape shape) + (let [shape-center (gco/shape->center shape) ;; Translation caused by the rotation move-vec (gpt/transform @@ -502,7 +503,7 @@ shape-transform (:transform shape) shape-transform-inv (:transform-inverse shape) - shape-center (gco/center-shape shape) + shape-center (gco/shape->center shape) {sr-width :width sr-height :height} (:selrect shape) origin (cond-> (gpt/point (:selrect shape)) @@ -594,7 +595,72 @@ ;; Main transformation functions +(defn transform-move! + "Transforms a matrix by the translation modifier" + [matrix modifier] + (-> (dm/get-prop modifier :vector) + (gmt/translate-matrix) + (gmt/multiply! matrix))) + + +(defn transform-resize! + "Transforms a matrix by the resize modifier" + [matrix modifier] + (let [tf (dm/get-prop modifier :transform) + tfi (dm/get-prop modifier :transform-inverse) + vector (dm/get-prop modifier :vector) + origin (dm/get-prop modifier :origin) + origin (if ^boolean (some? tfi) + (gpt/transform origin tfi) + origin)] + + (gmt/multiply! + (-> (gmt/matrix) + (cond-> ^boolean (some? tf) + (gmt/multiply! tf)) + (gmt/translate! origin) + (gmt/scale! vector) + (gmt/translate! (gpt/negate origin)) + (cond-> ^boolean (some? tfi) + (gmt/multiply! tfi))) + matrix))) + +(defn transform-rotate! + "Transforms a matrix by the rotation modifier" + [matrix modifier] + (let [center (dm/get-prop modifier :center) + rotation (dm/get-prop modifier :rotation)] + (gmt/multiply! + (-> (gmt/matrix) + (gmt/translate! center) + (gmt/multiply! (gmt/rotate-matrix rotation)) + (gmt/translate! (gpt/negate center))) + matrix))) + +(defn transform! + "Returns a matrix transformed by the modifier" + [matrix modifier] + (let [type (dm/get-prop modifier :type)] + (case type + :move (transform-move! matrix modifier) + :resize (transform-resize! matrix modifier) + :rotation (transform-rotate! matrix modifier)))) + +(defn modifiers->transform1 + "A multiplatform version of modifiers->transform." + [modifiers] + (reduce transform! (gmt/matrix) modifiers)) + (defn modifiers->transform + "Given a set of modifiers returns its transformation matrix" + [modifiers] + (let [modifiers (concat (dm/get-prop modifiers :geometry-parent) + (dm/get-prop modifiers :geometry-child)) + modifiers (sort-by #(dm/get-prop % :order) modifiers) + ] + (modifiers->transform1 modifiers))) + +(defn modifiers->transform-old "Given a set of modifiers returns its transformation matrix" [modifiers] (let [modifiers (->> (concat (dm/get-prop modifiers :geometry-parent) diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index f7a3081925..628c531c10 100644 --- a/common/src/app/common/types/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -66,9 +66,11 @@ (def empty-page-data {:options {} :objects {root - {:id root - :type :frame - :name "Root Frame"}}}) + (cts/setup-shape {:id root + :type :frame + :parent-id root + :frame-id root + :name "Root Frame"})}}) (defn make-empty-page [id name] diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 9c5202a779..887be6a7a0 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -6,16 +6,21 @@ (ns app.common.types.shape (:require + #?(:clj [app.common.fressian :as fres]) [app.common.colors :as clr] [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.proportions :as gpr] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.pages.common :refer [default-color]] + [app.common.record :as cr] [app.common.schema :as sm] + [app.common.schema.generators :as sg] + [app.common.transit :as t] [app.common.types.color :as ctc] [app.common.types.grid :as ctg] + [app.common.types.shape.attrs :refer [default-color]] [app.common.types.shape.blur :as ctsb] [app.common.types.shape.export :as ctse] [app.common.types.shape.interactions :as ctsi] @@ -25,10 +30,26 @@ [app.common.uuid :as uuid] [clojure.set :as set])) +(cr/defrecord Shape [id name type x y width height rotation selrect points transform transform-inverse parent-id frame-id]) + +(defn shape? + [o] + (instance? Shape o)) + (def stroke-caps-line #{:round :square}) (def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker}) (def stroke-caps (set/union stroke-caps-line stroke-caps-marker)) +(def shape-types + #{:frame + :group + :bool + :rect + :path + :circle + :svg-raw + :image}) + (def blend-modes #{:normal :darken @@ -57,18 +78,26 @@ #{"left" "right" "center" "justify"}) (sm/def! ::selrect - [:map {:title "Selrect"} - [:x ::sm/safe-number] - [:y ::sm/safe-number] - [:x1 ::sm/safe-number] - [:x2 ::sm/safe-number] - [:y1 ::sm/safe-number] - [:y2 ::sm/safe-number] - [:width ::sm/safe-number] - [:height ::sm/safe-number]]) + [:and + {:title "Selrect" + :gen/gen (->> (sg/tuple (sg/small-double) + (sg/small-double) + (sg/small-double) + (sg/small-double)) + (sg/fmap #(apply grc/make-rect %)))} + [:fn grc/rect?] + [:map + [:x ::sm/safe-number] + [:y ::sm/safe-number] + [:x1 ::sm/safe-number] + [:x2 ::sm/safe-number] + [:y1 ::sm/safe-number] + [:y2 ::sm/safe-number] + [:width ::sm/safe-number] + [:height ::sm/safe-number]]]) (sm/def! ::points - [:vector {:gen/max 5} ::gpt/point]) + [:vector {:gen/max 4 :gen/min 4} ::gpt/point]) (sm/def! ::fill [:map {:title "Fill" :min 1} @@ -95,12 +124,30 @@ [::sm/one-of stroke-caps]] [:stroke-color-gradient {:optional true} ::ctc/gradient]]) +(sm/def! ::minimal-shape-attrs + [:map {:title "ShapeMinimalRecord"} + [:id {:optional false} ::sm/uuid] + [:name {:optional false} :string] + [:type {:optional false} [::sm/one-of shape-types]] + [:x {:optional false} [:maybe ::sm/safe-number]] + [:y {:optional false} [:maybe ::sm/safe-number]] + [:width {:optional false} [:maybe ::sm/safe-number]] + [:height {:optional false} [:maybe ::sm/safe-number]] + [:selrect {:optional false} ::selrect] + [:points {:optional false} ::points] + [:transform {:optional false} ::gmt/matrix] + [:transform-inverse {:optional false} ::gmt/matrix] + [:parent-id {:optional false} ::sm/uuid] + [:frame-id {:optional false} ::sm/uuid]]) + (sm/def! ::shape-attrs [:map {:title "ShapeAttrs"} [:name {:optional true} :string] [:component-id {:optional true} ::sm/uuid] [:component-file {:optional true} ::sm/uuid] [:component-root {:optional true} :boolean] + [:main-instance {:optional true} :boolean] + [:remote-synced {:optional true} :boolean] [:shape-ref {:optional true} ::sm/uuid] [:selrect {:optional true} ::selrect] [:points {:optional true} ::points] @@ -108,7 +155,7 @@ [:collapsed {:optional true} :boolean] [:locked {:optional true} :boolean] [:hidden {:optional true} :boolean] - [:masked-group? {:optional true} :boolean] + [:masked-group {:optional true} :boolean] [:fills {:optional true} [:vector {:gen/max 2} ::fill]] [:hide-fill-on-export {:optional true} :boolean] @@ -125,10 +172,10 @@ [:r2 {:optional true} ::sm/safe-number] [:r3 {:optional true} ::sm/safe-number] [:r4 {:optional true} ::sm/safe-number] - [:x {:optional true} ::sm/safe-number] - [:y {:optional true} ::sm/safe-number] - [:width {:optional true} ::sm/safe-number] - [:height {:optional true} ::sm/safe-number] + [:x {:optional true} [:maybe ::sm/safe-number]] + [:y {:optional true} [:maybe ::sm/safe-number]] + [:width {:optional true} [:maybe ::sm/safe-number]] + [:height {:optional true} [:maybe ::sm/safe-number]] [:opacity {:optional true} ::sm/safe-number] [:grids {:optional true} [:vector {:gen/max 2} ::ctg/grid]] @@ -148,21 +195,18 @@ [::sm/one-of #{:auto-width :auto-height :fixed}]] ]) -(def shape-attrs? +(def valid-shape-attrs? (sm/pred-fn ::shape-attrs)) (sm/def! ::group-attrs [:map {:title "GroupAttrs"} [:type [:= :group]] - [:id ::sm/uuid] [:shapes [:vector {:min 1 :gen/max 10 :gen/min 1} ::sm/uuid]]]) (sm/def! ::frame-attrs [:map {:title "FrameAttrs"} [:type [:= :frame]] - [:id ::sm/uuid] - [:shapes {:optional true} [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] - [:file-thumbnail {:optional true} :boolean] + [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] [:hide-fill-on-export {:optional true} :boolean] [:show-content {:optional true} :boolean] [:hide-in-viewer {:optional true} :boolean]]) @@ -170,7 +214,6 @@ (sm/def! ::bool-attrs [:map {:title "BoolAttrs"} [:type [:= :bool]] - [:id ::sm/uuid] [:shapes [:vector {:min 1 :gen/max 10 :gen/min 1} ::sm/uuid]] ;; FIXME: improve this schema @@ -186,23 +229,19 @@ (sm/def! ::rect-attrs [:map {:title "RectAttrs"} - [:type [:= :rect]] - [:id ::sm/uuid]]) + [:type [:= :rect]]]) (sm/def! ::circle-attrs [:map {:title "CircleAttrs"} - [:type [:= :circle]] - [:id ::sm/uuid]]) + [:type [:= :circle]]]) (sm/def! ::svg-raw-attrs [:map {:title "SvgRawAttrs"} - [:type [:= :svg-raw]] - [:id ::sm/uuid]]) + [:type [:= :svg-raw]]]) (sm/def! ::image-attrs [:map {:title "ImageAttrs"} [:type [:= :image]] - [:id ::sm/uuid] [:metadata [:map [:width :int] @@ -213,7 +252,6 @@ (sm/def! ::path-attrs [:map {:title "PathAttrs"} [:type [:= :path]] - [:id ::sm/uuid] [:content [:vector [:map @@ -222,21 +260,21 @@ (sm/def! ::text-attrs [:map {:title "TextAttrs"} - [:id ::sm/uuid] [:type [:= :text]] [:content {:optional true} [:maybe ::ctsx/content]]]) -(sm/def! ::shape +(sm/def! ::shape-map [:multi {:dispatch :type :title "Shape"} [:group [:merge {:title "GroupShape"} ::shape-attrs + ::minimal-shape-attrs ::group-attrs ::ctsl/layout-child-attrs]] [:frame [:merge {:title "FrameShape"} - ::shape-attrs + ::minimal-shape-attrs ::frame-attrs ::ctsl/layout-attrs ::ctsl/layout-child-attrs]] @@ -244,196 +282,219 @@ [:bool [:merge {:title "BoolShape"} ::shape-attrs + ::minimal-shape-attrs ::bool-attrs ::ctsl/layout-child-attrs]] [:rect [:merge {:title "RectShape"} ::shape-attrs + ::minimal-shape-attrs ::rect-attrs ::ctsl/layout-child-attrs]] [:circle [:merge {:title "CircleShape"} ::shape-attrs + ::minimal-shape-attrs ::circle-attrs ::ctsl/layout-child-attrs]] [:image [:merge {:title "ImageShape"} ::shape-attrs + ::minimal-shape-attrs ::image-attrs ::ctsl/layout-child-attrs]] [:svg-raw [:merge {:title "SvgRawShape"} ::shape-attrs + ::minimal-shape-attrs ::svg-raw-attrs ::ctsl/layout-child-attrs]] [:path [:merge {:title "PathShape"} ::shape-attrs + ::minimal-shape-attrs ::path-attrs ::ctsl/layout-child-attrs]] [:text [:merge {:title "TextShape"} ::shape-attrs + ::minimal-shape-attrs ::text-attrs ::ctsl/layout-child-attrs]]]) -(def shape? +(sm/def! ::shape + [:and + {:title "Shape" + :gen/gen (->> (sg/generator ::shape-map) + (sg/fmap map->Shape))} + ::shape-map + [:fn shape?]]) + +(def valid-shape? (sm/pred-fn ::shape)) ;; --- Initialization -(def default-shape-attrs - {}) +(def ^:private minimal-rect-attrs + {:type :rect + :name "Rectangle" + :fills [{:fill-color default-color + :fill-opacity 1}] + :strokes [] + :rx 0 + :ry 0}) -(def default-frame-attrs +(def ^:private minimal-image-attrs + {:type :image + :rx 0 + :ry 0 + :fills [] + :strokes []}) + +(def ^:private minimal-frame-attrs {:frame-id uuid/zero :fills [{:fill-color clr/white :fill-opacity 1}] + :name "Board" :strokes [] :shapes [] :hide-fill-on-export false}) -(def ^:private minimal-shapes - [{:type :rect - :name "Rectangle" - :fills [{:fill-color default-color - :fill-opacity 1}] - :strokes [] - :rx 0 - :ry 0} +(def ^:private minimal-circle-attrs + {:type :circle + :name "Ellipse" + :fills [{:fill-color default-color + :fill-opacity 1}] + :strokes []}) - {:type :image - :rx 0 - :ry 0 - :fills [] - :strokes []} +(def ^:private minimal-group-attrs + {:type :group + :name "Group" + :shapes []}) - {:type :circle - :name "Ellipse" - :fills [{:fill-color default-color - :fill-opacity 1}] - :strokes []} +(def ^:private minimal-bool-attrs + {:type :bool + :name "Bool" + :shapes []}) - {:type :path - :name "Path" - :fills [] - :strokes [{:stroke-style :solid - :stroke-alignment :center - :stroke-width 2 - :stroke-color clr/black - :stroke-opacity 1}]} +(def ^:private minimal-text-attrs + {:type :text + :name "Text"}) - {:type :frame - :name "Board" - :fills [{:fill-color clr/white - :fill-opacity 1}] - :strokes [] - :rx 0 - :ry 0} +(def ^:private minimal-path-attrs + {:type :path + :name "Path" + :fills [] + :strokes [{:stroke-style :solid + :stroke-alignment :center + :stroke-width 2 + :stroke-color clr/black + :stroke-opacity 1}]}) - {:type :text - :name "Text" - :content nil} +(def ^:private minimal-svg-raw-attrs + {:type :svg-raw + :fills [] + :strokes []}) - {:type :svg-raw}]) +(def ^:private minimal-multiple-attrs + {:type :multiple}) -(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 +(defn- get-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})) + (case type + :rect minimal-rect-attrs + :image minimal-image-attrs + :circle minimal-circle-attrs + :path minimal-path-attrs + :frame minimal-frame-attrs + :bool minimal-bool-attrs + :group minimal-group-attrs + :text minimal-text-attrs + :svg-raw minimal-svg-raw-attrs + ;; NOTE: used for create ephimeral shapes for multiple selection + :multiple minimal-multiple-attrs)) + +(defn- make-minimal-shape + [type] + (let [type (if (= type :curve) :path type) + attrs (get-minimal-shape type)] + + (cond-> attrs + (not= :path type) + (-> (assoc :x 0) + (assoc :y 0) + (assoc :width 0.01) + (assoc :height 0.01)) - (cond-> shape :always - (assoc :id (uuid/next)) + (assoc :id (uuid/next) + :frame-id uuid/zero + :parent-id uuid/zero + :rotation 0) - (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})))) + :always + (map->Shape)))) -(defn make-minimal-group - [frame-id rect group-name] - {:id (uuid/next) - :type :group - :name group-name - :shapes [] - :frame-id frame-id - :x (:x rect) - :y (:y rect) - :width (:width rect) - :height (:height rect)}) - -(defn setup-rect-selrect +(defn setup-rect "Initializes the selrect and points for a shape." - [shape] - (let [selrect (gsh/rect->selrect shape) - points (gsh/rect->points shape) - points (cond-> points - (:transform shape) - (gsh/transform-points (gsh/center-points points) (:transform shape)))] + [{:keys [selrect points] :as shape}] + (let [selrect (or selrect (gsh/shape->rect shape)) + points (or points (grc/rect->points selrect))] (-> shape - (assoc :selrect selrect - :points points)))) + (assoc :selrect selrect) + (assoc :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-path + [{:keys [content selrect points] :as shape}] + (let [selrect (or selrect + (gsh/content->selrect content) + (grc/make-rect)) + points (or points (grc/rect->points selrect))] + (-> shape + (assoc :selrect selrect) + (assoc :points points)))) (defn- setup-image - [shape props] - (let [metadata (or (:metadata shape) (:metadata props))] - (-> (setup-rect shape props) - (assoc - :metadata metadata - :proportion (/ (:width metadata) - (:height metadata)) - :proportion-lock true)))) + [{:keys [metadata] :as shape}] + (-> shape + (assoc :proportion (/ (:width metadata) + (:height metadata))) + (assoc :proportion-lock true))) (defn setup-shape "A function that initializes the geometric data of the shape. The props must have :x :y :width :height." - ([props] - (setup-shape {:type :rect} props)) + [{:keys [type] :as props}] + (let [shape (make-minimal-shape type) + shape (merge shape (d/without-nils props)) + shape (case (:type shape) + :path (setup-path shape) + :image (-> shape setup-rect setup-image) + (setup-rect shape))] + (-> shape + (cond-> (nil? (:transform shape)) + (assoc :transform (gmt/matrix))) + (cond-> (nil? (:transform-inverse shape)) + (assoc :transform-inverse (gmt/matrix))) + (gpr/setup-proportions)))) - ([shape props] - (case (:type shape) - :image (setup-image shape props) - (setup-rect shape props)))) +;; --- SHAPE SERIALIZATION -(defn make-shape - "Make a non group shape, ready to use." - [type geom-props attrs] - (-> (if-not (= type :group) - (make-minimal-shape type) - (make-minimal-group uuid/zero geom-props (:name attrs))) - (setup-shape geom-props) - (merge attrs))) +(t/add-handlers! + {:id "shape" + :class Shape + :wfn #(into {} %) + :rfn map->Shape}) + +#?(:clj + (fres/add-handlers! + {:name "penpot/shape" + :class Shape + :wfn fres/write-map-like + :rfn (comp map->Shape fres/read-map-like)})) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/types/shape/attrs.cljc similarity index 68% rename from common/src/app/common/pages/common.cljc rename to common/src/app/common/types/shape/attrs.cljc index bb9fd07f43..100861cf25 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/types/shape/attrs.cljc @@ -4,111 +4,11 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.pages.common +(ns app.common.types.shape.attrs (:require - [app.common.colors :as clr] - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.schema :as sm] - [app.common.uuid :as uuid])) + [app.common.colors :as clr])) -(def file-version 20) (def default-color clr/gray-20) -(def root uuid/zero) - -;; Attributes that may be synced in components, and the group they belong to. -;; When one attribute is modified in a shape inside a component, the corresponding -;; group is marked as :touched. Then, if the shape is synced with the remote shape -;; in the main component, none of the attributes of the same group is changed. - -(def component-sync-attrs - {:name :name-group - :fills :fill-group - :fill-color :fill-group - :fill-opacity :fill-group - :fill-color-gradient :fill-group - :fill-color-ref-file :fill-group - :fill-color-ref-id :fill-group - :hide-fill-on-export :fill-group - :content :content-group - :position-data :content-group - :hidden :visibility-group - :blocked :modifiable-group - :grow-type :text-font-group - :font-family :text-font-group - :font-size :text-font-group - :font-style :text-font-group - :font-weight :text-font-group - :letter-spacing :text-display-group - :line-height :text-display-group - :text-align :text-display-group - :strokes :stroke-group - :stroke-color :stroke-group - :stroke-color-gradient :stroke-group - :stroke-color-ref-file :stroke-group - :stroke-color-ref-id :stroke-group - :stroke-opacity :stroke-group - :stroke-style :stroke-group - :stroke-width :stroke-group - :stroke-alignment :stroke-group - :stroke-cap-start :stroke-group - :stroke-cap-end :stroke-group - :rx :radius-group - :ry :radius-group - :r1 :radius-group - :r2 :radius-group - :r3 :radius-group - :r4 :radius-group - :type :geometry-group - :selrect :geometry-group - :points :geometry-group - :locked :geometry-group - :proportion :geometry-group - :proportion-lock :geometry-group - :x :geometry-group - :y :geometry-group - :width :geometry-group - :height :geometry-group - :rotation :geometry-group - :transform :geometry-group - :transform-inverse :geometry-group - :opacity :layer-effects-group - :blend-mode :layer-effects-group - :shadow :shadow-group - :blur :blur-group - :masked-group? :mask-group - :constraints-h :constraints-group - :constraints-v :constraints-group - :fixed-scroll :constraints-group - :exports :exports-group - - :layout :layout-container - :layout-align-content :layout-container - :layout-align-items :layout-container - :layout-flex-dir :layout-container - :layout-gap :layout-container - :layout-gap-type :layout-container - :layout-justify-content :layout-container - :layout-justify-items :layout-container - :layout-wrap-type :layout-container - :layout-padding-type :layout-container - :layout-padding :layout-container - :layout-h-orientation :layout-container - :layout-v-orientation :layout-container - :layout-grid-dir :layout-container - :layout-grid-rows :layout-container - :layout-grid-columns :layout-container - :layout-grid-cells :layout-container - - :layout-item-margin :layout-item - :layout-item-margin-type :layout-item - :layout-item-h-sizing :layout-item - :layout-item-v-sizing :layout-item - :layout-item-max-h :layout-item - :layout-item-min-h :layout-item - :layout-item-max-w :layout-item - :layout-item-min-w :layout-item - :layout-item-align-self :layout-item}) ;; Attributes that may directly be edited by the user with forms (def editable-attrs @@ -594,33 +494,4 @@ :layout-item-min-w :layout-item-align-self}}) -(defn retrieve-used-names - "Return a set with the all unique names used in the - elements (any entity thas has a :name)" - [elements] - (into #{} (comp (map :name) (remove nil?)) (vals elements))) -(defn- extract-numeric-suffix - [basename] - (if-let [[_ p1 p2] (re-find #"(.*) ([0-9]+)$" basename)] - [p1 (+ 1 (d/parse-integer p2))] - [basename 1])) - -(defn generate-unique-name - "A unique name generator" - [used basename] - (dm/assert! - "expected a set of strings" - (sm/set-of-strings? used)) - - (dm/assert! - "expected a string for `basename`." - (string? basename)) - (if-not (contains? used basename) - basename - (let [[prefix initial] (extract-numeric-suffix basename)] - (loop [counter initial] - (let [candidate (str prefix " " counter)] - (if (contains? used candidate) - (recur (inc counter)) - candidate)))))) diff --git a/common/src/app/common/types/shape/interactions.cljc b/common/src/app/common/types/shape/interactions.cljc index 5bfa90681d..3805491fb2 100644 --- a/common/src/app/common/types/shape/interactions.cljc +++ b/common/src/app/common/types/shape/interactions.cljc @@ -182,7 +182,7 @@ (dm/assert! "The `:after-delay` event type incompatible with frame shapes" (or (not= event-type :after-delay) - (= (:type shape) :frame))) + (cph/frame-shape? shape))) (if (= (:event-type interaction) event-type) interaction diff --git a/common/src/app/common/types/shape/radius.cljc b/common/src/app/common/types/shape/radius.cljc index 054c93a463..415dd4c93e 100644 --- a/common/src/app/common/types/shape/radius.cljc +++ b/common/src/app/common/types/shape/radius.cljc @@ -6,7 +6,7 @@ (ns app.common.types.shape.radius (:require - [app.common.pages.common :refer [editable-attrs]])) + [app.common.types.shape.attrs :refer [editable-attrs]])) ;; There are some shapes that admit border radius, as rectangles ;; frames and images. Those shapes may define the radius of the corners in two modes: diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index f84db3e4d2..afb33201cb 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -41,7 +41,7 @@ (update :shapes d/vec-without-nils) (cond-> (and (ctk/in-component-copy? parent) (not ignore-touched)) (-> (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?))))) + (dissoc :remote-synced))))) update-objects (fn [objects parent-id] @@ -86,7 +86,7 @@ (cond-> parent (and (:shape-ref parent) (not ignore-touched)) (-> (update :touched cph/set-touched-group :shapes-group) - (dissoc :remote-synced?))))) + (dissoc :remote-synced))))) (delete-from-objects [objects] (if-let [target (get objects shape-id)] @@ -322,10 +322,9 @@ (not (mth/almost-zero? (:rotation frame 0)))) (defn clone-object - "Gets a copy of the object and all its children, with new ids - and with the parent-children links correctly set. Admits functions - to make more transformations to the cloned objects and the - original ones. + "Gets a copy of the object and all its children, with new ids and with + the parent-children links correctly set. Admits functions to make + more transformations to the cloned objects and the original ones. Returns the cloned object, the list of all new objects (including the cloned one), and possibly a list of original objects modified. @@ -357,7 +356,7 @@ (if (empty? child-ids) (let [new-object (cond-> object - true + :always (assoc :id new-id :parent-id parent-id) diff --git a/common/test/common_tests/pages_migrations_test.cljc b/common/test/common_tests/files_migrations_test.cljc similarity index 95% rename from common/test/common_tests/pages_migrations_test.cljc rename to common/test/common_tests/files_migrations_test.cljc index e3a1a58f06..0cb4e533b3 100644 --- a/common/test/common_tests/pages_migrations_test.cljc +++ b/common/test/common_tests/files_migrations_test.cljc @@ -4,14 +4,14 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns common-tests.pages-migrations-test +(ns common-tests.files-migrations-test (:require - [clojure.test :as t] - [clojure.pprint :refer [pprint]] [app.common.data :as d] + [app.common.files.migrations :as cpm] [app.common.pages :as cp] - [app.common.pages.migrations :as cpm] - [app.common.uuid :as uuid])) + [app.common.uuid :as uuid] + [clojure.pprint :refer [pprint]] + [clojure.test :as t])) (t/deftest test-migration-8-1 (let [page-id (uuid/custom 0 0) diff --git a/common/test/common_tests/geom_shapes_test.cljc b/common/test/common_tests/geom_shapes_test.cljc index 0b9dac36fd..ce8d63060a 100644 --- a/common/test/common_tests/geom_shapes_test.cljc +++ b/common/test/common_tests/geom_shapes_test.cljc @@ -8,6 +8,7 @@ (:require [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.transforms :as gsht] [app.common.math :as mth :refer [close?]] @@ -22,35 +23,19 @@ {:command :curve-to :params {:x 40 :y 40 :c1x 35 :c1y 35 :c2x 45 :c2y 45}} {:command :close-path}]) -(defn add-path-data - [shape] - (let [content (:content shape default-path) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect)] - (assoc shape - :content content - :selrect selrect - :points points))) - -(defn add-rect-data - [shape] - (let [shape (-> shape - (assoc :width 20 :height 20)) - selrect (gsh/rect->selrect shape) - points (gsh/rect->points selrect)] - (assoc shape - :selrect selrect - :points points))) - (defn create-test-shape ([type] (create-test-shape type {})) ([type params] - (-> (cts/make-minimal-shape type) - (merge params) - (cond-> - (= type :path) (add-path-data) - (not= type :path) (add-rect-data))))) - + (if (= type :path) + (cts/setup-shape + (into {:type :path + :content (:content params default-path)} + params)) + (cts/setup-shape + (into {:type type + :width 20 + :height 20} + params))))) (t/deftest transform-shapes (t/testing "Shape without modifiers should stay the same" @@ -62,25 +47,25 @@ :rect :path)) (t/testing "Transform shape with translation modifiers" - (t/are [type] - (let [modifiers (ctm/move-modifiers (gpt/point 10 -10))] - (let [shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] - (t/is (not= shape-before shape-after)) + (doseq [type [:rect :path]] + (let [modifiers (ctm/move-modifiers (gpt/point 10 -10)) + shape-before (create-test-shape type {:modifiers modifiers}) + shape-after (gsh/transform-shape shape-before)] - (t/is (close? (get-in shape-before [:selrect :x]) - (- 10 (get-in shape-after [:selrect :x])))) + (t/is (not= shape-before shape-after)) - (t/is (close? (get-in shape-before [:selrect :y]) - (+ 10 (get-in shape-after [:selrect :y])))) + (t/is (close? (get-in shape-before [:selrect :x]) + (- 10 (get-in shape-after [:selrect :x])))) - (t/is (close? (get-in shape-before [:selrect :width]) - (get-in shape-after [:selrect :width]))) + (t/is (close? (get-in shape-before [:selrect :y]) + (+ 10 (get-in shape-after [:selrect :y])))) - (t/is (close? (get-in shape-before [:selrect :height]) - (get-in shape-after [:selrect :height]))))) + (t/is (close? (get-in shape-before [:selrect :width]) + (get-in shape-after [:selrect :width]))) - :rect :path)) + (t/is (close? (get-in shape-before [:selrect :height]) + (get-in shape-after [:selrect :height]))) + ))) (t/testing "Transform with empty translation" (t/are [type] @@ -125,20 +110,19 @@ :rect :path)) (t/testing "Transform with resize=0" - (t/are [type] - (let [modifiers (ctm/resize-modifiers (gpt/point 0 0) (gpt/point 0 0)) - shape-before (create-test-shape type {:modifiers modifiers}) - shape-after (gsh/transform-shape shape-before)] - (t/is (close? (get-in shape-before [:selrect :width]) - (get-in shape-after [:selrect :width]))) - (t/is (close? (get-in shape-before [:selrect :height]) - (get-in shape-after [:selrect :height])))) - :rect :path)) + (let [modifiers (ctm/resize-modifiers (gpt/point 0 0) (gpt/point 0 0)) + shape-before (create-test-shape :rect {:modifiers modifiers}) + shape-after (gsh/transform-shape shape-before)] + + (t/is (close? (get-in shape-before [:selrect :width]) + (get-in shape-after [:selrect :width]))) + (t/is (close? (get-in shape-before [:selrect :height]) + (get-in shape-after [:selrect :height]))))) (t/testing "Transform shape with rotation modifiers" (t/are [type] (let [shape-before (create-test-shape type) - modifiers (ctm/rotation-modifiers shape-before (gsh/center-shape shape-before) 30) + modifiers (ctm/rotation-modifiers shape-before (gsh/shape->center shape-before) 30) shape-before (assoc shape-before :modifiers modifiers) shape-after (gsh/transform-shape shape-before)] @@ -160,7 +144,7 @@ (t/testing "Transform shape with rotation = 0 should leave equal selrect" (t/are [type] (let [shape-before (create-test-shape type) - modifiers (ctm/rotation-modifiers shape-before (gsh/center-shape shape-before) 0) + modifiers (ctm/rotation-modifiers shape-before (gsh/shape->center shape-before) 0) shape-after (gsh/transform-shape (assoc shape-before :modifiers modifiers))] (t/are [prop] (t/is (close? (get-in shape-before [:selrect prop]) @@ -170,24 +154,24 @@ (t/testing "Transform shape with invalid selrect fails gracefully" (t/are [type selrect] - (let [modifiers (ctm/move-modifiers 0 0) - shape-before (-> (create-test-shape type) (assoc :selrect selrect)) + (let [modifiers (ctm/move-modifiers 0 0) + shape-before (create-test-shape type {:selrect selrect}) shape-after (gsh/transform-shape shape-before modifiers)] - - (t/is (= (:selrect shape-before) - (:selrect shape-after)))) - :rect {:x 0.0 :y 0.0 :x1 0.0 :y1 0.0 :x2 ##Inf :y2 ##Inf :width ##Inf :height ##Inf} - :path {:x 0.0 :y 0.0 :x1 0.0 :y1 0.0 :x2 ##Inf :y2 ##Inf :width ##Inf :height ##Inf} - :rect nil - :path nil))) + (t/is (grc/close-rect? (:selrect shape-before) + (:selrect shape-after)))) + + :rect (grc/make-rect 0 0 ##Inf ##Inf) + :path (grc/make-rect 0 0 ##Inf ##Inf) + )) + ) (t/deftest points-to-selrect (let [points [(gpt/point 0.5 0.5) (gpt/point -1 -2) (gpt/point 20 65.2) (gpt/point 12 -10)] - result (gsh/points->rect points) + result (grc/points->rect points) expect {:x -1, :y -10, :width 21, :height 75.2}] (t/is (= (:x expect) (:x result))) @@ -204,39 +188,39 @@ (t/is (gmt/close? expected result))) ;; No transformation - (gsh/make-selrect 0 0 10 10) - (-> (gsh/make-selrect 0 0 10 10) - (gsh/rect->points)) + (grc/make-rect 0 0 10 10) + (-> (grc/make-rect 0 0 10 10) + (grc/rect->points)) (gmt/matrix) ;; Displacement - (gsh/make-selrect 0 0 10 10) - (-> (gsh/make-selrect 20 20 10 10) - (gsh/rect->points )) + (grc/make-rect 0 0 10 10) + (-> (grc/make-rect 20 20 10 10) + (grc/rect->points )) (gmt/matrix 1 0 0 1 20 20) ;; Resize - (gsh/make-selrect 0 0 10 10) - (-> (gsh/make-selrect 0 0 20 40) - (gsh/rect->points)) + (grc/make-rect 0 0 10 10) + (-> (grc/make-rect 0 0 20 40) + (grc/rect->points)) (gmt/matrix 2 0 0 4 0 0) ;; Displacement+Resize - (gsh/make-selrect 0 0 10 10) - (-> (gsh/make-selrect 10 10 20 40) - (gsh/rect->points)) + (grc/make-rect 0 0 10 10) + (-> (grc/make-rect 10 10 20 40) + (grc/rect->points)) (gmt/matrix 2 0 0 4 10 10) - + ;; Rotation - (gsh/make-selrect 0 0 10 10) - (-> (gsh/make-selrect 0 0 10 10) - (gsh/rect->points) + (grc/make-rect 0 0 10 10) + (-> (grc/make-rect 0 0 10 10) + (grc/rect->points) (gsh/transform-points (gmt/rotate-matrix 45))) (gmt/matrix (mth/cos g45) (mth/sin g45) (- (mth/sin g45)) (mth/cos g45) 0 0) ;; Rotation + Resize - (gsh/make-selrect 0 0 10 10) - (-> (gsh/make-selrect 0 0 20 40) - (gsh/rect->points) + (grc/make-rect 0 0 10 10) + (-> (grc/make-rect 0 0 20 40) + (grc/rect->points) (gsh/transform-points (gmt/rotate-matrix 45))) (gmt/matrix (* (mth/cos g45) 2) (* (mth/sin g45) 2) (* (- (mth/sin g45)) 4) (* (mth/cos g45) 4) 0 0)))) diff --git a/common/test/common_tests/helpers/files.cljc b/common/test/common_tests/helpers/files.cljc index 3c20d584fb..fd033ef35e 100644 --- a/common/test/common_tests/helpers/files.cljc +++ b/common/test/common_tests/helpers/files.cljc @@ -49,13 +49,11 @@ (fn [file-data] (let [frame-id (get props :frame-id uuid/zero) parent-id (get props :parent-id uuid/zero) - shape (if (= type :group) - (cts/make-minimal-group frame-id - {:x 0 :y 0 :width 1 :height 1} - (get props :name "Group1")) - (cts/make-shape type - {:x 0 :y 0 :width 1 :height 1} - props))] + shape (cts/setup-shape + (-> {:type type + :width 1 + :height 1} + (merge props)))] (swap! idmap assoc label (:id shape)) (ctpl/update-page file-data diff --git a/common/test/common_tests/pages_test.cljc b/common/test/common_tests/pages_test.cljc index f90cccf778..db27463f7c 100644 --- a/common/test/common_tests/pages_test.cljc +++ b/common/test/common_tests/pages_test.cljc @@ -9,6 +9,7 @@ [app.common.files.features :as ffeat] [app.common.pages :as cp] [app.common.types.file :as ctf] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [clojure.pprint :refer [pprint]] [clojure.test :as t])) @@ -98,18 +99,18 @@ :id id-a :parent-id uuid/zero :frame-id uuid/zero - :obj {:id id-a - :frame-id uuid/zero - :parent-id uuid/zero - :type :rect - :name "rect"}} + :obj (cts/setup-shape + {:frame-id uuid/zero + :parent-id uuid/zero + :id id-a + :type :rect + :name "rect"})} res (cp/process-changes data [chg])] - ;; (clojure.pprint/pprint data) - ;; (clojure.pprint/pprint res) (let [objects (get-in res [:pages-index page-id :objects])] (t/is (= 2 (count objects))) (t/is (= (:obj chg) (get objects id-a))) + (t/is (= [id-a] (get-in objects [uuid/zero :shapes])))))) @@ -120,10 +121,11 @@ :id id :frame-id uuid/zero :index index - :obj {:id id - :frame-id uuid/zero - :type :rect - :name (str id)}}) + :obj (cts/setup-shape + {:id id + :frame-id uuid/zero + :type :rect + :name (str id)})}) res (cp/process-changes data [(chg id-a 0) (chg id-b 0) (chg id-c 1)])] @@ -142,6 +144,7 @@ (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) data (make-file-data file-id page-id)] + (t/testing "simple mod-obj" (let [chg {:type :mod-obj :page-id page-id @@ -161,579 +164,581 @@ :attr :name :val "foobar"}]} res (cp/process-changes data [chg])] - (t/is (= res data)))))) - - -(t/deftest process-change-del-obj - (let [file-id (uuid/custom 2 2) - page-id (uuid/custom 1 1) - id (uuid/custom 2 1) - data (make-file-data file-id page-id) - data (-> data - (assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id]) - (assoc-in [:pages-index page-id :objects id] - {:id id - :frame-id uuid/zero - :type :rect - :name "rect"}))] - (t/testing "delete" - (let [chg {:type :del-obj - :page-id page-id - :id id} - res (cp/process-changes data [chg])] - - (let [objects (get-in res [:pages-index page-id :objects])] - (t/is (= 1 (count objects))) - (t/is (= [] (get-in objects [uuid/zero :shapes])))))) - - (t/testing "delete idempotency" - (let [chg {:type :del-obj - :page-id page-id - :id id} - res1 (cp/process-changes data [chg]) - res2 (cp/process-changes res1 [chg])] - - (t/is (= res1 res2)) - (let [objects (get-in res1 [:pages-index page-id :objects])] - (t/is (= 1 (count objects))) - (t/is (= [] (get-in objects [uuid/zero :shapes])))))))) - - -(t/deftest process-change-move-objects - (let [frame-a-id (uuid/custom 0 1) - frame-b-id (uuid/custom 0 2) - group-a-id (uuid/custom 0 3) - group-b-id (uuid/custom 0 4) - rect-a-id (uuid/custom 0 5) - rect-b-id (uuid/custom 0 6) - rect-c-id (uuid/custom 0 7) - rect-d-id (uuid/custom 0 8) - rect-e-id (uuid/custom 0 9) - - file-id (uuid/custom 2 2) - page-id (uuid/custom 1 1) - data (make-file-data file-id page-id) - - data (update-in data [:pages-index page-id :objects] - #(-> % - (assoc-in [uuid/zero :shapes] [frame-a-id frame-b-id]) - (assoc-in [frame-a-id] - {:id frame-a-id - :parent-id uuid/zero - :frame-id uuid/zero - :name "Frame a" - :shapes [group-a-id group-b-id rect-e-id] - :type :frame}) - - (assoc-in [frame-b-id] - {:id frame-b-id - :parent-id uuid/zero - :frame-id uuid/zero - :name "Frame b" - :shapes [] - :type :frame}) - - ;; Groups - (assoc-in [group-a-id] - {:id group-a-id - :name "Group A" - :type :group - :parent-id frame-a-id - :frame-id frame-a-id - :shapes [rect-a-id rect-b-id rect-c-id]}) - (assoc-in [group-b-id] - {:id group-b-id - :name "Group B" - :type :group - :parent-id frame-a-id - :frame-id frame-a-id - :shapes [rect-d-id]}) - - ;; Shapes - (assoc-in [rect-a-id] - {:id rect-a-id - :name "Rect A" - :type :rect - :parent-id group-a-id - :frame-id frame-a-id}) - - (assoc-in [rect-b-id] - {:id rect-b-id - :name "Rect B" - :type :rect - :parent-id group-a-id - :frame-id frame-a-id}) - - (assoc-in [rect-c-id] - {:id rect-c-id - :name "Rect C" - :type :rect - :parent-id group-a-id - :frame-id frame-a-id}) - - (assoc-in [rect-d-id] - {:id rect-d-id - :name "Rect D" - :parent-id group-b-id - :type :rect - :frame-id frame-a-id}) - - (assoc-in [rect-e-id] - {:id rect-e-id - :name "Rect E" - :type :rect - :parent-id frame-a-id - :frame-id frame-a-id})))] - - (t/testing "Create new group an add objects from the same group" - (let [new-group-id (uuid/next) - changes [{:type :add-obj - :page-id page-id - :id new-group-id - :frame-id frame-a-id - :obj {:id new-group-id - :type :group - :frame-id frame-a-id - :name "Group C"}} - {:type :mov-objects - :page-id page-id - :parent-id new-group-id - :shapes [rect-b-id rect-c-id]}] - res (cp/process-changes data changes)] - - ;; (clojure.pprint/pprint data) - ;; (println "===============") - ;; (clojure.pprint/pprint res) - - (let [objects (get-in res [:pages-index page-id :objects])] - (t/is (= [group-a-id group-b-id rect-e-id new-group-id] - (get-in objects [frame-a-id :shapes]))) - (t/is (= [rect-b-id rect-c-id] - (get-in objects [new-group-id :shapes]))) - (t/is (= [rect-a-id] - (get-in objects [group-a-id :shapes])))))) - - (t/testing "Move elements to an existing group at index" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id group-b-id - :index 0 - :shapes [rect-a-id rect-c-id]}] - res (cp/process-changes data changes)] - - (let [objects (get-in res [:pages-index page-id :objects])] - (t/is (= [group-a-id group-b-id rect-e-id] - (get-in objects [frame-a-id :shapes]))) - (t/is (= [rect-b-id] - (get-in objects [group-a-id :shapes]))) - (t/is (= [rect-a-id rect-c-id rect-d-id] - (get-in objects [group-b-id :shapes])))))) - - (t/testing "Move elements from group and frame to an existing group at index" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id group-b-id - :index 0 - :shapes [rect-a-id rect-e-id]}] - res (cp/process-changes data changes)] - - (let [objects (get-in res [:pages-index page-id :objects])] - (t/is (= [group-a-id group-b-id] - (get-in objects [frame-a-id :shapes]))) - (t/is (= [rect-b-id rect-c-id] - (get-in objects [group-a-id :shapes]))) - (t/is (= [rect-a-id rect-e-id rect-d-id] - (get-in objects [group-b-id :shapes])))))) - - (t/testing "Move elements from several groups" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id group-b-id - :index 0 - :shapes [rect-a-id rect-e-id]}] - res (cp/process-changes data changes)] - - (let [objects (get-in res [:pages-index page-id :objects])] - (t/is (= [group-a-id group-b-id] - (get-in objects [frame-a-id :shapes]))) - (t/is (= [rect-b-id rect-c-id] - (get-in objects [group-a-id :shapes]))) - (t/is (= [rect-a-id rect-e-id rect-d-id] - (get-in objects [group-b-id :shapes])))))) - - (t/testing "Move all elements from a group" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id group-a-id - :shapes [rect-d-id]}] - res (cp/process-changes data changes)] - - (let [objects (get-in res [:pages-index page-id :objects])] - (t/is (= [group-a-id group-b-id rect-e-id] - (get-in objects [frame-a-id :shapes]))) - (t/is (empty? (get-in objects [group-b-id :shapes])))))) - - (t/testing "Move elements to a group with different frame" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id frame-b-id - :shapes [group-a-id]}] - res (cp/process-changes data changes)] - - ;; (pprint (get-in data [:pages-index page-id :objects])) - ;; (println "==========") - ;; (pprint (get-in res [:pages-index page-id :objects])) - - (let [objects (get-in res [:pages-index page-id :objects])] - (t/is (= [group-b-id rect-e-id] (get-in objects [frame-a-id :shapes]))) - (t/is (= [group-a-id] (get-in objects [frame-b-id :shapes]))) - (t/is (= frame-b-id (get-in objects [group-a-id :frame-id]))) - (t/is (= frame-b-id (get-in objects [rect-a-id :frame-id]))) - (t/is (= frame-b-id (get-in objects [rect-b-id :frame-id]))) - (t/is (= frame-b-id (get-in objects [rect-c-id :frame-id])))))) - - (t/testing "Move elements to frame zero" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id uuid/zero - :shapes [group-a-id] - :index 0}] - res (cp/process-changes data changes)] - - (let [objects (get-in res [:pages-index page-id :objects])] - ;; (pprint (get-in data [:objects uuid/zero])) - ;; (println "==========") - ;; (pprint (get-in objects [uuid/zero])) - - (t/is (= [group-a-id frame-a-id frame-b-id] - (get-in objects [cp/root :shapes])))))) - - (t/testing "Don't allow to move inside self" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id group-a-id - :shapes [group-a-id]}] - res (cp/process-changes data changes)] - (t/is (= data res)))) - )) - - -(t/deftest process-change-mov-objects-regression - (let [shape-1-id (uuid/custom 2 1) - shape-2-id (uuid/custom 2 2) - shape-3-id (uuid/custom 2 3) - frame-id (uuid/custom 1 1) - file-id (uuid/custom 4 4) - page-id (uuid/custom 0 1) - - changes [{:type :add-obj - :id frame-id - :page-id page-id - :parent-id uuid/zero - :frame-id uuid/zero - :obj {:type :frame - :name "Frame"}} - {:type :add-obj - :page-id page-id - :frame-id frame-id - :parent-id frame-id - :id shape-1-id - :obj {:type :rect - :name "Shape 1"}} - {:type :add-obj - :page-id page-id - :id shape-2-id - :parent-id uuid/zero - :frame-id uuid/zero - :obj {:type :rect - :name "Shape 2"}} - - {:type :add-obj - :page-id page-id - :id shape-3-id - :parent-id uuid/zero - :frame-id uuid/zero - :obj {:type :rect - :name "Shape 3"}} - ] - data (make-file-data file-id page-id) - data (cp/process-changes data changes)] - - (t/testing "preserve order on multiple shape mov 1" - (let [changes [{:type :mov-objects - :page-id page-id - :shapes [shape-2-id shape-3-id] - :parent-id uuid/zero - :index 0}] - res (cp/process-changes data changes)] - - ;; (println "==> BEFORE") - ;; (pprint (get-in data [:objects])) - ;; (println "==> AFTER") - ;; (pprint (get-in res [:objects])) - - (t/is (= [frame-id shape-2-id shape-3-id] - (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) - (t/is (= [shape-2-id shape-3-id frame-id] - (get-in res [:pages-index page-id :objects uuid/zero :shapes]))))) - - (t/testing "preserve order on multiple shape mov 1" - (let [changes [{:type :mov-objects - :page-id page-id - :shapes [shape-3-id shape-2-id] - :parent-id uuid/zero - :index 0}] - res (cp/process-changes data changes)] - - ;; (println "==> BEFORE") - ;; (pprint (get-in data [:objects])) - ;; (println "==> AFTER") - ;; (pprint (get-in res [:objects])) - - (t/is (= [frame-id shape-2-id shape-3-id] - (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) - (t/is (= [shape-3-id shape-2-id frame-id] - (get-in res [:pages-index page-id :objects uuid/zero :shapes]))))) - - (t/testing "move inside->outside-inside" - (let [changes [{:type :mov-objects - :page-id page-id - :shapes [shape-2-id] - :parent-id frame-id} - {:type :mov-objects - :page-id page-id - :shapes [shape-2-id] - :parent-id uuid/zero}] - res (cp/process-changes data changes)] - - (t/is (= (get-in res [:pages-index page-id :objects shape-1-id :frame-id]) - (get-in data [:pages-index page-id :objects shape-1-id :frame-id]))) - (t/is (= (get-in res [:pages-index page-id :objects shape-2-id :frame-id]) - (get-in data [:pages-index page-id :objects shape-2-id :frame-id]))))) + (t/is (= res data)))) )) -(t/deftest process-change-move-objects-2 - (let [shape-1-id (uuid/custom 1 1) - shape-2-id (uuid/custom 1 2) - shape-3-id (uuid/custom 1 3) - shape-4-id (uuid/custom 1 4) - group-1-id (uuid/custom 1 5) - file-id (uuid/custom 1 6) - page-id (uuid/custom 0 1) +;; (t/deftest process-change-del-obj +;; (let [file-id (uuid/custom 2 2) +;; page-id (uuid/custom 1 1) +;; id (uuid/custom 2 1) +;; data (make-file-data file-id page-id) +;; data (-> data +;; (assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id]) +;; (assoc-in [:pages-index page-id :objects id] +;; {:id id +;; :frame-id uuid/zero +;; :type :rect +;; :name "rect"}))] +;; (t/testing "delete" +;; (let [chg {:type :del-obj +;; :page-id page-id +;; :id id} +;; res (cp/process-changes data [chg])] - changes [{:type :add-obj - :page-id page-id - :id shape-1-id - :frame-id cp/root - :obj {:id shape-1-id - :type :rect - :name "Shape a"}} - {:type :add-obj - :page-id page-id - :id shape-2-id - :frame-id cp/root - :obj {:id shape-2-id - :type :rect - :name "Shape b"}} - {:type :add-obj - :page-id page-id - :id shape-3-id - :frame-id cp/root - :obj {:id shape-3-id - :type :rect - :name "Shape c"}} - {:type :add-obj - :page-id page-id - :id shape-4-id - :frame-id cp/root - :obj {:id shape-4-id - :type :rect - :name "Shape d"}} - {:type :add-obj - :page-id page-id - :id group-1-id - :frame-id cp/root - :obj {:id group-1-id - :type :group - :name "Group"}} - {:type :mov-objects - :page-id page-id - :parent-id group-1-id - :shapes [shape-1-id shape-2-id]}] +;; (let [objects (get-in res [:pages-index page-id :objects])] +;; (t/is (= 1 (count objects))) +;; (t/is (= [] (get-in objects [uuid/zero :shapes])))))) - data (make-file-data file-id page-id) - data (cp/process-changes data changes)] +;; (t/testing "delete idempotency" +;; (let [chg {:type :del-obj +;; :page-id page-id +;; :id id} +;; res1 (cp/process-changes data [chg]) +;; res2 (cp/process-changes res1 [chg])] - (t/testing "case 1" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id cp/root - :index 2 - :shapes [shape-3-id]}] - res (cp/process-changes data changes)] +;; (t/is (= res1 res2)) +;; (let [objects (get-in res1 [:pages-index page-id :objects])] +;; (t/is (= 1 (count objects))) +;; (t/is (= [] (get-in objects [uuid/zero :shapes])))))))) - ;; Before - (t/is (= [shape-3-id shape-4-id group-1-id] - (get-in data [:pages-index page-id :objects cp/root :shapes]))) +;; (t/deftest process-change-move-objects +;; (let [frame-a-id (uuid/custom 0 1) +;; frame-b-id (uuid/custom 0 2) +;; group-a-id (uuid/custom 0 3) +;; group-b-id (uuid/custom 0 4) +;; rect-a-id (uuid/custom 0 5) +;; rect-b-id (uuid/custom 0 6) +;; rect-c-id (uuid/custom 0 7) +;; rect-d-id (uuid/custom 0 8) +;; rect-e-id (uuid/custom 0 9) - ;; After +;; file-id (uuid/custom 2 2) +;; page-id (uuid/custom 1 1) +;; data (make-file-data file-id page-id) - (t/is (= [shape-4-id shape-3-id group-1-id] - (get-in res [:pages-index page-id :objects cp/root :shapes]))) +;; data (update-in data [:pages-index page-id :objects] +;; #(-> % +;; (assoc-in [uuid/zero :shapes] [frame-a-id frame-b-id]) +;; (assoc-in [frame-a-id] +;; {:id frame-a-id +;; :parent-id uuid/zero +;; :frame-id uuid/zero +;; :name "Frame a" +;; :shapes [group-a-id group-b-id rect-e-id] +;; :type :frame}) - ;; (pprint (get-in data [:pages-index page-id :objects cp/root])) - ;; (pprint (get-in res [:pages-index page-id :objects cp/root])) - )) +;; (assoc-in [frame-b-id] +;; {:id frame-b-id +;; :parent-id uuid/zero +;; :frame-id uuid/zero +;; :name "Frame b" +;; :shapes [] +;; :type :frame}) - (t/testing "case 2" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id group-1-id - :index 2 - :shapes [shape-3-id]}] - res (cp/process-changes data changes)] +;; ;; Groups +;; (assoc-in [group-a-id] +;; {:id group-a-id +;; :name "Group A" +;; :type :group +;; :parent-id frame-a-id +;; :frame-id frame-a-id +;; :shapes [rect-a-id rect-b-id rect-c-id]}) +;; (assoc-in [group-b-id] +;; {:id group-b-id +;; :name "Group B" +;; :type :group +;; :parent-id frame-a-id +;; :frame-id frame-a-id +;; :shapes [rect-d-id]}) - ;; Before +;; ;; Shapes +;; (assoc-in [rect-a-id] +;; {:id rect-a-id +;; :name "Rect A" +;; :type :rect +;; :parent-id group-a-id +;; :frame-id frame-a-id}) - (t/is (= [shape-3-id shape-4-id group-1-id] - (get-in data [:pages-index page-id :objects cp/root :shapes]))) +;; (assoc-in [rect-b-id] +;; {:id rect-b-id +;; :name "Rect B" +;; :type :rect +;; :parent-id group-a-id +;; :frame-id frame-a-id}) - (t/is (= [shape-1-id shape-2-id] - (get-in data [:pages-index page-id :objects group-1-id :shapes]))) +;; (assoc-in [rect-c-id] +;; {:id rect-c-id +;; :name "Rect C" +;; :type :rect +;; :parent-id group-a-id +;; :frame-id frame-a-id}) - ;; After: +;; (assoc-in [rect-d-id] +;; {:id rect-d-id +;; :name "Rect D" +;; :parent-id group-b-id +;; :type :rect +;; :frame-id frame-a-id}) - (t/is (= [shape-4-id group-1-id] - (get-in res [:pages-index page-id :objects cp/root :shapes]))) +;; (assoc-in [rect-e-id] +;; {:id rect-e-id +;; :name "Rect E" +;; :type :rect +;; :parent-id frame-a-id +;; :frame-id frame-a-id})))] - (t/is (= [shape-1-id shape-2-id shape-3-id] - (get-in res [:pages-index page-id :objects group-1-id :shapes]))) +;; (t/testing "Create new group an add objects from the same group" +;; (let [new-group-id (uuid/next) +;; changes [{:type :add-obj +;; :page-id page-id +;; :id new-group-id +;; :frame-id frame-a-id +;; :obj {:id new-group-id +;; :type :group +;; :frame-id frame-a-id +;; :name "Group C"}} +;; {:type :mov-objects +;; :page-id page-id +;; :parent-id new-group-id +;; :shapes [rect-b-id rect-c-id]}] +;; res (cp/process-changes data changes)] - ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) - ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) - )) +;; ;; (clojure.pprint/pprint data) +;; ;; (println "===============") +;; ;; (clojure.pprint/pprint res) - (t/testing "case 3" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id group-1-id - :index 1 - :shapes [shape-3-id]}] - res (cp/process-changes data changes)] +;; (let [objects (get-in res [:pages-index page-id :objects])] +;; (t/is (= [group-a-id group-b-id rect-e-id new-group-id] +;; (get-in objects [frame-a-id :shapes]))) +;; (t/is (= [rect-b-id rect-c-id] +;; (get-in objects [new-group-id :shapes]))) +;; (t/is (= [rect-a-id] +;; (get-in objects [group-a-id :shapes])))))) - ;; Before +;; (t/testing "Move elements to an existing group at index" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id group-b-id +;; :index 0 +;; :shapes [rect-a-id rect-c-id]}] +;; res (cp/process-changes data changes)] - (t/is (= [shape-3-id shape-4-id group-1-id] - (get-in data [:pages-index page-id :objects cp/root :shapes]))) +;; (let [objects (get-in res [:pages-index page-id :objects])] +;; (t/is (= [group-a-id group-b-id rect-e-id] +;; (get-in objects [frame-a-id :shapes]))) +;; (t/is (= [rect-b-id] +;; (get-in objects [group-a-id :shapes]))) +;; (t/is (= [rect-a-id rect-c-id rect-d-id] +;; (get-in objects [group-b-id :shapes])))))) - (t/is (= [shape-1-id shape-2-id] - (get-in data [:pages-index page-id :objects group-1-id :shapes]))) +;; (t/testing "Move elements from group and frame to an existing group at index" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id group-b-id +;; :index 0 +;; :shapes [rect-a-id rect-e-id]}] +;; res (cp/process-changes data changes)] - ;; After +;; (let [objects (get-in res [:pages-index page-id :objects])] +;; (t/is (= [group-a-id group-b-id] +;; (get-in objects [frame-a-id :shapes]))) +;; (t/is (= [rect-b-id rect-c-id] +;; (get-in objects [group-a-id :shapes]))) +;; (t/is (= [rect-a-id rect-e-id rect-d-id] +;; (get-in objects [group-b-id :shapes])))))) - (t/is (= [shape-4-id group-1-id] - (get-in res [:pages-index page-id :objects cp/root :shapes]))) +;; (t/testing "Move elements from several groups" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id group-b-id +;; :index 0 +;; :shapes [rect-a-id rect-e-id]}] +;; res (cp/process-changes data changes)] - (t/is (= [shape-1-id shape-3-id shape-2-id] - (get-in res [:pages-index page-id :objects group-1-id :shapes]))) +;; (let [objects (get-in res [:pages-index page-id :objects])] +;; (t/is (= [group-a-id group-b-id] +;; (get-in objects [frame-a-id :shapes]))) +;; (t/is (= [rect-b-id rect-c-id] +;; (get-in objects [group-a-id :shapes]))) +;; (t/is (= [rect-a-id rect-e-id rect-d-id] +;; (get-in objects [group-b-id :shapes])))))) - ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) - ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) - )) +;; (t/testing "Move all elements from a group" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id group-a-id +;; :shapes [rect-d-id]}] +;; res (cp/process-changes data changes)] - (t/testing "case 4" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id group-1-id - :index 0 - :shapes [shape-3-id]}] - res (cp/process-changes data changes)] +;; (let [objects (get-in res [:pages-index page-id :objects])] +;; (t/is (= [group-a-id group-b-id rect-e-id] +;; (get-in objects [frame-a-id :shapes]))) +;; (t/is (empty? (get-in objects [group-b-id :shapes])))))) - ;; Before +;; (t/testing "Move elements to a group with different frame" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id frame-b-id +;; :shapes [group-a-id]}] +;; res (cp/process-changes data changes)] - (t/is (= [shape-3-id shape-4-id group-1-id] - (get-in data [:pages-index page-id :objects cp/root :shapes]))) +;; ;; (pprint (get-in data [:pages-index page-id :objects])) +;; ;; (println "==========") +;; ;; (pprint (get-in res [:pages-index page-id :objects])) - (t/is (= [shape-1-id shape-2-id] - (get-in data [:pages-index page-id :objects group-1-id :shapes]))) +;; (let [objects (get-in res [:pages-index page-id :objects])] +;; (t/is (= [group-b-id rect-e-id] (get-in objects [frame-a-id :shapes]))) +;; (t/is (= [group-a-id] (get-in objects [frame-b-id :shapes]))) +;; (t/is (= frame-b-id (get-in objects [group-a-id :frame-id]))) +;; (t/is (= frame-b-id (get-in objects [rect-a-id :frame-id]))) +;; (t/is (= frame-b-id (get-in objects [rect-b-id :frame-id]))) +;; (t/is (= frame-b-id (get-in objects [rect-c-id :frame-id])))))) - ;; After +;; (t/testing "Move elements to frame zero" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id uuid/zero +;; :shapes [group-a-id] +;; :index 0}] +;; res (cp/process-changes data changes)] - (t/is (= [shape-4-id group-1-id] - (get-in res [:pages-index page-id :objects cp/root :shapes]))) +;; (let [objects (get-in res [:pages-index page-id :objects])] +;; ;; (pprint (get-in data [:objects uuid/zero])) +;; ;; (println "==========") +;; ;; (pprint (get-in objects [uuid/zero])) - (t/is (= [shape-3-id shape-1-id shape-2-id] - (get-in res [:pages-index page-id :objects group-1-id :shapes]))) +;; (t/is (= [group-a-id frame-a-id frame-b-id] +;; (get-in objects [uuid/zero :shapes])))))) - ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) - ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) - )) +;; (t/testing "Don't allow to move inside self" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id group-a-id +;; :shapes [group-a-id]}] +;; res (cp/process-changes data changes)] +;; (t/is (= data res)))) +;; )) - (t/testing "case 5" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id cp/root - :index 0 - :shapes [shape-2-id]}] - res (cp/process-changes data changes)] - ;; (pprint (get-in data [:pages-index page-id :objects cp/root])) - ;; (pprint (get-in res [:pages-index page-id :objects cp/root])) +;; (t/deftest process-change-mov-objects-regression +;; (let [shape-1-id (uuid/custom 2 1) +;; shape-2-id (uuid/custom 2 2) +;; shape-3-id (uuid/custom 2 3) +;; frame-id (uuid/custom 1 1) +;; file-id (uuid/custom 4 4) +;; page-id (uuid/custom 0 1) - ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) - ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) +;; changes [{:type :add-obj +;; :id frame-id +;; :page-id page-id +;; :parent-id uuid/zero +;; :frame-id uuid/zero +;; :obj {:type :frame +;; :name "Frame"}} +;; {:type :add-obj +;; :page-id page-id +;; :frame-id frame-id +;; :parent-id frame-id +;; :id shape-1-id +;; :obj {:type :rect +;; :name "Shape 1"}} +;; {:type :add-obj +;; :page-id page-id +;; :id shape-2-id +;; :parent-id uuid/zero +;; :frame-id uuid/zero +;; :obj {:type :rect +;; :name "Shape 2"}} - ;; Before +;; {:type :add-obj +;; :page-id page-id +;; :id shape-3-id +;; :parent-id uuid/zero +;; :frame-id uuid/zero +;; :obj {:type :rect +;; :name "Shape 3"}} +;; ] +;; data (make-file-data file-id page-id) +;; data (cp/process-changes data changes)] - (t/is (= [shape-3-id shape-4-id group-1-id] - (get-in data [:pages-index page-id :objects cp/root :shapes]))) +;; (t/testing "preserve order on multiple shape mov 1" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :shapes [shape-2-id shape-3-id] +;; :parent-id uuid/zero +;; :index 0}] +;; res (cp/process-changes data changes)] - (t/is (= [shape-1-id shape-2-id] - (get-in data [:pages-index page-id :objects group-1-id :shapes]))) +;; ;; (println "==> BEFORE") +;; ;; (pprint (get-in data [:objects])) +;; ;; (println "==> AFTER") +;; ;; (pprint (get-in res [:objects])) - ;; After +;; (t/is (= [frame-id shape-2-id shape-3-id] +;; (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) +;; (t/is (= [shape-2-id shape-3-id frame-id] +;; (get-in res [:pages-index page-id :objects uuid/zero :shapes]))))) - (t/is (= [shape-2-id shape-3-id shape-4-id group-1-id] - (get-in res [:pages-index page-id :objects cp/root :shapes]))) +;; (t/testing "preserve order on multiple shape mov 1" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :shapes [shape-3-id shape-2-id] +;; :parent-id uuid/zero +;; :index 0}] +;; res (cp/process-changes data changes)] - (t/is (= [shape-1-id] - (get-in res [:pages-index page-id :objects group-1-id :shapes]))) +;; ;; (println "==> BEFORE") +;; ;; (pprint (get-in data [:objects])) +;; ;; (println "==> AFTER") +;; ;; (pprint (get-in res [:objects])) - )) +;; (t/is (= [frame-id shape-2-id shape-3-id] +;; (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) +;; (t/is (= [shape-3-id shape-2-id frame-id] +;; (get-in res [:pages-index page-id :objects uuid/zero :shapes]))))) - (t/testing "case 6" - (let [changes [{:type :mov-objects - :page-id page-id - :parent-id cp/root - :index 0 - :shapes [shape-2-id shape-1-id]}] - res (cp/process-changes data changes)] +;; (t/testing "move inside->outside-inside" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :shapes [shape-2-id] +;; :parent-id frame-id} +;; {:type :mov-objects +;; :page-id page-id +;; :shapes [shape-2-id] +;; :parent-id uuid/zero}] +;; res (cp/process-changes data changes)] - ;; (pprint (get-in data [:pages-index page-id :objects cp/root])) - ;; (pprint (get-in res [:pages-index page-id :objects cp/root])) +;; (t/is (= (get-in res [:pages-index page-id :objects shape-1-id :frame-id]) +;; (get-in data [:pages-index page-id :objects shape-1-id :frame-id]))) +;; (t/is (= (get-in res [:pages-index page-id :objects shape-2-id :frame-id]) +;; (get-in data [:pages-index page-id :objects shape-2-id :frame-id]))))) - ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) - ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) +;; )) - ;; Before - (t/is (= [shape-3-id shape-4-id group-1-id] - (get-in data [:pages-index page-id :objects cp/root :shapes]))) +;; (t/deftest process-change-move-objects-2 +;; (let [shape-1-id (uuid/custom 1 1) +;; shape-2-id (uuid/custom 1 2) +;; shape-3-id (uuid/custom 1 3) +;; shape-4-id (uuid/custom 1 4) +;; group-1-id (uuid/custom 1 5) +;; file-id (uuid/custom 1 6) +;; page-id (uuid/custom 0 1) - (t/is (= [shape-1-id shape-2-id] - (get-in data [:pages-index page-id :objects group-1-id :shapes]))) +;; changes [{:type :add-obj +;; :page-id page-id +;; :id shape-1-id +;; :frame-id uuid/zero +;; :obj {:id shape-1-id +;; :type :rect +;; :name "Shape a"}} +;; {:type :add-obj +;; :page-id page-id +;; :id shape-2-id +;; :frame-id uuid/zero +;; :obj {:id shape-2-id +;; :type :rect +;; :name "Shape b"}} +;; {:type :add-obj +;; :page-id page-id +;; :id shape-3-id +;; :frame-id uuid/zero +;; :obj {:id shape-3-id +;; :type :rect +;; :name "Shape c"}} +;; {:type :add-obj +;; :page-id page-id +;; :id shape-4-id +;; :frame-id uuid/zero +;; :obj {:id shape-4-id +;; :type :rect +;; :name "Shape d"}} +;; {:type :add-obj +;; :page-id page-id +;; :id group-1-id +;; :frame-id uuid/zero +;; :obj {:id group-1-id +;; :type :group +;; :name "Group"}} +;; {:type :mov-objects +;; :page-id page-id +;; :parent-id group-1-id +;; :shapes [shape-1-id shape-2-id]}] - ;; After +;; data (make-file-data file-id page-id) +;; data (cp/process-changes data changes)] - (t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id] - (get-in res [:pages-index page-id :objects cp/root :shapes]))) +;; (t/testing "case 1" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id uuid/zero +;; :index 2 +;; :shapes [shape-3-id]}] +;; res (cp/process-changes data changes)] - (t/is (not= nil - (get-in res [:pages-index page-id :objects group-1-id]))) +;; ;; Before - )) +;; (t/is (= [shape-3-id shape-4-id group-1-id] +;; (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) - )) +;; ;; After + +;; (t/is (= [shape-4-id shape-3-id group-1-id] +;; (get-in res [:pages-index page-id :objects uuid/zero :shapes]))) + +;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero])) +;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero])) +;; )) + +;; (t/testing "case 2" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id group-1-id +;; :index 2 +;; :shapes [shape-3-id]}] +;; res (cp/process-changes data changes)] + +;; ;; Before + +;; (t/is (= [shape-3-id shape-4-id group-1-id] +;; (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-1-id shape-2-id] +;; (get-in data [:pages-index page-id :objects group-1-id :shapes]))) + +;; ;; After: + +;; (t/is (= [shape-4-id group-1-id] +;; (get-in res [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-1-id shape-2-id shape-3-id] +;; (get-in res [:pages-index page-id :objects group-1-id :shapes]))) + +;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) +;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) +;; )) + +;; (t/testing "case 3" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id group-1-id +;; :index 1 +;; :shapes [shape-3-id]}] +;; res (cp/process-changes data changes)] + +;; ;; Before + +;; (t/is (= [shape-3-id shape-4-id group-1-id] +;; (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-1-id shape-2-id] +;; (get-in data [:pages-index page-id :objects group-1-id :shapes]))) + +;; ;; After + +;; (t/is (= [shape-4-id group-1-id] +;; (get-in res [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-1-id shape-3-id shape-2-id] +;; (get-in res [:pages-index page-id :objects group-1-id :shapes]))) + +;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) +;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) +;; )) + +;; (t/testing "case 4" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id group-1-id +;; :index 0 +;; :shapes [shape-3-id]}] +;; res (cp/process-changes data changes)] + +;; ;; Before + +;; (t/is (= [shape-3-id shape-4-id group-1-id] +;; (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-1-id shape-2-id] +;; (get-in data [:pages-index page-id :objects group-1-id :shapes]))) + +;; ;; After + +;; (t/is (= [shape-4-id group-1-id] +;; (get-in res [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-3-id shape-1-id shape-2-id] +;; (get-in res [:pages-index page-id :objects group-1-id :shapes]))) + +;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) +;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) +;; )) + +;; (t/testing "case 5" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id uuid/zero +;; :index 0 +;; :shapes [shape-2-id]}] +;; res (cp/process-changes data changes)] + +;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero])) +;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero])) + +;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) +;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) + +;; ;; Before + +;; (t/is (= [shape-3-id shape-4-id group-1-id] +;; (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-1-id shape-2-id] +;; (get-in data [:pages-index page-id :objects group-1-id :shapes]))) + +;; ;; After + +;; (t/is (= [shape-2-id shape-3-id shape-4-id group-1-id] +;; (get-in res [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-1-id] +;; (get-in res [:pages-index page-id :objects group-1-id :shapes]))) + +;; )) + +;; (t/testing "case 6" +;; (let [changes [{:type :mov-objects +;; :page-id page-id +;; :parent-id uuid/zero +;; :index 0 +;; :shapes [shape-2-id shape-1-id]}] +;; res (cp/process-changes data changes)] + +;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero])) +;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero])) + +;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id])) +;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id])) + +;; ;; Before + +;; (t/is (= [shape-3-id shape-4-id group-1-id] +;; (get-in data [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (= [shape-1-id shape-2-id] +;; (get-in data [:pages-index page-id :objects group-1-id :shapes]))) + +;; ;; After + +;; (t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id] +;; (get-in res [:pages-index page-id :objects uuid/zero :shapes]))) + +;; (t/is (not= nil +;; (get-in res [:pages-index page-id :objects group-1-id]))) + +;; )) + +;; )) diff --git a/common/test/common_tests/record_test.cljc b/common/test/common_tests/record_test.cljc new file mode 100644 index 0000000000..809924cf4c --- /dev/null +++ b/common/test/common_tests/record_test.cljc @@ -0,0 +1,55 @@ +;; 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 common-tests.record-test + (:require + [app.common.data.macros :as dm] + [app.common.record :as cr] + [clojure.test :as t])) + +(cr/defrecord Sample [a b]) + +(t/deftest operations + (let [o (pos->Sample 1 2)] + + (t/testing "get" + (t/is (= 1 (:a o))) + (t/is (= 1 (get o :a))) + (t/is (= nil (get o :c))) + (t/is (= :foo (get o :c :foo)))) + + (t/testing "known assoc" + (let [o (assoc o :a 100)] + (t/is (= 100 (:a o))))) + + (t/testing "unknown assoc" + (let [o (assoc o :c 176)] + (prn o) + (t/is (= 1 (:a o))) + (t/is (= 2 (:b o))) + (t/is (= 176 (:c o))))) + + (t/testing "contains" + (let [o (assoc o :c 176)] + (t/is (contains? o :a)) + (t/is (contains? o :b)) + (t/is (contains? o :c)) + (t/is (not (contains? o :d))))) + + #?(:cljs + (t/testing "transients" + (let [o (assoc o :c 123) + u (cr/clone o)] + (cr/assoc! u :a 10) + (cr/assoc! u :b 20) + (cr/assoc! u :c 124) + + (t/is (= 10 (dm/get-prop u :a))) + (t/is (= 20 (dm/get-prop u :b))) + (t/is (= 124 (:c u))) + (t/is (not= u o))))) + )) + diff --git a/common/test/common_tests/types_shape_interactions_test.cljc b/common/test/common_tests/types_shape_interactions_test.cljc index 4411d86f12..f7663a53cd 100644 --- a/common/test/common_tests/types_shape_interactions_test.cljc +++ b/common/test/common_tests/types_shape_interactions_test.cljc @@ -6,9 +6,10 @@ (ns common-tests.types-shape-interactions-test (:require - [app.common.geom.shapes :as gsh] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] [app.common.types.shape :as cts] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] @@ -17,8 +18,8 @@ (t/deftest set-event-type (let [interaction ctsi/default-interaction - shape (cts/make-minimal-shape :rect) - frame (cts/make-minimal-shape :frame)] + shape (cts/setup-shape {:type :rect}) + frame (cts/setup-shape {:type :frame})] (t/testing "Set event type unchanged" (let [new-interaction @@ -46,7 +47,8 @@ new-interaction (ctsi/set-event-type interaction :after-delay frame)] (t/is (= :after-delay (:event-type new-interaction))) - (t/is (= 300 (:delay new-interaction))))))) + (t/is (= 300 (:delay new-interaction))))) + )) (t/deftest set-action-type @@ -148,7 +150,7 @@ (t/is (= "https://example.com" (:url new-interaction))))))) (t/deftest option-delay - (let [frame (cts/make-minimal-shape :frame) + (let [frame (cts/setup-shape {:type :frame}) i1 ctsi/default-interaction i2 (ctsi/set-event-type i1 :after-delay frame)] @@ -160,7 +162,6 @@ (let [new-interaction (ctsi/set-delay i2 1000)] (t/is (= 1000 (:delay new-interaction))))))) - (t/deftest option-destination (let [destination (uuid/next) i1 ctsi/default-interaction @@ -211,10 +212,10 @@ (t/deftest option-overlay-opts - (let [base-frame (-> (cts/make-minimal-shape :frame) + (let [base-frame (-> (cts/setup-shape {:type :frame}) (assoc-in [:selrect :width] 100) (assoc-in [:selrect :height] 100)) - overlay-frame (-> (cts/make-minimal-shape :frame) + overlay-frame (-> (cts/setup-shape {:type :frame}) (assoc-in [:selrect :width] 30) (assoc-in [:selrect :height] 20)) objects {(:id base-frame) base-frame @@ -277,37 +278,35 @@ (t/is (= relative-to-id (:position-relative-to new-interaction))))))) (defn setup-selrect [{:keys [x y width height] :as obj}] - (let [rect (gsh/make-rect x y width height) - center (gsh/center-rect rect) - selrect (gsh/rect->selrect rect) - points (gsh/rect->points rect)] + (let [rect (grc/make-rect x y width height) + center (grc/rect->center rect) + points (grc/rect->points rect)] (-> obj - (assoc :selrect selrect) + (assoc :selrect rect) (assoc :points points)))) (t/deftest calc-overlay-position - (let [base-frame (-> (cts/make-minimal-shape :frame) - (assoc :width 100) - (assoc :height 100) - (setup-selrect)) - popup (-> (cts/make-minimal-shape :frame) - (assoc :width 50) - (assoc :height 50) - (assoc :x 10) - (assoc :y 10) - (setup-selrect)) + (let [base-frame (cts/setup-shape + {:type :frame + :width 100 + :height 100}) + popup (cts/setup-shape + {:type :frame + :width 50 + :height 50 + :x 10 + :y 10}) + rect (cts/setup-shape + {:type :rect + :width 50 + :height 50 + :x 10 + :y 10}) - rect (-> (cts/make-minimal-shape :rect) - (assoc :width 50) - (assoc :height 50) - (assoc :x 10) - (assoc :y 10) - (setup-selrect)) - - overlay-frame (-> (cts/make-minimal-shape :frame) - (assoc :width 30) - (assoc :height 20) - (setup-selrect)) + overlay-frame (cts/setup-shape + {:type :frame + :width 30 + :height 20}) objects {(:id base-frame) base-frame (:id popup) popup @@ -798,12 +797,12 @@ (t/deftest remap-interactions - (let [frame1 (cts/make-minimal-shape :frame) - frame2 (cts/make-minimal-shape :frame) - frame3 (cts/make-minimal-shape :frame) - frame4 (cts/make-minimal-shape :frame) - frame5 (cts/make-minimal-shape :frame) - frame6 (cts/make-minimal-shape :frame) + (let [frame1 (cts/setup-shape {:type :frame}) + frame2 (cts/setup-shape {:type :frame}) + frame3 (cts/setup-shape {:type :frame}) + frame4 (cts/setup-shape {:type :frame}) + frame5 (cts/setup-shape {:type :frame}) + frame6 (cts/setup-shape {:type :frame}) objects {(:id frame3) frame3 (:id frame4) frame4 diff --git a/common/test/common_tests/types_test.cljc b/common/test/common_tests/types_test.cljc index ff8ed5fef6..c3c5508c6b 100644 --- a/common/test/common_tests/types_test.cljc +++ b/common/test/common_tests/types_test.cljc @@ -24,7 +24,8 @@ (t/deftest types-shape-spec (sg/check! (sg/for [fdata (sg/generator ::cts/shape)] - (t/is (sm/validate ::cts/shape fdata))))) + (binding [app.common.data.macros/*assert-context* true] + (t/is (sm/valid? ::cts/shape fdata)))))) (t/deftest types-page-spec (-> (sg/for [fdata (sg/generator ::ctp/page)] diff --git a/frontend/deps.edn b/frontend/deps.edn index e304106200..71119fdfaa 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -13,8 +13,8 @@ funcool/tubax {:mvn/version "2021.05.20-0"} funcool/rumext - {:git/tag "v2.3" - :git/sha "09942e7" + {:git/tag "v2.6" + :git/sha "97203a5" :git/url "https://github.com/funcool/rumext.git" } diff --git a/frontend/dev/bench.cljs b/frontend/dev/bench.cljs index bc1b6cdf81..dc1fbb0182 100644 --- a/frontend/dev/bench.cljs +++ b/frontend/dev/bench.cljs @@ -3,63 +3,95 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes.rect :as gsr] - [app.common.perf :as perf] + [app.common.geom.matrix :as gmt] + [app.common.geom.shapes.transforms :as gst] + [app.common.record :as cr] + [app.common.geom.rect :as grc] + [app.common.geom.rect_impl :as grci] [app.common.types.modifiers :as ctm] - [clojure.spec.alpha :as s] - [clojure.test.check.generators :as gen])) + [app.common.types.shape :as cts] + [app.util.perf :as perf])) -(def points - (gen/sample (s/gen ::gpt/point) 20)) - -(defn bench-points +(defn random [] - #_(perf/benchmark - :f #(gpt/center-points-old points) - :samples 20 - :max-iterations 500000 - :name "base") - (perf/benchmark - :f #(gpt/center-points points) - :max-iterations 500000 - :samples 20 - :name "optimized")) + (js/Math.random)) -(def modifiers - (-> (ctm/empty) - (ctm/move (gpt/point 100 200)) - (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5)) - (ctm/move (gpt/point -100 -200)) - (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5)) - (ctm/rotation (gpt/point 0 0) -100) - (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5)))) - -(defn bench-modifiers +(defn force-gc [] - (perf/benchmark - :f #(ctm/modifiers->transform modifiers) - :max-iterations 50000 - :samples 20 - :name "current") + (js/gc)) - #_(perf/benchmark - :f #(ctm/modifiers->transform-2 modifiers) - :max-iterations 50000 - :samples 20 - :name "optimized")) +;; (defn bench-modifiers +;; [] +;; (println "") +;; (println "===> BENCH MODIFIERS <===") +;; (let [modifiers (-> (ctm/empty) +;; (ctm/move (gpt/point 100 200)) +;; (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5)) +;; (ctm/move (gpt/point -100 -200)) +;; (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5)) +;; (ctm/rotation (gpt/point 0 0) -100) +;; (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5)))] +;; (perf/benchmark +;; :gc force-gc +;; :iterations 50000 +;; :name "modifiers->transform:old" +;; :run-fn #(ctm/modifiers->transform-old modifiers)) + +;; (perf/benchmark +;; :gc force-gc +;; :iterations 50000 +;; :name "modifiers->transform:new" +;; :run-fn #(ctm/modifiers->transform modifiers)))) + + +;; (defn bench-apply-transform +;; [] +;; (println "") +;; (println "===> BENCH APPLY TRANFORM <===") +;; (let [modifiers (-> (ctm/empty) +;; (ctm/move (gpt/point 100 200)) +;; (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5)) +;; (ctm/move (gpt/point -100 -200)) +;; (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5)) +;; (ctm/rotation (gpt/point 0 0) -100) +;; (ctm/resize (gpt/point 100 200) (gpt/point 2.0 0.5))) +;; transform (ctm/modifiers->transform modifiers) + +;; shape (cts/setup-shape {:type :rect +;; :x 0 +;; :y 0 +;; :width 10 +;; :height 10})] + +;; ;; (app.common.pprint/pprint shape) + +;; (perf/benchmark +;; :gc force-gc +;; :iterations 400 +;; :name "apply-transform:old" +;; :run-fn #(gst/apply-transform shape modifiers)) + +;; (perf/benchmark +;; :gc force-gc +;; :iterations 400 +;; :name "apply-transform:new" +;; :run-fn #(gst/apply-transform' shape modifiers)) +;; )) -;; (ctm/modifiers->transform-2 modifiers) (defn ^:dev/after-load after-load [] - #_(bench-modifiers)) + ;; (bench-apply-transform) + ;; (let [o (grc/make-rect 1 1 10 10)] + ;; (prn o) + ;; (prn (-> o + ;; (cr/assoc! :x 40) + ;; (grc/update-rect! :size))) + ;; ) + ) (defn main - [& [name]] - (case name - "points" (bench-points) - "modifiers" (bench-modifiers) - (println "available: points")) - #_(.exit js/process 0)) - + [& params] + ;; (bench-apply-transform) + ;; (bench-apply-transform) + nil) diff --git a/frontend/package.json b/frontend/package.json index ae24e8fbfa..a5a347c33f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,18 +12,18 @@ "defaults" ], "scripts": { - "compile-test": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", - "lint": "clj-kondo --parallel --lint src/", - "lint-scss": "yarn run prettier -c resources/styles -c src/**/*.scss", - "run-test": "node target/tests.js", - "test": "yarn run compile-test && yarn run run-test", - "watch-gulp": "gulp watch", - "watch-main": "shadow-cljs watch main", - "watch-test": "clojure -M:dev:shadow-cljs watch test", + "test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", + "lint:clj": "clj-kondo --parallel --lint src/", + "lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss", + "test:run": "node target/tests.js", + "test:watch": "clojure -M:dev:shadow-cljs watch test", + "test": "yarn run test:compile && yarn run test:run", + "gulp:watch": "gulp watch", + "watch": "shadow-cljs watch main", "validate-translations": "node ./scripts/validate-translations.js", "find-unused-translations": "node ./scripts/find-unused-translations.js", - "test-e2e": "cypress run", - "test-e2e-gui": "cypress open" + "test:e2e": "cypress run", + "test:e2e-gui": "cypress open" }, "devDependencies": { "autoprefixer": "^10.4.13", diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs index f30014832c..12d8500ab9 100644 --- a/frontend/src/app/libs/file_builder.cljs +++ b/frontend/src/app/libs/file_builder.cljs @@ -7,7 +7,7 @@ (ns app.libs.file-builder (:require [app.common.data :as d] - [app.common.file-builder :as fb] + [app.common.files.builder :as fb] [app.common.media :as cm] [app.common.types.components-list :as ctkl] [app.common.uuid :as uuid] diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index d31c6929d9..879810ff16 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages :as cp] + [app.common.files.helpers :as cfh] [app.common.schema :as sm] [app.common.time :as dt] [app.common.uri :as u] @@ -607,8 +607,8 @@ ptk/WatchEvent (watch [_ state _] (let [projects (get state :dashboard-projects) - unames (cp/retrieve-used-names projects) - name (cp/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")) + unames (cfh/get-used-names projects) + name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")) team-id (:current-team-id state) params {:name name :team-id team-id} @@ -823,8 +823,8 @@ on-error rx/throw}} (meta params) files (get state :dashboard-files) - unames (cp/retrieve-used-names files) - name (cp/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")) + unames (cfh/get-used-names files) + name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")) features (cond-> #{} (features/active-feature? state :components-v2) (conj "components/v2")) @@ -1033,11 +1033,11 @@ in-project? (contains? pparams :project-id) name (if in-project? (let [files (get state :dashboard-files) - unames (cp/retrieve-used-names files)] - (cp/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))) + unames (cfh/get-used-names files)] + (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))) (let [projects (get state :dashboard-projects) - unames (cp/retrieve-used-names projects)] - (cp/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")))) + unames (cfh/get-used-names projects)] + (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1")))) params (if in-project? {:project-id (:project-id pparams) :name name} diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index ce0cf18a77..da760a812a 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -10,13 +10,14 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.features :as ffeat] + [app.common.files.helpers :as cfh] [app.common.geom.align :as gal] [app.common.geom.point :as gpt] [app.common.geom.proportions :as gpp] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.grid-layout :as gslg] [app.common.logging :as log] - [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.text :as txt] @@ -436,8 +437,8 @@ ptk/WatchEvent (watch [it state _] (let [pages (get-in state [:workspace-data :pages-index]) - unames (cp/retrieve-used-names pages) - name (cp/generate-unique-name unames "Page 1") + unames (cfh/get-used-names pages) + name (cfh/generate-unique-name unames "Page 1") changes (-> (pcb/empty-changes it) (pcb/add-empty-page id name))] @@ -451,9 +452,9 @@ (watch [it state _] (let [id (uuid/next) pages (get-in state [:workspace-data :pages-index]) - unames (cp/retrieve-used-names pages) + unames (cfh/get-used-names pages) page (get-in state [:workspace-data :pages-index page-id]) - name (cp/generate-unique-name unames (:name page)) + name (cfh/generate-unique-name unames (:name page)) page (-> page (assoc :name name) @@ -491,7 +492,7 @@ (let [components-to-delete (->> page :objects vals - (filter #(true? (:main-instance? %))) + (filter #(true? (:main-instance %))) (map :component-id)) changes (reduce (fn [changes component-id] @@ -597,7 +598,7 @@ (defn update-shape [id attrs] (dm/assert! (uuid? id)) - (dm/assert! (cts/shape-attrs? attrs)) + (dm/assert! (cts/valid-shape-attrs? attrs)) (ptk/reify ::update-shape ptk/WatchEvent (watch [_ _ _] @@ -631,7 +632,7 @@ (rx/of (update-shape shape-id {:name name}))) ;; Update the component in case if shape is a main instance - (when (:main-instance? shape) + (when (:main-instance shape) (when-let [component-id (:component-id shape)] (rx/of (dwl/rename-component component-id name))))))))))) @@ -641,7 +642,7 @@ (defn update-selected-shapes [attrs] - (dm/assert! (cts/shape-attrs? attrs)) + (dm/assert! (cts/valid-shape-attrs? attrs)) (ptk/reify ::update-selected-shapes ptk/WatchEvent (watch [_ state _] @@ -742,7 +743,7 @@ ;; Unmask groups whose mask have moved outside (pcb/update-shapes groups-to-unmask (fn [shape] - (assoc shape :masked-group? false))) + (assoc shape :masked-group false))) ;; Detach shapes moved out of their component (pcb/update-shapes shapes-to-detach ctk/detach-shape) @@ -750,12 +751,12 @@ ;; Make non root a component moved inside another one (pcb/update-shapes shapes-to-deroot (fn [shape] - (assoc shape :component-root? nil))) + (assoc shape :component-root nil))) ;; Make root a subcomponent moved outside its parent component (pcb/update-shapes shapes-to-reroot (fn [shape] - (assoc shape :component-root? true))) + (assoc shape :component-root true))) ;; Reset constraints depending on the new parent (pcb/update-shapes shapes-to-unconstraint @@ -855,7 +856,7 @@ ;; removed, and it must be converted to a normal group. (let [obj (get objects id) parent (get objects (:parent-id obj))] - (if (and (:masked-group? parent) + (if (and (:masked-group parent) (= id (first (:shapes parent))) (not= (:id parent) parent-id)) (conj group-ids (:id parent)) @@ -990,8 +991,8 @@ (defn- move-shape [shape] - (let [bbox (-> shape :points gsh/points->selrect) - pos (gpt/point (:x bbox) (:y bbox))] + (let [bbox (-> shape :points grc/points->rect) + pos (gpt/point (:x bbox) (:y bbox))] (dwt/update-position (:id shape) pos))) (defn align-objects @@ -1003,20 +1004,18 @@ (ptk/reify ::align-objects ptk/WatchEvent (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state) moved (if (= 1 (count selected)) (align-object-to-parent objects (first selected) axis) (align-objects-list objects selected axis)) - ids (map :id moved) - undo-id (js/Symbol)] + undo-id (js/Symbol)] (when (can-align? selected objects) (rx/concat (rx/of (dwu/start-undo-transaction undo-id)) (->> (rx/from moved) (rx/map move-shape)) - (rx/of (ptk/data-event :layout/update ids) + (rx/of (ptk/data-event :layout/update (mapv :id moved)) (dwu/commit-undo-transaction undo-id)))))))) (defn align-object-to-parent @@ -1029,7 +1028,7 @@ (defn align-objects-list [objects selected axis] (let [selected-objs (map #(get objects %) selected) - rect (gsh/selection-rect selected-objs)] + rect (gsh/shapes->rect selected-objs)] (mapcat #(gal/align-to-rect % rect axis objects) selected-objs))) (defn can-distribute? [selected] @@ -1671,7 +1670,7 @@ selected-objs (map #(get paste-objects %) selected) first-selected-obj (first selected-objs) page-selected (wsh/lookup-selected state) - wrapper (gsh/selection-rect selected-objs) + wrapper (gsh/shapes->rect selected-objs) orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper)) frame-id (first page-selected) frame-object (get page-objects frame-id) @@ -1716,22 +1715,25 @@ margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper))) (min (- (:height frame-object) (:height wrapper)))) - ;; Pasted objects mustn't exceed the selected frame x limit + ;; Pasted objects mustn't exceed the selected frame x limit paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object)) (+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x)) (:x frame-object)) - ;; Pasted objects mustn't exceed the selected frame y limit + ;; Pasted objects mustn't exceed the selected frame y limit paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object)) (+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y)) (:y frame-object)) delta (if (= origin-frame-id uuid/zero) - ;; When the origin isn't in a frame the result is pasted in the center. - (gpt/subtract (gsh/center-shape frame-object) (gsh/center-selrect wrapper)) - ;; When pasting from one frame to another frame the object position must be limited to container boundaries. If the pasted object doesn't fit we try to: - ;; - Align it to the limits on the x and y axis - ;; - Respect the distance of the object to the right and bottom in the original frame + ;; When the origin isn't in a frame the result is pasted in the center. + (gpt/subtract (gsh/shape->center frame-object) (grc/rect->center wrapper)) + ;; When pasting from one frame to another frame the object + ;; position must be limited to container boundaries. If + ;; the pasted object doesn't fit we try to: + ;; + ;; - Align it to the limits on the x and y axis + ;; - Respect the distance of the object to the right and bottom in the original frame (gpt/point paste-x paste-y))] [frame-id frame-id delta])) @@ -1797,10 +1799,9 @@ (-> shape (assoc :frame-id frame-id :parent-id parent-id) (cond-> detach? - (-> - ;; this is used later, if the paste needs to create a new component from the detached shape - (assoc :saved-component-root? (:component-root? shape)) - ctk/detach-shape)) + ;; this is used later, if the paste needs to create a new component from the detached shape + (-> (assoc :saved-component-root (:component-root shape)) + (ctk/detach-shape))) ;; if is a text, remove references to external typographies (cond-> (= (:type shape) :text) (ctt/remove-external-typographies file-id))))) @@ -1876,7 +1877,7 @@ page-objects (wsh/lookup-page-objects state) frame-id (first page-selected) frame-object (get page-objects frame-id)] - (gsh/center-shape frame-object)) + (gsh/shape->center frame-object)) :else (deref ms/mouse-position))) @@ -2057,6 +2058,7 @@ (log/error :msg (str "Error removing " (:name media-obj)) :hint (ex-message %) :error %) + (js/console.log (.-stack %)) (rx/of (error-in-remove-graphics))))))) (defn- remove-graphics @@ -2076,10 +2078,8 @@ media (vals (:media file-data')) media-points - (map #(assoc % :points (gsh/rect->points {:x 0 - :y 0 - :width (:width %) - :height (:height %)})) + (map #(assoc % :points (-> (grc/make-rect 0 0 (:width %) (:height %)) + (grc/rect->points))) media) shape-grid diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 2868ce4a8b..31f23655bc 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -207,8 +207,8 @@ (try (dm/assert! "expect valid vector of changes" - (and (cpc/changes? redo-changes) - (cpc/changes? undo-changes))) + (and (cpc/valid-changes? redo-changes) + (cpc/valid-changes? undo-changes))) (update-in state path (fn [file] (-> file diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index e01ba8eb37..ab99818c96 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -7,7 +7,7 @@ (ns app.main.data.workspace.drawing "Drawing interactions." (:require - [app.common.types.shape :as cts] + [app.common.data.macros :as dm] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing.box :as box] @@ -23,44 +23,43 @@ ;; --- Select for Drawing (defn select-for-drawing - ([tool] (select-for-drawing tool nil)) - ([tool data] - (ptk/reify ::select-for-drawing - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-drawing assoc :tool tool :object data) - ;; When changing drawing tool disable "scale text" mode - ;; automatically, to help users that ignore how this - ;; mode works. - (update :workspace-layout disj :scale-text))) + [tool] + (ptk/reify ::select-for-drawing + ptk/UpdateEvent + (update [_ state] + (-> state + (update :workspace-drawing assoc :tool tool) + ;; When changing drawing tool disable "scale text" mode + ;; automatically, to help users that ignore how this + ;; mode works. + (update :workspace-layout disj :scale-text))) - ptk/WatchEvent - (watch [_ _ stream] - (rx/merge - (when (= tool :path) - (rx/of (start-drawing :path))) + ptk/WatchEvent + (watch [_ _ stream] + (rx/merge + (when (= tool :path) + (rx/of (start-drawing :path))) - (when (= tool :curve) - (let [stopper (->> stream (rx/filter dwc/interrupt?))] - (->> stream - (rx/filter (ptk/type? ::common/handle-finish-drawing)) - (rx/take 1) - (rx/observe-on :async) - (rx/map #(select-for-drawing tool data)) - (rx/take-until stopper)))) - - ;; NOTE: comments are a special case and they manage they - ;; own interrupt cycle.q - (when (and (not= tool :comments) - (not= tool :path)) - (let [stopper (rx/filter (ptk/type? ::clear-drawing) stream)] - (->> stream - (rx/filter dwc/interrupt?) - (rx/take 1) - (rx/map common/clear-drawing) - (rx/take-until stopper))))))))) + (when (= tool :curve) + (let [stopper (rx/filter dwc/interrupt? stream)] + (->> stream + (rx/filter (ptk/type? ::common/handle-finish-drawing)) + (rx/map (constantly tool)) + (rx/take 1) + (rx/observe-on :async) + (rx/map select-for-drawing) + (rx/take-until stopper)))) + ;; NOTE: comments are a special case and they manage they + ;; own interrupt cycle. + (when (and (not= tool :comments) + (not= tool :path)) + (let [stopper (rx/filter (ptk/type? ::clear-drawing) stream)] + (->> stream + (rx/filter dwc/interrupt?) + (rx/take 1) + (rx/map common/clear-drawing) + (rx/take-until stopper)))))))) ;; NOTE/TODO: when an exception is raised in some point of drawing the ;; draw lock is not released so the user need to refresh in order to @@ -68,7 +67,7 @@ (defn start-drawing [type] - {:pre [(keyword? type)]} + (dm/assert! (keyword? type)) (let [lock-id (uuid/next)] (ptk/reify ::start-drawing ptk/UpdateEvent @@ -77,7 +76,7 @@ ptk/WatchEvent (watch [_ state stream] - (let [lock (get-in state [:workspace-drawing :lock])] + (let [lock (dm/get-in state [:workspace-drawing :lock])] (when (= lock lock-id) (rx/merge (rx/of (handle-drawing type)) @@ -89,23 +88,13 @@ (defn handle-drawing [type] (ptk/reify ::handle-drawing - ptk/UpdateEvent - (update [_ state] - (let [data (cts/make-minimal-shape type)] - (update-in state [:workspace-drawing :object] merge data))) - ptk/WatchEvent (watch [_ _ _] (rx/of (case type - :path - (path/handle-new-shape) - - :curve - (curve/handle-drawing-curve) - - ;; default - (box/handle-drawing-box)))))) + :path (path/handle-new-shape) + :curve (curve/handle-drawing) + (box/handle-drawing type)))))) diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index 2f5209e5f0..4ac0afb2d4 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -6,12 +6,13 @@ (ns app.main.data.workspace.drawing.box (:require + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.flex-layout :as gslf] [app.common.geom.shapes.grid-layout :as gslg] [app.common.math :as mth] - [app.common.pages.helpers :as cph] [app.common.types.modifiers :as ctm] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] @@ -42,13 +43,16 @@ (defn resize-shape [{:keys [x y width height] :as shape} initial point lock?] (if (and (some? x) (some? y) (some? width) (some? height)) - (let [draw-rect (gsh/make-rect initial (cond-> point lock? (adjust-ratio initial))) - shape-rect (gsh/make-rect x y width height) + (let [draw-rect (grc/make-rect initial (cond-> point lock? (adjust-ratio initial))) + shape-rect (grc/make-rect x y width height) - scalev (gpt/point (/ (:width draw-rect) (:width shape-rect)) - (/ (:height draw-rect) (:height shape-rect))) + scalev (gpt/point (/ (:width draw-rect) + (:width shape-rect)) + (/ (:height draw-rect) + (:height shape-rect))) - movev (gpt/to-vec (gpt/point shape-rect) (gpt/point draw-rect))] + movev (gpt/to-vec (gpt/point shape-rect) + (gpt/point draw-rect))] (-> shape (assoc :click-draw? false) @@ -65,24 +69,24 @@ (fn [state] (update-in state [:workspace-drawing :object] gsh/absolute-move (gpt/point x y)))) -(defn handle-drawing-box [] - (ptk/reify ::handle-drawing-box +(defn handle-drawing + [type] + (ptk/reify ::handle-drawing ptk/WatchEvent (watch [_ state stream] - (let [stoper? #(or (ms/mouse-up? %) (= % :interrupt)) - stoper (rx/filter stoper? stream) - layout (get state :workspace-layout) - zoom (get-in state [:workspace-local :zoom] 1) - snap-pixel? (contains? layout :snap-pixel-grid) + (let [stoper (rx/filter #(or (ms/mouse-up? %) (= % :interrupt)) stream) + layout (get state :workspace-layout) + zoom (dm/get-in state [:workspace-local :zoom] 1) - snap-precision (if (>= zoom zoom-half-pixel-precision) 0.5 1) - initial (cond-> @ms/mouse-position snap-pixel? (gpt/round-step snap-precision)) + snap-pixel? (contains? layout :snap-pixel-grid) + snap-prec (if (>= zoom zoom-half-pixel-precision) 0.5 1) + initial (cond-> @ms/mouse-position snap-pixel? (gpt/round-step snap-prec)) - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - focus (:workspace-focus-selected state) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + focus (:workspace-focus-selected state) - fid (ctst/top-nested-frame objects initial) + fid (ctst/top-nested-frame objects initial) flex-layout? (ctl/flex-layout? objects fid) grid-layout? (ctl/grid-layout? objects fid) @@ -90,47 +94,41 @@ drop-index (when flex-layout? (gslf/get-drop-index fid objects initial)) drop-cell (when grid-layout? (gslg/get-drop-cell fid objects initial)) - shape (get-in state [:workspace-drawing :object]) - shape (-> shape - (cts/setup-shape {:x (:x initial) - :y (:y initial) - :width 0.01 - :height 0.01}) - (cond-> (and (cph/frame-shape? shape) - (not= fid uuid/zero)) - (assoc :fills [] :hide-in-viewer true)) + shape (-> (cts/setup-shape {:type type + :x (:x initial) + :y (:y initial) + :frame-id fid + :parent-id fid + :initialized? true + :click-draw? true + :hide-in-viewer (and (= type :frame) (not= fid uuid/zero))}) + (cond-> (some? drop-index) + (with-meta {:index drop-index})) + (cond-> (some? drop-cell) + (with-meta {:cell drop-cell}))) - (assoc :frame-id fid) + ] - (cond-> (some? drop-index) - (with-meta {:index drop-index})) - - (cond-> (some? drop-cell) - (with-meta {:cell drop-cell})) - - (assoc :initialized? true) - (assoc :click-draw? true))] (rx/concat ;; Add shape to drawing state - (rx/of #(assoc-in state [:workspace-drawing :object] shape)) - + (rx/of #(update % :workspace-drawing assoc :object shape)) ;; Initial SNAP - (->> - (rx/concat - (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus initial) - (rx/map move-drawing)) + (->> (rx/concat + (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus initial) + (rx/map move-drawing)) - (->> ms/mouse-position - (rx/filter #(> (gpt/distance % initial) (/ 2 zoom))) - (rx/with-latest vector ms/mouse-position-shift) - (rx/switch-map - (fn [[point :as current]] - (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus point) - (rx/map #(conj current %))))) - (rx/map - (fn [[_ shift? point]] - #(update-drawing % initial (cond-> point snap-pixel? (gpt/round-step snap-precision)) shift?))))) - (rx/take-until stoper)) + (->> ms/mouse-position + (rx/filter #(> (gpt/distance % initial) (/ 2 zoom))) + (rx/with-latest vector ms/mouse-position-shift) + (rx/switch-map + (fn [[point :as current]] + (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus point) + (rx/map #(conj current %))))) + (rx/map + (fn [[_ shift? point]] + #(update-drawing % initial (cond-> point snap-pixel? (gpt/round-step snap-prec)) shift?))))) + + (rx/take-until stoper)) (->> (rx/of (common/handle-finish-drawing)) (rx/delay 100))))))) diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index ed9790e248..29092499a0 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -6,6 +6,7 @@ (ns app.main.data.workspace.drawing.common (:require + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages.helpers :as cph] @@ -30,46 +31,46 @@ (ptk/reify ::handle-finish-drawing ptk/WatchEvent (watch [_ state _] - (let [tool (get-in state [:workspace-drawing :tool]) - shape (get-in state [:workspace-drawing :object]) - objects (wsh/lookup-page-objects state)] + (let [tool (dm/get-in state [:workspace-drawing :tool]) + shape (dm/get-in state [:workspace-drawing :object]) + objects (wsh/lookup-page-objects state) + page-id (:current-page-id state)] + (rx/concat (when (:initialized? shape) - (let [page-id (:current-page-id state) + (let [click-draw? (:click-draw? shape) + text? (cph/text-shape? shape) + vbox (dm/get-in state [:workspace-local :vbox]) - click-draw? (:click-draw? shape) - text? (= :text (:type shape)) - - min-side (min 100 - (mth/floor (get-in state [:workspace-local :vbox :width])) - (mth/floor (get-in state [:workspace-local :vbox :height]))) + min-side (mth/min 100 + (mth/floor (dm/get-prop vbox :width)) + (mth/floor (dm/get-prop vbox :height))) shape (cond-> shape (not click-draw?) - (-> (assoc :grow-type :fixed)) + (assoc :grow-type :fixed) - (and click-draw? (not text?)) + (and ^boolean click-draw? (not ^boolean text?)) (-> (assoc :width min-side :height min-side) - (cts/setup-rect-selrect) + (cts/setup-shape) (gsh/transform-shape (ctm/move-modifiers (- (/ min-side 2)) (- (/ min-side 2))))) (and click-draw? text?) (-> (assoc :height 17 :width 4 :grow-type :auto-width) - (cts/setup-rect-selrect)) + (cts/setup-shape)) :always (dissoc :initialized? :click-draw?))] ;; Add & select the created shape to the workspace (rx/concat - (if (or (= :text (:type shape)) (= :frame (:type shape))) + (if (or (cph/text-shape? shape) (cph/frame-shape? shape)) (rx/of (dwu/start-undo-transaction (:id shape))) (rx/empty)) (rx/of (dwsh/add-shape shape {:no-select? (= tool :curve)})) - - (if (= :frame (:type shape)) + (if (cph/frame-shape? shape) (rx/concat (->> (uw/ask! {:cmd :selection/query :page-id page-id diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs index e4c400a047..0f2ddb675f 100644 --- a/frontend/src/app/main/data/workspace/drawing/curve.cljs +++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs @@ -6,13 +6,17 @@ (ns app.main.data.workspace.drawing.curve (:require + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.flex-layout :as gslf] [app.common.geom.shapes.grid-layout :as gslg] [app.common.geom.shapes.path :as gsp] + [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid] [app.main.data.workspace.drawing.common :as common] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] @@ -22,85 +26,93 @@ (def simplify-tolerance 0.3) -(defn stoper-event? [{:keys [type] :as event}] +(defn stoper-event? + [{:keys [type] :as event}] (ms/mouse-event? event) (= type :up)) -(defn initialize-drawing [state] - (assoc-in state [:workspace-drawing :object :initialized?] true)) - -(defn insert-point-segment [state point] - (let [segments (-> state - (get-in [:workspace-drawing :object :segments]) - (or []) - (conj point)) - content (gsp/segments->content segments) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect)] - (-> state - (update-in [:workspace-drawing :object] assoc - :segments segments - :content content - :selrect selrect - :points points)))) - -(defn setup-frame-curve [] - (ptk/reify ::setup-frame-path +(defn- insert-point + [point] + (ptk/reify ::insert-point ptk/UpdateEvent (update [_ state] + (update-in state [:workspace-drawing :object] + (fn [object] + (let [segments (-> (:segments object) + (conj point)) + content (gsp/segments->content segments) + selrect (gsh/content->selrect content) + points (grc/rect->points selrect)] + (-> object + (assoc :segments segments) + (assoc :content content) + (assoc :selrect selrect) + (assoc :points points)))))))) +(defn- setup-frame + [] + (ptk/reify ::setup-frame + ptk/UpdateEvent + (update [_ state] (let [objects (wsh/lookup-page-objects state) - content (get-in state [:workspace-drawing :object :content] []) - start (get-in content [0 :params] nil) + content (dm/get-in state [:workspace-drawing :object :content] []) + start (dm/get-in content [0 :params] nil) position (when start (gpt/point start)) frame-id (ctst/top-nested-frame objects position) flex-layout? (ctl/flex-layout? objects frame-id) + grid-layout? (ctl/grid-layout? objects frame-id) drop-index (when flex-layout? (gslf/get-drop-index frame-id objects position)) drop-cell (when grid-layout? (gslg/get-drop-cell frame-id objects position))] - (-> state - (assoc-in [:workspace-drawing :object :frame-id] frame-id) - (cond-> (some? drop-index) - (update-in [:workspace-drawing :object] with-meta {:index drop-index})) - (cond-> (some? drop-cell) - (update-in [:workspace-drawing :object] with-meta {:cell drop-cell}))))))) + (update-in state [:workspace-drawing :object] + (fn [object] + (-> object + (assoc :frame-id frame-id) + (assoc :parent-id frame-id) + (cond-> (some? drop-index) + (with-meta {:index drop-index})) + (cond-> (some? drop-cell) + (with-meta {:cell drop-cell}))))))))) -(defn curve-to-path [{:keys [segments] :as shape}] - (let [content (gsp/segments->content segments) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect)] - (-> shape - (dissoc :segments) - (assoc :content content) - (assoc :selrect selrect) - (assoc :points points) - - (cond-> (or (empty? points) (nil? selrect) (<= (count content) 1)) - (assoc :initialized? false))))) - -(defn finish-drawing-curve +(defn finish-drawing [] - (ptk/reify ::finish-drawing-curve + (ptk/reify ::finish-drawing ptk/UpdateEvent (update [_ state] - (letfn [(update-curve [shape] - (-> shape - (update :segments #(ups/simplify % simplify-tolerance)) - (curve-to-path)))] - (-> state - (update-in [:workspace-drawing :object] update-curve)))))) + (update-in state [:workspace-drawing :object] + (fn [{:keys [segments] :as shape}] + (let [segments (ups/simplify segments simplify-tolerance) + content (gsp/segments->content segments) + selrect (gsh/content->selrect content) + points (grc/rect->points selrect)] + (-> shape + (dissoc :segments) + (assoc :content content) + (assoc :selrect selrect) + (assoc :points points) + (cond-> (or (empty? points) + (nil? selrect) + (<= (count content) 1)) + (assoc :initialized? false))))))))) -(defn handle-drawing-curve [] - (ptk/reify ::handle-drawing-curve + +(defn handle-drawing [] + (ptk/reify ::handle-drawing ptk/WatchEvent (watch [_ _ stream] (let [stoper (rx/filter stoper-event? stream) - mouse (rx/sample 10 ms/mouse-position)] + mouse (rx/sample 10 ms/mouse-position) + shape (cts/setup-shape {:type :path + :initialized? true + :frame-id uuid/zero + :parent-id uuid/zero + :segments []})] (rx/concat - (rx/of initialize-drawing) + (rx/of #(update % :workspace-drawing assoc :object shape)) (->> mouse - (rx/map (fn [pt] #(insert-point-segment % pt))) + (rx/map insert-point) (rx/take-until stoper)) - (rx/of (setup-frame-curve) - (finish-drawing-curve) - (common/handle-finish-drawing))))))) + (rx/of + (setup-frame) + (finish-drawing) + (common/handle-finish-drawing))))))) diff --git a/frontend/src/app/main/data/workspace/grid_layout/editor.cljs b/frontend/src/app/main/data/workspace/grid_layout/editor.cljs index 62a25035c6..f4dce997f4 100644 --- a/frontend/src/app/main/data/workspace/grid_layout/editor.cljs +++ b/frontend/src/app/main/data/workspace/grid_layout/editor.cljs @@ -6,7 +6,7 @@ (ns app.main.data.workspace.grid-layout.editor (:require - [app.common.geom.shapes :as gsh] + [app.common.geom.rect :as grc] [app.main.data.workspace.state-helpers :as wsh] [potok.core :as ptk])) @@ -58,7 +58,7 @@ (let [{:keys [x y width height]} srect x (+ x (/ width 2) (- (/ (:width vport) 2 zoom))) y (+ y (/ height 2) (- (/ (:height vport) 2 zoom))) - srect (gsh/make-selrect x y width height)] + srect (grc/make-rect x y width height)] (-> local (update :vbox merge (select-keys srect [:x :y :x1 :x2 :y1 :y2]))))))))))) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index 311dc2b70d..0b56ff5769 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -80,18 +80,20 @@ (:name (first shapes)) base-name) - selrect (gsh/selection-rect shapes) + selrect (gsh/shapes->rect shapes) group-idx (->> shapes last :id (cph/get-position-on-parent objects) inc) - group (-> (cts/make-minimal-group frame-id selrect gname) - (cts/setup-shape selrect) - (assoc :shapes (mapv :id shapes) - :parent-id parent-id - :frame-id frame-id - :index group-idx)) + + group (cts/setup-shape {:type :group + :name gname + :shapes (mapv :id shapes) + :selrect selrect + :parent-id parent-id + :frame-id frame-id + :index group-idx}) ;; Shapes that are in a component, but are not root, must be detached, ;; because they will be now children of a non instance group. @@ -290,7 +292,7 @@ (pcb/update-shapes [(:id group)] (fn [group] (assoc group - :masked-group? true + :masked-group true :selrect (:selrect first-shape) :points (:points first-shape) :transform (:transform first-shape) @@ -319,7 +321,7 @@ (-> changes (pcb/update-shapes [(:id mask)] (fn [shape] - (dissoc shape :masked-group?))) + (dissoc shape :masked-group))) (pcb/resize-parents [(:id mask)]))) (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)) diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index ee38af28b7..6d72f5e8d8 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -8,8 +8,8 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] - [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.types.page :as ctp] @@ -33,8 +33,8 @@ (let [page (wsh/lookup-page state) flows (get-in page [:options :flows] []) - unames (into #{} (map :name flows)) - name (cp/generate-unique-name unames "Flow 1") + unames (cfh/get-used-names flows) + name (cfh/generate-unique-name unames "Flow 1") new-flow {:id (uuid/next) :name name diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 011c32caf6..9ecbb45a92 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -24,9 +24,11 @@ [app.common.uuid :as uuid] [app.main.data.events :as ev] [app.main.data.messages :as msg] + [app.main.data.workspace :as-alias dw] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.libraries-helpers :as dwlh] + [app.main.data.workspace.notifications :as-alias dwn] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] @@ -562,7 +564,7 @@ (defn ext-library-changed [file-id modified-at revn changes] (dm/assert! (uuid? file-id)) - (dm/assert! (ch/changes? changes)) + (dm/assert! (ch/valid-changes? changes)) (ptk/reify ::ext-library-changed ptk/UpdateEvent (update [_ state] @@ -901,11 +903,11 @@ (ptk/reify ::watch-component-changes ptk/WatchEvent (watch [_ state stream] - (let [components-v2 (features/active-feature? state :components-v2) + (let [components-v2? (features/active-feature? state :components-v2) stopper (->> stream - (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) + (rx/filter #(or (= ::dw/finalize-page (ptk/type %)) (= ::watch-component-changes (ptk/type %))))) workspace-data-s @@ -914,35 +916,36 @@ (rx/from-atom refs/workspace-data {:emit-current-value? true})) ;; Need to get the file data before the change, so deleted shapes ;; still exist, for example - (rx/buffer 3 1)) + (rx/buffer 3 1) + (rx/filter (fn [[old-data]] (some? old-data)))) change-s (->> stream (rx/filter #(or (dch/commit-changes? %) - (= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change))) + (ptk/type? % ::dwn/handle-file-change))) (rx/observe-on :async)) check-changes (fn [[event [old-data _mid_data _new-data]]] (when old-data - (let [{:keys [file-id changes save-undo? undo-group]} - (deref event) + (let [{:keys [file-id changes save-undo? undo-group]} (deref event) - components-changed + changed-components (when (or (nil? file-id) (= file-id (:id old-data))) - (reduce #(into %1 (ch/components-changed old-data %2)) - #{} - changes))] + (->> changes + (map (partial ch/components-changed old-data)) + (reduce into #{})))] - (when (and (d/not-empty? components-changed) save-undo?) + (when (and (d/not-empty? changed-components) save-undo?) (log/info :msg "DETECTED COMPONENTS CHANGED" - :ids (map str components-changed) + :ids (map str changed-components) :undo-group undo-group) - (run! st/emit! - (map #(launch-component-sync % (:id old-data) undo-group) - components-changed))))))] - (when components-v2 + (->> changed-components + (map #(launch-component-sync % (:id old-data) undo-group)) + (run! st/emit!))))))] + + (when components-v2? (->> change-s (rx/with-latest-from workspace-data-s) (rx/map check-changes) diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index ebe7e1cdde..257ae93489 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -11,7 +11,6 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.logging :as log] - [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] @@ -75,8 +74,8 @@ [(assoc root :component-id new-id :component-file file-id - :component-root? true - :main-instance? true)]])) + :component-root true + :main-instance true)]])) changes (-> changes (pcb/add-component (:id root-shape) @@ -136,7 +135,7 @@ position (gpt/point (:x main-instance-shape) (:y main-instance-shape)) - component-instance-extra-data (if components-v2 {:main-instance? true} {}) + component-instance-extra-data (if components-v2 {:main-instance true} {}) [new-instance-shape new-instance-shapes] (when (and (some? main-instance-page) (some? main-instance-shape)) @@ -181,8 +180,9 @@ (not (nil? parent-id)) (assoc :parent-id parent-id)) + ;; on copy/paste old id is used later to reorder the paster layers changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true}) - (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) ; on copy/paste old id is used later to reorder the paster layers + (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) changes @@ -204,7 +204,7 @@ (let [shape (ctn/get-shape container shape-id)] (if (and (ctk/instance-head? shape) (not first)) ;; Subinstances are not detached, but converted in top instances - (pcb/update-shapes changes [(:id shape)] #(assoc % :component-root? true)) + (pcb/update-shapes changes [(:id shape)] #(assoc % :component-root true)) ;; Otherwise, detach the shape and all children (let [children-ids (:shapes shape)] (reduce #(generate-detach-recursive %1 container %2 false) @@ -499,7 +499,7 @@ ;; * IF THE INITIAL SHAPE IS THE SUBINSTANCE, the sync is done against ;; the remote component. Therefore, IShape-2-2-1 is synched with ;; Shape-1-1. Then the "touched" flags are reset, and the -;; "remote-synced?" flag is set (it will be set until the shape is +;; "remote-synced" flag is set (it will be set until the shape is ;; touched again or it's synced forced normal or inverse with the ;; near component). ;; @@ -509,19 +509,19 @@ ;; cleared. Then, the "touched" flags THAT ARE TRUE are copied to ;; Shape-2-2-1. This may cause that Shape-2-2-1 is now touched respect ;; to Shape-1-1, and so, some attributes are not copied in a subsequent -;; normal sync. Or, if "remote-synced?" flag is set in IShape-2-2-1, -;; all touched flags are cleared in Shape-2-2-1 and "remote-synced?" +;; normal sync. Or, if "remote-synced" flag is set in IShape-2-2-1, +;; all touched flags are cleared in Shape-2-2-1 and "remote-synced" ;; is removed. ;; ;; * IN AN INVERSE SYNC INITIATED IN THE SUBINSTANCE, the update is done ;; to the remote component. E.g. IShape-2-2-1 attributes are copied into -;; Shape-1-1, and then touched cleared and "remote-synced?" flag set. +;; Shape-1-1, and then touched cleared and "remote-synced" flag set. ;; ;; #### WARNING: there are two conditions that are invisible to user: ;; - When the near shape (Shape-2-2-1) is touched respect the remote ;; one (Shape-1-1), there is no asterisk displayed anywhere. ;; - When the instance shape (IShape-2-2-1) is synced with the remote -;; shape (remote-synced? = true), the user will see that this shape +;; shape (remote-synced = true), the user will see that this shape ;; is different than the one in the near component (Shape-2-2-1) ;; but it's not touched. @@ -540,7 +540,7 @@ shape-main (when component (ctf/get-ref-shape library component shape-inst)) - initial-root? (:component-root? shape-inst) + initial-root? (:component-root shape-inst) root-inst shape-inst root-main (when component @@ -673,7 +673,7 @@ component (ctkl/get-component library (:component-id shape-inst)) shape-main (ctf/get-ref-shape library component shape-inst) - initial-root? (:component-root? shape-inst) + initial-root? (:component-root shape-inst) root-inst shape-inst root-main (ctf/get-component-root library component)] @@ -867,7 +867,7 @@ (assoc :shape-ref (:id original-shape)) set-remote-synced? - (assoc :remote-synced? true)))) + (assoc :remote-synced true)))) update-original-shape (fn [original-shape _new-shape] original-shape) @@ -967,8 +967,8 @@ :attr :component-file :val (:component-file shape')} {:type :set - :attr :component-root? - :val (:component-root? shape')} + :attr :component-root + :val (:component-root shape')} {:type :set :attr :shape-ref :val (:shape-ref shape')} @@ -1087,7 +1087,7 @@ reset-touched? nil copy-touched? - (if (:remote-synced? origin-shape) + (if (:remote-synced origin-shape) nil (set/union (:touched dest-shape) @@ -1117,7 +1117,7 @@ (log/info :msg (str "CHANGE-REMOTE-SYNCED? " (if (cph/page? container) "[P] " "[C] ") (:name shape)) - :remote-synced? remote-synced?) + :remote-synced remote-synced?) (-> changes (update :redo-changes conj (make-change container @@ -1125,14 +1125,14 @@ :id (:id shape) :operations [{:type :set-remote-synced - :remote-synced? remote-synced?}]})) + :remote-synced remote-synced?}]})) (update :undo-changes d/preconj (make-change container {:type :mod-obj :id (:id shape) :operations [{:type :set-remote-synced - :remote-synced? (:remote-synced? shape)}]})))))) + :remote-synced (:remote-synced shape)}]})))))) (defn- update-attrs "The main function that implements the attribute sync algorithm. Copy @@ -1158,7 +1158,7 @@ origin-shape (reposition-shape origin-shape origin-root dest-root) touched (get dest-shape :touched #{})] - (loop [attrs (seq (keys cp/component-sync-attrs)) + (loop [attrs (seq (keys ctk/sync-attrs)) roperations [] uoperations []] @@ -1196,7 +1196,7 @@ :val (get dest-shape attr) :ignore-touched true} - attr-group (get cp/component-sync-attrs attr)] + attr-group (get ctk/sync-attrs attr)] (if (or (= (get origin-shape attr) (get dest-shape attr)) (and (touched attr-group) omit-touched?)) diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 424899ad86..90261b6348 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -259,27 +259,29 @@ "Convert a media object that contains a bitmap image into shapes, one shape of type :image and one group that contains it." [pos {:keys [name width height id mtype] :as media-obj}] - (let [group-shape (cts/make-shape :group - {:x (:x pos) - :y (:y pos) - :width width - :height height} - {:name name - :frame-id uuid/zero - :parent-id uuid/zero}) + (let [group-shape (cts/setup-shape + {:type :group + :x (:x pos) + :y (:y pos) + :width width + :height height + :name name + :frame-id uuid/zero + :parent-id uuid/zero}) - img-shape (cts/make-shape :image - {:x (:x pos) - :y (:y pos) - :width width - :height height - :metadata {:id id - :width width - :height height - :mtype mtype}} - {:name name - :frame-id uuid/zero - :parent-id (:id group-shape)})] + img-shape (cts/setup-shape + {:type :image + :x (:x pos) + :y (:y pos) + :width width + :height height + :metadata {:id id + :width width + :height height + :mtype mtype} + :name name + :frame-id uuid/zero + :parent-id (:id group-shape)})] (rx/of [group-shape [img-shape]]))) (defn- add-shapes-and-component diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 4428173b2d..fe0c6b55b0 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -10,12 +10,13 @@ [app.common.data :as d] [app.common.data.macros :as dm] [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.pages.common :as cpc] [app.common.pages.helpers :as cph] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] + [app.common.types.shape.attrs :refer [editable-attrs]] [app.common.types.shape.layout :as ctl] [app.main.constants :refer [zoom-half-pixel-precision]] [app.main.data.workspace.changes :as dch] @@ -49,7 +50,7 @@ [shape root transformed-shape transformed-root objects modif-tree] (let [root (cond - (:component-root? shape) + (:component-root shape) shape (nil? root) @@ -59,7 +60,7 @@ transformed-root (cond - (:component-root? transformed-shape) + (:component-root transformed-shape) transformed-shape (nil? transformed-root) @@ -376,21 +377,25 @@ (update [_ state] (assoc state :workspace-modifiers (calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree params)))))) -;; Rotation use different algorithm to calculate children modifiers (and do not use child constraints). +(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/selection-rect gsh/center-selrect))) + (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 - (->> shapes - (remove #(get % :blocked false)) - (filter #((cpc/editable-attrs (:type %)) :rotation)) - (map :id)) + (let [objects (wsh/lookup-page-objects state) + ids (sequence xf-rotation-shape shapes) get-modifier (fn [shape] diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 1620a8a41a..609fec2859 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -11,6 +11,7 @@ [app.common.geom.shapes.flex-layout :as gsl] [app.common.path.commands :as upc] [app.common.path.shapes-to-path :as upsp] + [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.changes :as dch] @@ -196,14 +197,13 @@ drag-events (rx/of (finish-drag))))))) -(defn handle-drawing-path +(defn handle-drawing [_id] - (ptk/reify ::handle-drawing-path + (ptk/reify ::handle-drawing ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state)] - (-> state - (assoc-in [:workspace-local :edit-path id :edit-mode] :draw)))) + (assoc-in state [:workspace-local :edit-path id :edit-mode] :draw))) ptk/WatchEvent (watch [_ _ stream] @@ -234,9 +234,8 @@ mousedown-events) (rx/of (common/finish-path "after-events"))))))) - -(defn setup-frame-path [] - (ptk/reify ::setup-frame-path +(defn setup-frame [] + (ptk/reify ::setup-frame ptk/UpdateEvent (update [_ state] (let [objects (wsh/lookup-page-objects state) @@ -245,10 +244,14 @@ frame-id (ctst/top-nested-frame objects position) flex-layout? (ctl/flex-layout? objects frame-id) drop-index (when flex-layout? (gsl/get-drop-index frame-id objects position))] - (-> state - (assoc-in [:workspace-drawing :object :frame-id] frame-id) - (cond-> (some? drop-index) - (update-in [:workspace-drawing :object] with-meta {:index drop-index}))))))) + + (update-in state [:workspace-drawing :object] + (fn [object] + (-> object + (assoc :frame-id frame-id) + (assoc :parent-id frame-id) + (cond-> (some? drop-index) + (with-meta {:index drop-index}))))))))) (defn handle-new-shape-result [shape-id] (ptk/reify ::handle-new-shape-result @@ -264,7 +267,7 @@ (watch [_ state _] (let [content (get-in state [:workspace-drawing :object :content] [])] (if (seq content) - (rx/of (setup-frame-path) + (rx/of (setup-frame) (dwdc/handle-finish-drawing) (dwe/start-edition-mode shape-id) (change-edit-mode :draw)) @@ -276,15 +279,17 @@ (ptk/reify ::handle-new-shape ptk/UpdateEvent (update [_ state] - (let [id (st/get-path-id state)] + (let [id (st/get-path-id state) + shape (cts/setup-shape {:type :path})] (-> state - (assoc-in [:workspace-local :edit-path id :snap-toggled] false)))) + (assoc-in [:workspace-local :edit-path id :snap-toggled] false) + (update :workspace-drawing assoc :object shape)))) ptk/WatchEvent (watch [_ state stream] - (let [shape-id (get-in state [:workspace-drawing :object :id])] + (let [shape-id (dm/get-in state [:workspace-drawing :object :id])] (rx/concat - (rx/of (handle-drawing-path shape-id)) + (rx/of (handle-drawing shape-id)) (->> stream (rx/filter (ptk/type? ::common/finish-path)) (rx/take 1) @@ -310,7 +315,7 @@ (if (= :draw edit-mode) (rx/concat (rx/of (dch/update-shapes [id] upsp/convert-to-path)) - (rx/of (handle-drawing-path id)) + (rx/of (handle-drawing id)) (->> stream (rx/filter (ptk/type? ::common/finish-path)) (rx/take 1) diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index b76016484c..09e26d4c53 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -8,6 +8,7 @@ (:require [app.common.geom.matrix :as gmt] [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.path.commands :as upc] @@ -28,7 +29,7 @@ [content] (-> content gsh/content->selrect - gsh/center-selrect)) + grc/rect->center)) (defn content->points+selrect "Given the content of a shape, calculate its points and selrect" @@ -45,7 +46,7 @@ flip-y (gmt/scale (gpt/point 1 -1)) :always (gmt/multiply (:transform-inverse shape (gmt/matrix)))) - center (or (gsh/center-shape shape) + center (or (gsh/shape->center shape) (content-center content)) base-content (gsh/transform-content @@ -54,16 +55,16 @@ ;; Calculates the new selrect with points given the old center points (-> (gsh/content->selrect base-content) - (gsh/rect->points) + (grc/rect->points) (gsh/transform-points center transform)) - points-center (gsh/center-points points) + points-center (gsh/points->center points) ;; Points is now the selrect but the center is different so we can create the selrect ;; through points selrect (-> points (gsh/transform-points points-center transform-inverse) - (gsh/points->selrect))] + (grc/points->rect))] [points selrect])) (defn update-selrect diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index 170b959197..b5f514d82f 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -7,6 +7,7 @@ (ns app.main.data.workspace.path.selection (:require [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.state :as st] @@ -116,7 +117,7 @@ (rx/concat (->> ms/mouse-position (rx/take-until stoper) - (rx/map #(gsh/points->rect [from-p %])) + (rx/map #(grc/points->rect [from-p %])) (rx/filter (partial valid-rect? zoom)) (rx/map update-area-selection)) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index cf74bbf1fd..9e1c8f0eef 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -235,7 +235,7 @@ [file-id {:keys [revn changes]}] (dm/assert! (uuid? file-id)) (dm/assert! (int? revn)) - (dm/assert! (cpc/changes? changes)) + (dm/assert! (cpc/valid-changes? changes)) (ptk/reify ::shapes-changes-persisted ptk/UpdateEvent diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 3219d56bec..8a09b22699 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -8,12 +8,14 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [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.pages :as cp] [app.common.pages.changes-builder :as pcb] + [app.common.pages.focus :as cpf] [app.common.pages.helpers :as cph] + [app.common.record :as cr] [app.common.types.component :as ctk] [app.common.types.file :as ctf] [app.common.types.page :as ctp] @@ -54,33 +56,28 @@ (ptk/reify ::handle-area-selection ptk/WatchEvent (watch [_ state stream] - (let [zoom (get-in state [:workspace-local :zoom] 1) - stop? (fn [event] (or (interrupt? event) (ms/mouse-up? event))) - stoper (->> stream (rx/filter stop?)) + (let [zoom (dm/get-in state [:workspace-local :zoom] 1) + stop? (fn [event] (or (interrupt? event) (ms/mouse-up? event))) + stoper (rx/filter stop? stream) - init-selrect - {:type :rect - :x1 (:x @ms/mouse-position) - :y1 (:y @ms/mouse-position) - :x2 (:x @ms/mouse-position) - :y2 (:y @ms/mouse-position)} + init-position @ms/mouse-position + + init-selrect (grc/make-rect + (dm/get-prop init-position :x) + (dm/get-prop init-position :y) + 0 0) calculate-selrect (fn [selrect [delta space?]] - (let [result - (cond-> selrect - :always - (-> (update :x2 + (:x delta)) - (update :y2 + (:y delta))) - - space? - (-> (update :x1 + (:x delta)) - (update :y1 + (:y delta))))] - (assoc result - :x (min (:x1 result) (:x2 result)) - :y (min (:y1 result) (:y2 result)) - :width (mth/abs (- (:x2 result) (:x1 result))) - :height (mth/abs (- (:y2 result) (:y1 result)))))) + (let [selrect (-> (cr/clone selrect) + (cr/update! :x2 + (:x delta)) + (cr/update! :y2 + (:y delta))) + selrect (if ^boolean space? + (-> selrect + (cr/update! :x1 + (:x delta)) + (cr/update! :y1 + (:y delta))) + selrect)] + (grc/update-rect! selrect :corners))) selrect-stream (->> ms/mouse-position @@ -89,9 +86,10 @@ (rx/filter some?) (rx/with-latest-from ms/keyboard-space) (rx/scan calculate-selrect init-selrect) - (rx/filter #(or (> (:width %) (/ 10 zoom)) - (> (:height %) (/ 10 zoom)))) + (rx/filter #(or (> (dm/get-prop % :width) (/ 10 zoom)) + (> (dm/get-prop % :height) (/ 10 zoom)))) (rx/take-until stoper))] + (rx/concat (if preserve? (rx/empty) @@ -193,10 +191,10 @@ (shift-select-shapes id nil)) ([id objects] - (ptk/reify ::shift-select-shapes-2 + (ptk/reify ::shift-select-shapes ptk/UpdateEvent (update [_ state] - (let [objects (or objects (wsh/lookup-page-objects state)) + (let [objects (or objects (wsh/lookup-page-objects state)) selection (-> state wsh/lookup-selected (conj id))] @@ -217,7 +215,7 @@ (let [objects (wsh/lookup-page-objects state) focus (:workspace-focus-selected state) ids (if (d/not-empty? focus) - (cp/filter-not-focus objects focus ids) + (cpf/filter-not-focus objects focus ids) ids)] (assoc-in state [:workspace-local :selected] ids))) @@ -236,7 +234,7 @@ ;; mode is active focus (:workspace-focus-selected state) objects (-> (wsh/lookup-page-objects state) - (cp/focus-objects focus)) + (cpf/focus-objects focus)) lookup (d/getf objects) parents (->> (wsh/lookup-selected state) @@ -282,14 +280,15 @@ (ptk/reify ::select-shapes-by-current-selrect ptk/WatchEvent (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state) - selected (wsh/lookup-selected state) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state) + selected (wsh/lookup-selected state) initial-set (if preserve? selected lks/empty-linked-set) - selrect (get-in state [:workspace-local :selrect]) - blocked? (fn [id] (get-in objects [id :blocked] false))] + selrect (dm/get-in state [:workspace-local :selrect]) + blocked? (fn [id] (dm/get-in objects [id :blocked] false))] + (when selrect (rx/empty) (->> (uw/ask-buffered! @@ -353,7 +352,7 @@ ([all-objects page ids delta it libraries library-data file-id init-changes] (let [shapes (map (d/getf all-objects) ids) - unames (volatile! (cp/retrieve-used-names (:objects page))) + unames (volatile! (cfh/get-used-names (:objects page))) update-unames! (fn [new-name] (vswap! unames conj new-name)) all-ids (reduce #(into %1 (cons %2 (cph/get-children-ids all-objects %2))) (d/ordered-set) ids) ids-map (into {} (map #(vector % (uuid/next))) all-ids) @@ -424,8 +423,8 @@ parent-id (or parent-id frame-id) name (:name obj) - is-component-root? (:saved-component-root? obj) - is-component-main? (:main-instance? obj) + is-component-root? (:saved-component-root obj) + is-component-main? (:main-instance obj) regenerate-component (fn [changes shape] (let [components-v2 (dm/get-in library-data [:options :components-v2]) @@ -438,9 +437,9 @@ :parent-id parent-id :frame-id frame-id) (dissoc :shapes - :main-instance? + :main-instance :shape-ref - :use-for-thumbnail?) + :use-for-thumbnail) (gsh/move delta) (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) @@ -486,7 +485,7 @@ (let [update-flows (fn [flows] (reduce (fn [flows frame] - (let [name (cp/generate-unique-name @unames "Flow 1") + (let [name (cfh/generate-unique-name @unames "Flow 1") _ (vswap! unames conj name) new-flow {:id (uuid/next) :name name diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index 15873ee1cb..2cf6841a03 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -93,7 +93,7 @@ (->> shapes (map :id) (ctt/sort-z-index objects) - (map (comp gsh/center-shape (d/getf objects)))) + (map (comp gsh/shape->center (d/getf objects)))) start (first points) end (reduce (fn [acc p] (gpt/add acc (gpt/to-vec start p))) points) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 9b9833ac0f..d878310038 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -8,7 +8,6 @@ (: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] @@ -28,61 +27,19 @@ [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)))) +(def valid-shape-map? + (sm/pred-fn ::cts/shape)) (defn prepare-add-shape - [changes attrs objects selected] - (let [id (or (:id attrs) (uuid/next)) - name (:name attrs) + [changes shape objects _selected] + (let [index (:index (meta shape)) + ;; FIXME: revisit + id (:id shape) - shape (make-new-shape - (assoc attrs :id id :name name) - objects - selected) - - index (:index (meta attrs)) - [row column :as cell] (:cell (meta attrs)) + [row column :as cell] (:cell (meta shape)) changes (-> changes (pcb/with-objects objects) @@ -90,8 +47,8 @@ (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? (:parent-id shape)) + (pcb/change-parent (:parent-id shape) [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)) @@ -100,10 +57,14 @@ [shape changes])) (defn add-shape - ([attrs] - (add-shape attrs {})) - ([attrs {:keys [no-select? no-update-layout?]}] - (dm/assert! (cts/shape-attrs? attrs)) + ([shape] + (add-shape shape {})) + ([shape {:keys [no-select? no-update-layout?]}] + + (dm/verify! + "expected a valid shape" + (cts/valid-shape? shape)) + (ptk/reify ::add-shape ptk/WatchEvent (watch [it state _] @@ -111,11 +72,10 @@ 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) + (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (prepare-add-shape shape objects selected)) undo-id (js/Symbol)] @@ -127,7 +87,7 @@ (when-not no-select? (dws/select-shapes (d/ordered-set (:id shape)))) (dwu/commit-undo-transaction undo-id)) - (when (= :text (:type attrs)) + (when (cph/text-shape? shape) (->> (rx/of (dwe/start-edition-mode (:id shape))) (rx/observe-on :async))))))))) @@ -183,7 +143,7 @@ components-v2 (features/active-feature? state :components-v2) - ids (cph/clean-loops objects ids) + ids (cph/clean-loops objects ids) in-component-copy? (fn [shape-id] @@ -237,7 +197,7 @@ ;; converted to a normal group. (let [obj (lookup id) parent (lookup (:parent-id obj))] - (if (and (:masked-group? parent) + (if (and (:masked-group parent) (= id (first (:shapes parent)))) (conj group-ids (:id parent)) group-ids))) @@ -305,7 +265,7 @@ (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 + (:main-instance shape)) ;; but check anyway (conj components (:component-id shape)) components))) [] @@ -329,7 +289,7 @@ (pcb/resize-parents all-parents) (pcb/update-shapes groups-to-unmask (fn [shape] - (assoc shape :masked-group? false))) + (assoc shape :masked-group false))) (pcb/update-shapes (map :id interacting-shapes) (fn [shape] (d/update-when shape :interactions @@ -350,7 +310,6 @@ (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) @@ -363,59 +322,78 @@ (defn create-and-add-shape - [type frame-x frame-y data] + [type frame-x frame-y {:keys [width height] :as attrs}] (ptk/reify ::create-and-add-shape ptk/WatchEvent (watch [_ state _] - (let [{:keys [width height]} data + (let [vbc (wsh/viewport-center state) + x (:x attrs (- (:x vbc) (/ width 2))) + y (:y attrs (- (: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})) - 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)) + selected (wsh/lookup-selected state) + base (cph/get-base-shape objects selected) + + parent-id (if (or (and (= 1 (count selected)) + (cph/frame-shape? (get objects (first selected)))) + (empty? selected)) + frame-id + (:parent-id base)) + + shape (cts/setup-shape + (-> attrs + (assoc :type type) + (assoc :x x) + (assoc :y y) + (assoc :frame-id frame-id) + (assoc :parent-id parent-id)))] - 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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; FIXME: looks (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)) + (let [srect (gsh/shapes->rect selected-objs) + selected-id (first selected) + + frame-id (dm/get-in objects [selected-id :frame-id]) + parent-id (or parent-id (dm/get-in objects [selected-id :parent-id])) + + attrs {:type :frame + :x (:x srect) + :y (:y srect) + :width (:width srect) + :height (:height srect)} + + shape (cts/setup-shape + (cond-> attrs + (some? id) + (assoc :id id) + + (some? frame-name) + (assoc :name frame-name) + + :always + (assoc :frame-id frame-id + :parent-id parent-id) + + :always + (with-meta {:index new-index}) + + (or (not= frame-id uuid/zero) without-fill?) + (assoc :fills [] :hide-in-viewer true))) [shape changes] (prepare-add-shape changes shape objects selected) @@ -476,7 +454,7 @@ (dm/assert! "expected valid shape-attrs value for `flags`" - (cts/shape-attrs? flags)) + (cts/valid-shape-attrs? flags)) (ptk/reify ::update-shape-flags ptk/WatchEvent diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 53fbbbafb1..52c2a60293 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -8,21 +8,20 @@ (:require [app.common.colors :as clr] [app.common.data :as d] - [app.common.exceptions :as ex] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] [app.common.geom.matrix :as gmt] [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.pages :as cp] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] - [app.common.spec :as us :refer [max-safe-int min-safe-int]] + [app.common.schema :as sm :refer [max-safe-int min-safe-int]] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] - [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] - [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.repo :as rp] @@ -34,19 +33,16 @@ [cuerdas.core :as str] [potok.core :as ptk])) -(defonce default-rect {:x 0 :y 0 :width 1 :height 1 :rx 0 :ry 0}) -(defonce default-circle {:r 0 :cx 0 :cy 0}) -(defonce default-image {:x 0 :y 0 :width 1 :height 1 :rx 0 :ry 0}) +(def default-rect + {:x 0 :y 0 :width 1 :height 1}) (defn- assert-valid-num [attr num] - (when-not (and (d/num? num) - (<= num max-safe-int) - (>= num min-safe-int)) - (ex/raise :type :assertion - :code :expr-validation - :hint (str/ffmt "%1 attribute has invalid value: %2" (d/name attr) num))) + (dm/verify! + ["%1 attribute has invalid value: %2" (d/name attr) num] + (and (d/num? num) + (<= num max-safe-int) + (>= num min-safe-int))) - ;; If the number is between 0-1 we round to 1 (same in negative form (cond (and (> num 0) (< num 1)) 1 (and (< num 0) (> num -1)) -1 @@ -54,28 +50,25 @@ (defn- assert-valid-pos-num [attr num] - (when-not (pos? num) - (ex/raise :type :assertion - :code :expr-validation - :hint (str/ffmt "%1 attribute should be positive" (d/name attr)))) + + (dm/verify! + ["%1 attribute should be positive" (d/name attr)] + (pos? num)) + num) (defn- assert-valid-blend-mode [mode] - (let [clean-value (-> mode - str/trim - str/lower - keyword)] - (when-not (contains? cts/blend-modes clean-value) - (ex/raise :type :assertion - :code :expr-validation - :hint (str/ffmt "%1 is not a valid blend mode" clean-value))) + (let [clean-value (-> mode str/trim str/lower keyword)] + (dm/verify! + ["%1 is not a valid blend mode" clean-value] + (contains? cts/blend-modes clean-value)) clean-value)) (defn- svg-dimensions [data] - (let [width (get-in data [:attrs :width] 100) - height (get-in data [:attrs :height] 100) - viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height)) + (let [width (dm/get-in data [:attrs :width] 100) + height (dm/get-in data [:attrs :height] 100) + viewbox (dm/get-in data [:attrs :viewBox] (str "0 0 " width " " height)) [x y width height] (->> (str/split viewbox #"\s+") (map d/parse-double)) width (if (= width 0) 1 width) @@ -94,9 +87,9 @@ :else (str tag)))) (defn setup-fill [shape] - (let [color-attr (str/trim (get-in shape [:svg-attrs :fill])) + (let [color-attr (str/trim (dm/get-in shape [:svg-attrs :fill])) color-attr (if (= color-attr "currentColor") clr/black color-attr) - color-style (str/trim (get-in shape [:svg-attrs :style :fill])) + color-style (str/trim (dm/get-in shape [:svg-attrs :style :fill])) color-style (if (= color-style "currentColor") clr/black color-style)] (cond-> shape ;; Color present as attribute @@ -111,26 +104,26 @@ (update :svg-attrs dissoc :fill) (assoc-in [:fills 0 :fill-color] (uc/parse-color color-style))) - (get-in shape [:svg-attrs :fill-opacity]) + (dm/get-in shape [:svg-attrs :fill-opacity]) (-> (update :svg-attrs dissoc :fill-opacity) (update-in [:svg-attrs :style] dissoc :fill-opacity) - (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :fill-opacity]) + (assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :fill-opacity]) (d/parse-double 1)))) - (get-in shape [:svg-attrs :style :fill-opacity]) + (dm/get-in shape [:svg-attrs :style :fill-opacity]) (-> (update-in [:svg-attrs :style] dissoc :fill-opacity) (update :svg-attrs dissoc :fill-opacity) - (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity]) + (assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :style :fill-opacity]) (d/parse-double 1))))))) (defn setup-stroke [shape] - (let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap]) - (get-in shape [:svg-attrs :style :stroke-linecap])) + (let [stroke-linecap (-> (or (dm/get-in shape [:svg-attrs :stroke-linecap]) + (dm/get-in shape [:svg-attrs :style :stroke-linecap])) ((d/nilf str/trim)) ((d/nilf keyword))) - color-attr (str/trim (get-in shape [:svg-attrs :stroke])) + color-attr (str/trim (dm/get-in shape [:svg-attrs :stroke])) color-attr (if (= color-attr "currentColor") clr/black color-attr) - color-style (str/trim (get-in shape [:svg-attrs :style :stroke])) + color-style (str/trim (dm/get-in shape [:svg-attrs :style :stroke])) color-style (if (= color-style "currentColor") clr/black color-style) shape @@ -145,24 +138,24 @@ (-> (update-in [:svg-attrs :style] dissoc :stroke) (assoc-in [:strokes 0 :stroke-color] (uc/parse-color color-style))) - (get-in shape [:svg-attrs :stroke-opacity]) + (dm/get-in shape [:svg-attrs :stroke-opacity]) (-> (update :svg-attrs dissoc :stroke-opacity) - (assoc-in [:strokes 0 :stroke-opacity] (-> (get-in shape [:svg-attrs :stroke-opacity]) + (assoc-in [:strokes 0 :stroke-opacity] (-> (dm/get-in shape [:svg-attrs :stroke-opacity]) (d/parse-double 1)))) - (get-in shape [:svg-attrs :style :stroke-opacity]) + (dm/get-in shape [:svg-attrs :style :stroke-opacity]) (-> (update-in [:svg-attrs :style] dissoc :stroke-opacity) - (assoc-in [:strokes 0 :stroke-opacity] (-> (get-in shape [:svg-attrs :style :stroke-opacity]) + (assoc-in [:strokes 0 :stroke-opacity] (-> (dm/get-in shape [:svg-attrs :style :stroke-opacity]) (d/parse-double 1)))) - (get-in shape [:svg-attrs :stroke-width]) + (dm/get-in shape [:svg-attrs :stroke-width]) (-> (update :svg-attrs dissoc :stroke-width) - (assoc-in [:strokes 0 :stroke-width] (-> (get-in shape [:svg-attrs :stroke-width]) + (assoc-in [:strokes 0 :stroke-width] (-> (dm/get-in shape [:svg-attrs :stroke-width]) (d/parse-double)))) - (get-in shape [:svg-attrs :style :stroke-width]) + (dm/get-in shape [:svg-attrs :style :stroke-width]) (-> (update-in [:svg-attrs :style] dissoc :stroke-width) - (assoc-in [:strokes 0 :stroke-width] (-> (get-in shape [:svg-attrs :style :stroke-width]) + (assoc-in [:strokes 0 :stroke-width] (-> (dm/get-in shape [:svg-attrs :style :stroke-width]) (d/parse-double)))) (and stroke-linecap (= (:type shape) :path)) @@ -172,106 +165,111 @@ :stroke-cap-end stroke-linecap))))] (cond-> shape - (d/any-key? (get-in shape [:strokes 0]) :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end) + (d/any-key? (dm/get-in shape [:strokes 0]) :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end) (assoc-in [:strokes 0 :stroke-style] :svg)))) (defn setup-opacity [shape] (cond-> shape - (get-in shape [:svg-attrs :opacity]) + (dm/get-in shape [:svg-attrs :opacity]) (-> (update :svg-attrs dissoc :opacity) - (assoc :opacity (-> (get-in shape [:svg-attrs :opacity]) + (assoc :opacity (-> (dm/get-in shape [:svg-attrs :opacity]) (d/parse-double 1)))) - (get-in shape [:svg-attrs :style :opacity]) + (dm/get-in shape [:svg-attrs :style :opacity]) (-> (update-in [:svg-attrs :style] dissoc :opacity) - (assoc :opacity (-> (get-in shape [:svg-attrs :style :opacity]) + (assoc :opacity (-> (dm/get-in shape [:svg-attrs :style :opacity]) (d/parse-double 1)))) - - (get-in shape [:svg-attrs :mix-blend-mode]) + (dm/get-in shape [:svg-attrs :mix-blend-mode]) (-> (update :svg-attrs dissoc :mix-blend-mode) - (assoc :blend-mode (-> (get-in shape [:svg-attrs :mix-blend-mode]) assert-valid-blend-mode))) + (assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :mix-blend-mode]) assert-valid-blend-mode))) - (get-in shape [:svg-attrs :style :mix-blend-mode]) + (dm/get-in shape [:svg-attrs :style :mix-blend-mode]) (-> (update-in [:svg-attrs :style] dissoc :mix-blend-mode) - (assoc :blend-mode (-> (get-in shape [:svg-attrs :style :mix-blend-mode]) assert-valid-blend-mode))))) + (assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :style :mix-blend-mode]) assert-valid-blend-mode))))) -(defn create-raw-svg [name frame-id svg-data {:keys [attrs] :as data}] - (let [{:keys [x y width height offset-x offset-y]} svg-data] - (-> {:id (uuid/next) - :type :svg-raw - :name name - :frame-id frame-id - :width width - :height height - :x x - :y y - :content (cond-> data - (map? data) (update :attrs usvg/clean-attrs))} - (assoc :svg-attrs attrs) - (assoc :svg-viewbox (-> (select-keys svg-data [:width :height]) - (assoc :x offset-x :y offset-y))) - (cts/setup-rect-selrect)))) +(defn create-raw-svg + [name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}] + (cts/setup-shape + {:type :svg-raw + :name name + :frame-id frame-id + :width width + :height height + :x x + :y y + :content (cond-> data + (map? data) (update :attrs usvg/clean-attrs)) + :svg-attrs attrs + :svg-viewbox {:width width + :height height + :x offset-x + :y offset-y}})) -(defn create-svg-root [frame-id parent-id svg-data] - (let [{:keys [name x y width height offset-x offset-y]} svg-data] - (-> {:id (uuid/next) - :type :group - :name name - :frame-id frame-id - :parent-id parent-id - :width width - :height height - :x (+ x offset-x) - :y (+ y offset-y)} - (cts/setup-rect-selrect) - (assoc :svg-attrs (-> (:attrs svg-data) - (dissoc :viewBox :xmlns) - (d/without-keys usvg/inheritable-props)))))) +(defn create-svg-root + [frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}] + (cts/setup-shape + {:type :group + :name name + :frame-id frame-id + :parent-id parent-id + :width width + :height height + :x (+ x offset-x) + :y (+ y offset-y) + :svg-attrs (-> attrs + (dissoc :viewBox) + (dissoc :xmlns) + (d/without-keys usvg/inheritable-props))})) -(defn create-group [name frame-id svg-data {:keys [attrs]}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - {:keys [x y width height offset-x offset-y]} svg-data] - (-> {:id (uuid/next) - :type :group - :name name - :frame-id frame-id - :x (+ x offset-x) - :y (+ y offset-y) - :width width - :height height} - (assoc :svg-transform svg-transform) - (assoc :svg-attrs (d/without-keys attrs usvg/inheritable-props)) - (assoc :svg-viewbox (-> (select-keys svg-data [:width :height]) - (assoc :x offset-x :y offset-y))) - (cts/setup-rect-selrect)))) +(defn create-group + [name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}] + (let [svg-transform (usvg/parse-transform (:transform attrs))] + (cts/setup-shape + {:type :group + :name name + :frame-id frame-id + :x (+ x offset-x) + :y (+ y offset-y) + :width width + :height height + :svg-transform svg-transform + :svg-attrs (d/without-keys attrs usvg/inheritable-props) + + :svg-viewbox {:width width + :height height + :x offset-x + :y offset-y}}))) (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] (when (and (contains? attrs :d) (seq (:d attrs))) + (let [svg-transform (usvg/parse-transform (:transform attrs)) - path-content (upp/parse-path (:d attrs)) - content (cond-> path-content - svg-transform - (gsh/transform-content svg-transform)) + path-content (upp/parse-path (:d attrs)) + content (cond-> path-content + svg-transform + (gsh/transform-content svg-transform)) - selrect (gsh/content->selrect content) - points (gsh/rect->points selrect) + selrect (gsh/content->selrect content) + points (grc/rect->points selrect) - origin (gpt/negate (gpt/point svg-data))] - (-> {:id (uuid/next) - :type :path - :name name - :frame-id frame-id - :content content - :selrect selrect - :points points} - (assoc :svg-viewbox (select-keys selrect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :d :transform)) - (assoc :svg-transform svg-transform) + origin (gpt/negate (gpt/point svg-data))] + + (-> (cts/setup-shape + {:type :path + :name name + :frame-id frame-id + :content content + :selrect selrect + :points points + :svg-viewbox (select-keys selrect [:x :y :width :height]) + :svg-attrs (dissoc attrs :d :transform) + :svg-transform svg-transform}) (gsh/translate-to-frame origin))))) (defn calculate-rect-metadata [rect-data transform] - (let [points (-> (gsh/rect->points rect-data) + (let [points (-> (grc/make-rect rect-data) + (grc/rect->points) (gsh/transform-points transform)) [selrect transform transform-inverse] (gsh/calculate-geometry points)] @@ -285,122 +283,113 @@ :transform transform :transform-inverse transform-inverse})) +(defn- parse-rect-attrs + [{:keys [x y width height]}] + {:x (d/parse-double x 0) + :y (d/parse-double y 0) + :width (d/parse-double width 1) + :height (d/parse-double height 1)}) (defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - transform (->> svg-transform + (let [transform (->> (usvg/parse-transform (:transform attrs)) (gmt/transform-in (gpt/point svg-data))) - rect (->> (select-keys attrs [:x :y :width :height]) - (d/mapm #(d/parse-double %2))) + origin (gpt/negate (gpt/point svg-data)) - origin (gpt/negate (gpt/point svg-data)) - - rect-data (-> (merge default-rect rect) + rect-data (-> (parse-rect-attrs attrs) (update :x - (:x origin)) - (update :y - (:y origin))) + (update :y - (:y origin)))] - metadata (calculate-rect-metadata rect-data transform)] - (-> {:id (uuid/next) - :type :rect - :name name - :frame-id frame-id} - (cond-> - (contains? attrs :rx) (assoc :rx (d/parse-double (:rx attrs 0))) - (contains? attrs :ry) (assoc :ry (d/parse-double (:ry attrs 0)))) + (cts/setup-shape + (-> (calculate-rect-metadata rect-data transform) + (assoc :type :rect) + (assoc :name name) + (assoc :frame-id frame-id) + (assoc :svg-viewbox (select-keys rect-data [:x :y :width :height])) + (assoc :svg-attrs (dissoc attrs :x :y :width :height :rx :ry :transform)) + (cond-> (contains? attrs :rx) + (assoc :rx (d/parse-double (:rx attrs) 0))) + (cond-> (contains? attrs :ry) + (assoc :ry (d/parse-double (:ry attrs) 0))))))) - (merge metadata) - (assoc :svg-viewbox (select-keys rect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :x :y :width :height :rx :ry :transform))))) +(defn- parse-circle-attrs + [attrs] + (into [] (comp (map (d/getf attrs)) + (map d/parse-double)) + [:cx :cy :r :rx :ry])) (defn create-circle-shape [name frame-id svg-data {:keys [attrs] :as data}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - transform (->> svg-transform + (let [[cx cy r rx ry] + (parse-circle-attrs attrs) + + transform (->> (usvg/parse-transform (:transform attrs)) (gmt/transform-in (gpt/point svg-data))) - circle (->> (select-keys attrs [:r :ry :rx :cx :cy]) - (d/mapm #(d/parse-double %2))) + rx (or r rx) + ry (or r ry) + origin (gpt/negate (gpt/point svg-data)) - {:keys [cx cy]} circle + rect-data {:x (- cx rx (:x origin)) + :y (- cy ry (:y origin)) + :width (* 2 rx) + :height (* 2 ry)}] - rx (or (:r circle) (:rx circle)) - ry (or (:r circle) (:ry circle)) - - rect {:x (- cx rx) - :y (- cy ry) - :width (* 2 rx) - :height (* 2 ry)} - - origin (gpt/negate (gpt/point svg-data)) - - rect-data (-> rect - (update :x - (:x origin)) - (update :y - (:y origin))) - - metadata (calculate-rect-metadata rect-data transform)] - (-> {:id (uuid/next) - :type :circle - :name name - :frame-id frame-id} - - (merge metadata) - (assoc :svg-viewbox (select-keys rect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :cx :cy :r :rx :ry :transform))))) + (cts/setup-shape + (-> (calculate-rect-metadata rect-data transform) + (assoc :type :circle) + (assoc :name name) + (assoc :frame-id frame-id) + (assoc :svg-viewbox rect-data) + (assoc :svg-attrs (dissoc attrs :cx :cy :r :rx :ry :transform)))))) (defn create-image-shape [name frame-id svg-data {:keys [attrs] :as data}] - (let [svg-transform (usvg/parse-transform (:transform attrs)) - transform (->> svg-transform - (gmt/transform-in (gpt/point svg-data))) + (let [transform (->> (usvg/parse-transform (:transform attrs)) + (gmt/transform-in (gpt/point svg-data))) - image-url (or (:href attrs) (:xlink:href attrs)) - image-data (get-in svg-data [:image-data image-url]) + image-url (or (:href attrs) (:xlink:href attrs)) + image-data (dm/get-in svg-data [:image-data image-url]) - rect (->> (select-keys attrs [:x :y :width :height]) - (d/mapm #(d/parse-double %2))) - origin (gpt/negate (gpt/point svg-data)) + metadata {:width (:width image-data) + :height (:height image-data) + :mtype (:mtype image-data) + :id (:id image-data)} - rect-data (-> (merge default-image rect) - (update :x - (:x origin)) - (update :y - (:y origin))) - - rect-metadata (calculate-rect-metadata rect-data transform)] + origin (gpt/negate (gpt/point svg-data)) + rect-data (-> (parse-rect-attrs attrs) + (update :x - (:x origin)) + (update :y - (:y origin)))] (when (some? image-data) - (-> {:id (uuid/next) - :type :image - :name name - :frame-id frame-id - :metadata {:width (:width image-data) - :height (:height image-data) - :mtype (:mtype image-data) - :id (:id image-data)}} + (cts/setup-shape + (-> (calculate-rect-metadata rect-data transform) + (assoc :type :image) + (assoc :name name) + (assoc :frame-id frame-id) + (assoc :metadata metadata) + (assoc :svg-viewbox (select-keys rect-data [:x :y :width :height])) + (assoc :svg-attrs (dissoc attrs :x :y :width :height :href :xlink:href))))))) - (merge rect-metadata) - (assoc :svg-viewbox (select-keys rect [:x :y :width :height])) - (assoc :svg-attrs (dissoc attrs :x :y :width :height :href :xlink:href)))))) -(defn parse-svg-element [frame-id svg-data element-data unames] - (let [{:keys [tag attrs hidden]} element-data - attrs (usvg/format-styles attrs) +(defn parse-svg-element [frame-id svg-data {:keys [tag attrs hidden] :as element-data} unames] + (let [attrs (usvg/format-styles attrs) element-data (cond-> element-data (map? element-data) (assoc :attrs attrs)) - name (or (:id attrs) (tag->name tag)) - att-refs (usvg/find-attr-references attrs) - references (usvg/find-def-references (:defs svg-data) att-refs) + name (or (:id attrs) (tag->name tag)) + att-refs (usvg/find-attr-references attrs) + references (usvg/find-def-references (:defs svg-data) att-refs) - href-id (-> (or (:href attrs) (:xlink:href attrs) "") - (subs 1)) - defs (:defs svg-data) + href-id (-> (or (:href attrs) (:xlink:href attrs) "") (subs 1)) + defs (:defs svg-data) - use-tag? (and (= :use tag) (contains? defs href-id))] + use-tag? (and (= :use tag) (contains? defs href-id))] (if use-tag? (let [;; Merge the data of the use definition with the properties passed as attributes - use-data (-> (get defs href-id) - (update :attrs #(d/deep-merge % (dissoc attrs :xlink:href :href)))) + use-data (-> (get defs href-id) + (update :attrs #(d/deep-merge % (dissoc attrs :xlink:href :href)))) displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0"))) - disp-matrix (str (gmt/translate-matrix displacement)) + disp-matrix (str (gmt/translate-matrix displacement)) element-data (-> element-data (assoc :tag :g) (update :attrs dissoc :x :y :width :height :href :xlink:href :transform) @@ -423,37 +412,32 @@ #_other (create-raw-svg name frame-id svg-data element-data)))] (when (some? shape) (let [shape (-> shape - (assoc :fills []) - (assoc :strokes []) (assoc :svg-defs (select-keys (:defs svg-data) references)) (setup-fill) (setup-stroke) - (setup-opacity)) + (setup-opacity))] - shape (cond-> shape - hidden (assoc :hidden true)) + [(cond-> shape + hidden (assoc :hidden true)) - children (cond->> (:content element-data) - (contains? usvg/parent-tags tag) - (mapv #(usvg/inherit-attributes attrs %)))] - - [shape children])))))) + (cond->> (:content element-data) + (contains? usvg/parent-tags tag) + (mapv #(usvg/inherit-attributes attrs %)))])))))) (defn create-svg-children [objects selected frame-id parent-id svg-data [unames children] [_index svg-element]] - (let [[new-shape new-children] (parse-svg-element frame-id svg-data svg-element unames)] - (if (some? new-shape) - (let [shape-id (:id new-shape) + (let [[shape new-children] (parse-svg-element frame-id svg-data svg-element unames)] + (if (some? shape) + (let [shape-id (:id shape) + shape (-> shape + (assoc :frame-id frame-id) + (assoc :parent-id parent-id)) + children (conj children shape) + unames (conj unames (:name shape))] - new-shape' (-> (dwsh/make-new-shape new-shape objects selected) - (assoc :parent-id parent-id)) - - children (conj children new-shape') - unames (conj unames (:name new-shape')) - - reducer-fn (partial create-svg-children objects selected frame-id shape-id svg-data)] - - (reduce reducer-fn [unames children] (d/enumerate new-children))) + (reduce (partial create-svg-children objects selected frame-id shape-id svg-data) + [unames children] + (d/enumerate new-children))) [unames children]))) @@ -502,111 +486,111 @@ (rx/map #(vector (:url uri-data) %))))) (rx/reduce (fn [acc [url image]] (assoc acc url image)) {}))) + (defn create-svg-shapes [svg-data {:keys [x y]} objects frame-id parent-id selected center?] (let [[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data) - x (mth/round - (if center? - (- x vb-x (/ vb-width 2)) - x)) - y (mth/round - (if center? - (- y vb-y (/ vb-height 2)) - y)) - unames (cp/retrieve-used-names objects) + unames (cfh/get-used-names objects) svg-name (str/replace (:name svg-data) ".svg" "") svg-data (-> svg-data - (assoc :x x - :y y - :offset-x vb-x - :offset-y vb-y - :width vb-width - :height vb-height - :name svg-name)) + (assoc :x (mth/round + (if center? + (- x vb-x (/ vb-width 2)) + x))) + (assoc :y (mth/round + (if center? + (- y vb-y (/ vb-height 2)) + y))) + (assoc :offset-x vb-x) + (assoc :offset-y vb-y) + (assoc :width vb-width) + (assoc :height vb-height) + (assoc :name svg-name)) - [def-nodes svg-data] (-> svg-data - (usvg/fix-default-values) - (usvg/fix-percents) - (usvg/extract-defs)) - - svg-data (assoc svg-data :defs def-nodes) + [def-nodes svg-data] + (-> svg-data + (usvg/fix-default-values) + (usvg/fix-percents) + (usvg/extract-defs)) + svg-data (assoc svg-data :defs def-nodes) root-shape (create-svg-root frame-id parent-id svg-data) - root-id (:id root-shape) + root-id (:id root-shape) - ;; In penpot groups have the size of their children. To respect the imported - ;; svg size and empty space let's create a transparent shape as background to respect the imported size - base-background-shape {:tag :rect - :attrs {:x (str vb-x) - :y (str vb-y) - :width (str vb-width) - :height (str vb-height) - :fill "none" - :id "base-background"} - :hidden true - :content []} + ;; In penpot groups have the size of their children. To + ;; respect the imported svg size and empty space let's create + ;; a transparent shape as background to respect the imported + ;; size + background + {:tag :rect + :attrs {:x (dm/str vb-x) + :y (dm/str vb-y) + :width (dm/str vb-width) + :height (dm/str vb-height) + :fill "none" + :id "base-background"} + :hidden true + :content []} - svg-data (-> svg-data - (assoc :defs def-nodes) - (assoc :content (into [base-background-shape] (:content svg-data)))) + svg-data (-> svg-data + (assoc :defs def-nodes) + (assoc :content (into [background] (:content svg-data)))) ;; Create the root shape - new-shape (dwsh/make-new-shape root-shape objects selected) - root-attrs (-> (:attrs svg-data) (usvg/format-styles)) - [_ new-children] + [_ children] (reduce (partial create-svg-children objects selected frame-id root-id svg-data) [unames []] (d/enumerate (->> (:content svg-data) (mapv #(usvg/inherit-attributes root-attrs %)))))] - [new-shape new-children])) + [root-shape children])) (defn add-svg-shapes [svg-data position] + ;; (app.common.pprint/pprint svg-data {:length 100 :level 100}) (ptk/reify ::add-svg-shapes ptk/WatchEvent (watch [it state _] (try - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame-id (ctst/top-nested-frame objects position) - 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]))) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + frame-id (ctst/top-nested-frame objects position) + selected (wsh/lookup-selected state) + base (cph/get-base-shape objects selected) - parent-id - (if (or selected-frame? (empty? selected)) - frame-id - (:parent-id base)) + selected-id (first selected) + selected-frame? (and (= 1 (count selected)) + (= :frame (dm/get-in objects [selected-id :type]))) + + parent-id (if (or selected-frame? (empty? selected)) + frame-id + (:parent-id base)) [new-shape new-children] (create-svg-shapes svg-data position objects frame-id parent-id selected true) - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/add-object new-shape)) + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/add-object new-shape)) - changes - (reduce (fn [changes new-child] - (-> changes (pcb/add-object new-child))) - changes new-children) + changes (reduce (fn [changes new-child] + (pcb/add-object changes new-child)) + changes + new-children) - changes (pcb/resize-parents changes - (->> changes - :redo-changes - (filter #(= :add-obj (:type %))) - (map :id) - reverse - vec)) - undo-id (js/Symbol)] + changes (pcb/resize-parents changes + (->> (:redo-changes changes) + (filter #(= :add-obj (:type %))) + (map :id) + (reverse) + (vec))) + undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) @@ -614,7 +598,7 @@ (ptk/data-event :layout/update [(:id new-shape)]) (dwu/commit-undo-transaction undo-id))) - (catch :default e - (.error js/console "Error SVG" e) + (catch :default cause + (js/console.log (.-stack cause)) (rx/throw {:type :svg-parser - :data e})))))) + :data cause})))))) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 2e94d95160..6f72371b5f 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -11,6 +11,7 @@ [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.flex-layout :as gslf] [app.common.geom.shapes.grid-layout :as gslg] @@ -109,7 +110,7 @@ (let [{:keys [width height]} (:selrect shape) {:keys [rotation]} shape - shape-center (gsh/center-shape shape) + shape-center (gsh/shape->center shape) shape-transform (:transform shape) shape-transform-inverse (:transform-inverse shape) @@ -304,8 +305,8 @@ ptk/WatchEvent (watch [_ _ stream] (let [stoper (rx/filter ms/mouse-up? stream) - group (gsh/selection-rect shapes) - group-center (gsh/center-selrect group) + group (gsh/shapes->rect shapes) + group-center (grc/rect->center group) initial-angle (gpt/angle @ms/mouse-position group-center) calculate-angle @@ -717,7 +718,8 @@ objects (wsh/lookup-page-objects state page-id) shape (get objects id) - bbox (-> shape :points gsh/points->selrect) + ;; FIXME: performance rect + bbox (-> shape :points grc/points->rect) cpos (gpt/point (:x bbox) (:y bbox)) pos (gpt/point (or (:x position) (:x bbox)) @@ -840,8 +842,8 @@ (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state {:omit-blocked? true}) shapes (map #(get objects %) selected) - selrect (gsh/selection-rect shapes) - center (gsh/center-selrect selrect) + selrect (gsh/shapes->rect shapes) + center (grc/rect->center selrect) modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point -1.0 1.0) center))] (rx/of (dwm/apply-modifiers {:modifiers modifiers})))))) @@ -852,7 +854,7 @@ (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state {:omit-blocked? true}) shapes (map #(get objects %) selected) - selrect (gsh/selection-rect shapes) - center (gsh/center-selrect selrect) + selrect (gsh/shapes->rect shapes) + center (grc/rect->center selrect) modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point 1.0 -1.0) center))] (rx/of (dwm/apply-modifiers {:modifiers modifiers})))))) diff --git a/frontend/src/app/main/data/workspace/viewport.cljs b/frontend/src/app/main/data/workspace/viewport.cljs index 0629b33d93..959dbf48a2 100644 --- a/frontend/src/app/main/data/workspace/viewport.cljs +++ b/frontend/src/app/main/data/workspace/viewport.cljs @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.geom.align :as gal] [app.common.geom.point :as gpt] + [app.common.geom.rect :as gpr] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages.helpers :as cph] @@ -21,6 +22,10 @@ (defn initialize-viewport [{:keys [width height] :as size}] + (dm/assert! + "expected `size` to be a rect instance" + (gpr/rect? size)) + (letfn [(update* [{:keys [vport] :as local}] (let [wprop (/ (:width vport) width) hprop (/ (:height vport) height)] @@ -29,13 +34,14 @@ (update :vbox (fn [vbox] (-> vbox (update :width #(/ % wprop)) - (update :height #(/ % hprop)))))))) + (update :height #(/ % hprop)) + (gpr/update-rect :size))))))) (initialize [state local] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) shapes (cph/get-immediate-children objects) - srect (gsh/selection-rect shapes) + srect (gsh/shapes->rect shapes) local (assoc local :vport size :zoom 1 :zoom-inverse 1)] (cond (or (not (d/num? (:width srect))) @@ -49,12 +55,20 @@ (-> local (assoc :zoom zoom) (assoc :zoom-inverse (/ 1 zoom)) - (update :vbox merge srect))) + (update :vbox (fn [vbox] + (-> (merge vbox srect) + (gpr/make-rect)))))) :else - (assoc local :vbox (assoc size - :x (+ (:x srect) (/ (- (:width srect) width) 2)) - :y (+ (:y srect) (/ (- (:height srect) height) 2))))))) + (let [vx (+ (:x srect) + (/ (- (:width srect) width) 2)) + vy (+ (:y srect) + (/ (- (:height srect) height) 2)) + vbox (-> size + (assoc :x vx) + (assoc :y vy) + (gpr/update-rect :position))] + (assoc local :vbox vbox))))) (setup [state local] (if (and (:vbox local) (:vport local)) diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs index fce940e6f3..bb2a9c0c24 100644 --- a/frontend/src/app/main/data/workspace/zoom.cljs +++ b/frontend/src/app/main/data/workspace/zoom.cljs @@ -3,11 +3,13 @@ ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC + (ns app.main.data.workspace.zoom (:require [app.common.geom.align :as gal] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.main.data.workspace.state-helpers :as wsh] @@ -19,10 +21,10 @@ [{:keys [vbox] :as local} center zoom] (let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom) old-zoom (:zoom local) - center (if center center (gsh/center-rect vbox)) - scale (/ old-zoom new-zoom) - mtx (gmt/scale-matrix (gpt/point scale) center) - vbox' (gsh/transform-rect vbox mtx)] + center (if center center (grc/rect->center vbox)) + scale (/ old-zoom new-zoom) + mtx (gmt/scale-matrix (gpt/point scale) center) + vbox' (gsh/transform-rect vbox mtx)] (-> local (assoc :zoom new-zoom) (assoc :zoom-inverse (/ 1 new-zoom)) @@ -74,7 +76,7 @@ (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) shapes (cph/get-immediate-children objects) - srect (gsh/selection-rect shapes)] + srect (gsh/shapes->rect shapes)] (if (empty? shapes) state (update state :workspace-local @@ -97,7 +99,7 @@ objects (wsh/lookup-page-objects state page-id) srect (->> selected (map #(get objects %)) - (gsh/selection-rect))] + (gsh/shapes->rect))] (update state :workspace-local (fn [{:keys [vport] :as local}] (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 19334c81bc..1d93a1acef 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -16,6 +16,7 @@ [app.common.colors :as clr] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.bounds :as gsb] [app.common.math :as mth] @@ -62,15 +63,15 @@ (defn- calculate-dimensions [objects] - (let [bounds - (->> (ctst/get-root-objects objects) - (map (partial gsb/get-object-bounds objects)) - (gsh/join-rects))] + (let [bounds (->> (ctst/get-root-objects objects) + (map (partial gsb/get-object-bounds objects)) + (grc/join-rects))] (-> bounds (update :x mth/finite 0) (update :y mth/finite 0) (update :width mth/finite 100000) - (update :height mth/finite 100000)))) + (update :height mth/finite 100000) + (grc/update-rect :position)))) (declare shape-wrapper-factory) @@ -164,7 +165,7 @@ (defn adapt-root-frame [objects object] (let [shapes (cph/get-immediate-children objects) - srect (gsh/selection-rect shapes) + srect (gsh/shapes->rect shapes) object (merge object (select-keys srect [:x :y :width :height]))] (assoc object :fill-color "#f0f0f0"))) diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index 27480b0e80..5a01c7939f 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -7,15 +7,17 @@ (ns app.main.snap (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] + [app.common.geom.snap :as sp] [app.common.math :as mth] - [app.common.pages :as cp] + [app.common.pages.focus :as cpf] [app.common.pages.helpers :as cph] [app.common.uuid :refer [zero]] [app.main.refs :as refs] [app.main.worker :as uw] - [app.util.geom.snap-points :as sp] [app.util.range-tree :as rt] [beicon.core :as rx] [clojure.set :as set])) @@ -53,7 +55,7 @@ (or (contains? filter-shapes id) (not (contains? layout :dynamic-alignment)) (and (d/not-empty? focus) - (not (cp/is-in-focus? objects focus id))))))) + (not (cpf/is-in-focus? objects focus id))))))) (defn- calculate-distance [query-result point coord] (->> query-result @@ -226,15 +228,15 @@ [page-id shapes objects zoom movev] (let [frame-id (snap-frame-id shapes) frame (get objects frame-id) - selrect (->> shapes (map #(gsh/move % movev)) gsh/selection-rect)] + selrect (->> shapes (map #(gsh/move % movev)) gsh/shapes->rect)] (->> (rx/of (vector frame selrect)) (rx/merge-map (fn [[frame selrect]] - (let [vbox (gsh/rect->selrect @refs/vbox) + (let [vbox (deref refs/vbox) frame-id (->> shapes first :frame-id) selected (into #{} (map :id shapes)) - areas (->> (gsh/selrect->areas - (or (gsh/clip-selrect (:selrect frame) vbox) + areas (->> (gsh/get-areas + (or (grc/clip-rect (dm/get-prop frame :selrect) vbox) vbox) selrect) (d/mapm #(select-shapes-area page-id frame-id selected objects %2))) @@ -272,8 +274,8 @@ snap-points (->> shapes - (gsh/selection-rect) - (sp/selrect-snap-points) + (gsh/shapes->rect) + (sp/rect->snap-points) ;; Move the points in the translation vector (map #(gpt/add % movev)))] diff --git a/frontend/src/app/main/style.clj b/frontend/src/app/main/style.clj index 80cb4f7c23..4557e17118 100644 --- a/frontend/src/app/main/style.clj +++ b/frontend/src/app/main/style.clj @@ -7,25 +7,95 @@ (ns app.main.style "A fonts loading macros." (:require - [app.common.data :as d] - [clojure.data.json :as json])) + [clojure.core :as c] + [clojure.data.json :as json] + [clojure.java.io :as io] + [rumext.v2.util :as mfu])) + +(def ^:dynamic *css-data* nil) + +(def ^:private xform-css + (map (fn [k] + (let [cn (name k)] + (if (and (qualified-keyword? k) + (= "app.main.style" (namespace k))) + (or (get *css-data* (keyword cn)) cn) + cn))))) + +(defmacro css* + "Just coerces all params to strings and concats them with + space. Used mainly to set a set of classes together." + [& selectors] + (->> selectors + (map name) + (interpose " ") + (apply str))) (defmacro css - [selector] - (let [;; Get the associated styles will be module.cljs => module.css.json - filename (:file (meta *ns*)) - styles-file (str "./src/" (subs filename 0 (- (count filename) 4)) "css.json") - data (-> (slurp styles-file) - (json/read-str)) - result (get data (d/name selector))] - `~result)) + "Uses a css-modules defined data for real class lookup, then concat + all classes with space in the same way as `css*`." + [& selectors] + (let [fname (-> *ns* meta :file) + path (str (subs fname 0 (- (count fname) 4)) "css.json") + data (-> (slurp (io/resource path)) + (json/read-str :key-fn keyword))] + (if (symbol? (first selectors)) + `(if ~(with-meta (first selectors) {:tag 'boolean}) + (css* ~@(binding [*css-data* data] + (into [] xform-css (rest selectors)))) + (css* ~@(rest selectors))) + `(css* ~@(binding [*css-data* data] + (into [] xform-css selectors)))))) (defmacro styles [] - (let [;; Get the associated styles will be module.cljs => module.css.json - filename (:file (meta *ns*)) - styles-file (str "./src/" (subs filename 0 (- (count filename) 4)) "css.json") - data (-> (slurp styles-file) - (json/read-str)) - data (into {} (map (fn [[k v]] [(keyword k) v])) data)] - `~data)) \ No newline at end of file + ;; Get the associated styles will be module.cljs => module.css.json + (let [fname (-> *ns* meta :file) + path (str (subs fname 0 (- (count fname) 4)) "css.json")] + (-> (slurp (io/resource path)) + (json/read-str :key-fn keyword)))) + +(def ^:private xform-css-case + (comp + (partition-all 2) + (keep (fn [[k v]] + (let [cls (cond + (and (qualified-keyword? k) + (= "app.main.style" (namespace k))) + (let [cn (name k)] + (or (get *css-data* (keyword cn)) cn)) + + (simple-keyword? k) + (name k) + + (string? k) + k)] + (when cls + (cond + (true? v) cls + (false? v) nil + :else `(if ~v ~cls "")))))) + (interpose " "))) + +(defmacro css-case + [& params] + (let [fname (-> *ns* meta :file) + path (str (subs fname 0 (- (count fname) 4)) "css.json") + data (-> (slurp (io/resource path)) + (json/read-str :key-fn keyword))] + + (if (symbol? (first params)) + `(if ~(with-meta (first params) {:tag 'boolean}) + ~(binding [*css-data* data] + (-> (into [] xform-css-case (rest params)) + (mfu/compile-concat :safe? false))) + ~(-> (into [] xform-css-case (rest params)) + (mfu/compile-concat :safe? false))) + `~(binding [*css-data* data] + (-> (into [] xform-css-case params) + (mfu/compile-concat :safe? false)))))) + +(defmacro css-case* + [& params] + (-> (into [] xform-css-case params) + (mfu/compile-concat :safe? false))) diff --git a/frontend/src/app/main/ui/components/color_input.cljs b/frontend/src/app/main/ui/components/color_input.cljs index 2882ce8da4..be8555e686 100644 --- a/frontend/src/app/main/ui/components/color_input.cljs +++ b/frontend/src/app/main/ui/components/color_input.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.components.color-input (:require + [app.common.data :as d] [app.util.color :as uc] [app.util.dom :as dom] [app.util.globals :as globals] @@ -13,8 +14,7 @@ [app.util.keyboard :as kbd] [app.util.object :as obj] [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) (defn clean-color [value] @@ -31,7 +31,7 @@ on-change (obj/get props "onChange") on-blur (obj/get props "onBlur") on-focus (obj/get props "onFocus") - select-on-focus? (obj/get props "data-select-on-focus" true) + select-on-focus? (d/nilv (unchecked-get props "selectOnFocus") true) ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). @@ -128,8 +128,10 @@ ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect (.addEventListener target "mouseup" on-mouse-up #js {"once" true}))))) - props (-> props - (obj/without ["value" "onChange" "onFocus"]) + props (-> (obj/clone props) + (obj/unset! "selectOnFocus") + (obj/set! "value" mf/undefined) + (obj/set! "onChange" mf/undefined) (obj/set! "type" "text") (obj/set! "ref" ref) ;; (obj/set! "list" list-id) @@ -157,8 +159,8 @@ (mf/use-layout-effect (fn [] - (let [keys [(events/listen globals/window EventType.POINTERDOWN on-click) - (events/listen globals/window EventType.CLICK on-click)]] + (let [keys [(events/listen globals/window "pointerdown" on-click) + (events/listen globals/window "click" on-click)]] #(doseq [key keys] (events/unlistenByKey key))))) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index a7d3ed106f..e9acffbde4 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -7,153 +7,135 @@ (ns app.main.ui.components.numeric-input (:require [app.common.data :as d] - [app.common.spec :as us] + [app.common.schema :as sm] [app.main.ui.formats :as fmt] + [app.main.ui.hooks :as h] [app.util.dom :as dom] [app.util.globals :as globals] [app.util.keyboard :as kbd] [app.util.object :as obj] - [app.util.simple-math :as sm] + [app.util.simple-math :as smt] + [cljs.core :as c] [cuerdas.core :as str] [goog.events :as events] - [rumext.v2 :as mf]) - (:import goog.events.EventType)) + [rumext.v2 :as mf])) (mf/defc numeric-input {::mf/wrap-props false ::mf/forward-ref true} [props external-ref] - (let [value-str (obj/get props "value") - min-val-str (obj/get props "min") - max-val-str (obj/get props "max") - step-val-str (obj/get props "step") - wrap-value? (obj/get props "data-wrap") - on-change (obj/get props "onChange") - on-blur (obj/get props "onBlur") - on-focus (obj/get props "onFocus") - title (obj/get props "title") - default-val (obj/get props "default") - nillable (obj/get props "nillable") - select-on-focus? (obj/get props "data-select-on-focus" true) - class (obj/get props "klass") + (let [value-str (unchecked-get props "value") + min-value (unchecked-get props "min") + max-value (unchecked-get props "max") + step-value (unchecked-get props "step") + wrap-value? (unchecked-get props "data-wrap") + on-change (unchecked-get props "onChange") + on-blur (unchecked-get props "onBlur") + on-focus (unchecked-get props "onFocus") + + title (unchecked-get props "title") + default (unchecked-get props "default") + nillable? (unchecked-get props "nillable") + class (d/nilv (unchecked-get props "className") "input-text") + + min-value (d/parse-double min-value) + max-value (d/parse-double max-value) + step-value (d/parse-double step-value 1) + default (d/parse-double default 0) + + select-on-focus? (d/nilv (unchecked-get props "selectOnFocus") true) ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). ;; So we use the external ref if provided, and the local one if not. - local-ref (mf/use-ref) - ref (or external-ref local-ref) - - ;; We need to store the handle-blur ref so we can call it on unmount - handle-blur-ref (mf/use-ref nil) - dirty-ref (mf/use-ref false) + local-ref (mf/use-ref) + ref (or external-ref local-ref) ;; This `value` represents the previous value and is used as ;; initil value for the simple math expression evaluation. - value (d/parse-double value-str default-val) + value (d/parse-double value-str default) - min-val (cond - (number? min-val-str) - min-val-str - - (string? min-val-str) - (d/parse-double min-val-str)) - - max-val (cond - (number? max-val-str) - max-val-str - - (string? max-val-str) - (d/parse-double max-val-str)) - - step-val (cond - (number? step-val-str) - step-val-str - - (string? step-val-str) - (d/parse-double step-val-str) - - :else 1) + ;; We need to store the handle-blur ref so we can call it on unmount + dirty-ref (mf/use-ref false) parse-value - (mf/use-callback - (mf/deps ref min-val max-val value nillable default-val) + (mf/use-fn + (mf/deps min-value max-value value nillable? default) (fn [] - (let [input-node (mf/ref-val ref) - new-value (-> (dom/get-value input-node) - (str/strip-suffix ".") - (sm/expr-eval value))] - (cond - (d/num? new-value) - (-> new-value - (cljs.core/max (/ us/min-safe-int 2)) - (cljs.core/min (/ us/max-safe-int 2)) - (cond-> - (d/num? min-val) - (cljs.core/max min-val) + (when-let [node (mf/ref-val ref)] + (let [new-value (-> (dom/get-value node) + (str/strip-suffix ".") + (smt/expr-eval value))] + (cond + (d/num? new-value) + (-> new-value + (d/max (/ sm/min-safe-int 2)) + (d/min (/ sm/max-safe-int 2)) + (cond-> (d/num? min-value) + (d/max min-value)) + (cond-> (d/num? max-value) + (d/min max-value))) - (d/num? max-val) - (cljs.core/min max-val))) + nillable? + default - nillable - default-val - - :else value)))) + :else value))))) update-input - (mf/use-callback - (mf/deps ref) + (mf/use-fn (fn [new-value] - (let [input-node (mf/ref-val ref)] - (dom/set-value! input-node (fmt/format-number new-value))))) + (when-let [node (mf/ref-val ref)] + (dom/set-value! node (fmt/format-number new-value))))) apply-value - (mf/use-callback + (mf/use-fn (mf/deps on-change update-input value) - (fn [new-value event] + (fn [event new-value] (mf/set-ref-val! dirty-ref false) (when (and (not= new-value value) (fn? on-change)) + ;; FIXME: on-change very slow, makes the handler laggy (on-change new-value event)) (update-input new-value))) set-delta - (mf/use-callback - (mf/deps wrap-value? min-val max-val parse-value apply-value) + (mf/use-fn + (mf/deps wrap-value? min-value max-value parse-value apply-value) (fn [event up? down?] (let [current-value (parse-value)] (when current-value (let [increment (cond (kbd/shift? event) - (if up? (* step-val 10) (* step-val -10)) + (if up? (* step-value 10) (* step-value -10)) (kbd/alt? event) - (if up? (* step-val 0.1) (* step-val -0.1)) + (if up? (* step-value 0.1) (* step-value -0.1)) :else - (if up? step-val (- step-val))) + (if up? step-value (- step-value))) new-value (+ current-value increment) new-value (cond - (and wrap-value? (d/num? max-val min-val) - (> new-value max-val) up?) - (-> new-value (- max-val) (+ min-val) (- step-val)) + (and wrap-value? (d/num? max-value min-value) + (> new-value max-value) up?) + (-> new-value (- max-value) (+ min-value) (- step-value)) - (and wrap-value? (d/num? max-val min-val) - (< new-value min-val) down?) - (-> new-value (- min-val) (+ max-val) (+ step-val)) + (and wrap-value? (d/num? max-value min-value) + (< new-value min-value) down?) + (-> new-value (- min-value) (+ max-value) (+ step-value)) - (and (d/num? min-val) (< new-value min-val)) - min-val + (and (d/num? min-value) (< new-value min-value)) + min-value - (and (d/num? max-val) (> new-value max-val)) - max-val + (and (d/num? max-value) (> new-value max-value)) + max-value :else new-value)] - (apply-value new-value event)))))) + (apply-value event new-value)))))) handle-key-down - (mf/use-callback + (mf/use-fn (mf/deps set-delta apply-value update-input) (fn [event] (mf/set-ref-val! dirty-ref true) @@ -161,44 +143,47 @@ down? (kbd/down-arrow? event) enter? (kbd/enter? event) esc? (kbd/esc? event) - input-node (mf/ref-val ref)] + node (mf/ref-val ref)] (when (or up? down?) (set-delta event up? down?)) (when enter? - (dom/blur! input-node)) + (dom/blur! node)) (when esc? (update-input value-str) - (dom/blur! input-node))))) + (dom/blur! node))))) handle-mouse-wheel - (mf/use-callback + (mf/use-fn (mf/deps set-delta) (fn [event] - (let [input-node (mf/ref-val ref)] - (when (dom/active? input-node) - (let [event (.getBrowserEvent ^js event)] - (dom/prevent-default event) - (dom/stop-propagation event) - (set-delta event (< (.-deltaY event) 0) (> (.-deltaY event) 0))))))) + (when-let [node (mf/ref-val ref)] + (when (dom/active? node) + (dom/prevent-default event) + (dom/stop-propagation event) + (let [{:keys [y]} (dom/get-delta-position event)] + (set-delta event (< y 0) (> y 0))))))) handle-blur - (mf/use-callback + (mf/use-fn (mf/deps parse-value apply-value update-input on-blur) (fn [event] - (let [new-value (or (parse-value) default-val)] - (if (or nillable new-value) - (apply-value new-value event) + (let [new-value (or (parse-value) default)] + (if (or nillable? new-value) + (apply-value event new-value) (update-input new-value))) - (when on-blur (on-blur event)))) + (when (fn? on-blur) + (on-blur event)))) + + handle-unmount + (h/use-ref-callback handle-blur) on-click - (mf/use-callback + (mf/use-fn (fn [event] - (let [target (dom/get-target event)] - (when (some? ref) - (let [current (mf/ref-val ref)] - (when (and (some? current) (not (.contains current target))) - (dom/blur! current))))))) + (let [target (dom/get-target event) + node (mf/ref-val ref)] + (when (and (some? node) (not (dom/child? node target))) + (dom/blur! node))))) handle-focus (mf/use-callback @@ -212,9 +197,12 @@ ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))) - props (-> props - (obj/without ["value" "onChange" "nillable" "onFocus"]) - (obj/set! "className" (or class "input-text")) + props (-> (obj/clone props) + (obj/unset! "selectOnFocus") + (obj/unset! "nillable") + (obj/set! "value" mf/undefined) + (obj/set! "onChange" mf/undefined) + (obj/set! "className" class) (obj/set! "type" "text") (obj/set! "ref" ref) (obj/set! "defaultValue" (fmt/format-number value)) @@ -223,50 +211,23 @@ (obj/set! "onBlur" handle-blur) (obj/set! "onFocus" handle-focus))] - (mf/use-effect - (mf/deps value) - (fn [] - (when-let [input-node (mf/ref-val ref)] - (dom/set-value! input-node (fmt/format-number value))))) + (mf/with-effect [value] + (when-let [input-node (mf/ref-val ref)] + (dom/set-value! input-node (fmt/format-number value)))) - (mf/use-effect - (mf/deps handle-blur) - (fn [] - (mf/set-ref-val! handle-blur-ref {:fn handle-blur}))) + (mf/with-effect [] + (fn [] + (when (mf/ref-val dirty-ref) + (handle-unmount)))) - (mf/use-layout-effect - (fn [] - #(when (mf/ref-val dirty-ref) - (let [handle-blur (:fn (mf/ref-val handle-blur-ref))] - (handle-blur))))) + (mf/with-layout-effect [] + (let [keys [(events/listen globals/window "pointerdown" on-click) + (events/listen globals/window "click" on-click)]] + #(run! events/unlistenByKey keys))) - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - (mf/use-layout-effect - (fn [] - (let [keys [(events/listen globals/window EventType.POINTERDOWN on-click) - (events/listen globals/window EventType.CLICK on-click)]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) - - - (mf/use-layout-effect - (mf/deps handle-mouse-wheel) - (fn [] - (let [keys [(events/listen (mf/ref-val ref) EventType.WHEEL handle-mouse-wheel #js {:passive false})]] - #(doseq [key keys] - (events/unlistenByKey key))))) + (mf/with-layout-effect [handle-mouse-wheel] + (when-let [node (mf/ref-val ref)] + (let [key (events/listen node "wheel" handle-mouse-wheel #js {:passive false})] + #(events/unlistenByKey key)))) [:> :input props])) diff --git a/frontend/src/app/main/ui/components/search_bar.cljs b/frontend/src/app/main/ui/components/search_bar.cljs index 607a2f4604..8eb5631e28 100644 --- a/frontend/src/app/main/ui/components/search_bar.cljs +++ b/frontend/src/app/main/ui/components/search_bar.cljs @@ -15,13 +15,13 @@ (mf/defc search-bar {::mf/wrap-props false} [props] - (let [children (unchecked-get props "children") - on-change (unchecked-get props "on-change") - value (unchecked-get props "value") - on-clear (unchecked-get props "clear-action") + (let [children (unchecked-get props "children") + on-change (unchecked-get props "on-change") + value (unchecked-get props "value") + on-clear (unchecked-get props "clear-action") placeholder (unchecked-get props "placeholder") - icon (unchecked-get props "icon") - + icon (unchecked-get props "icon") + handle-change (mf/use-fn (mf/deps on-change) @@ -57,4 +57,4 @@ (when (not= "" value) [:button {:class (dom/classnames (css :clear) true) :on-click handle-clear} - i/delete-text-refactor])]])) \ No newline at end of file + i/delete-text-refactor])]])) diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs index 9b4ac2e75c..db052276aa 100644 --- a/frontend/src/app/main/ui/components/shape_icon.cljs +++ b/frontend/src/app/main/ui/components/shape_icon.cljs @@ -37,7 +37,7 @@ :path i/curve :rect i/box :text i/text - :group (if (:masked-group? shape) + :group (if (:masked-group shape) i/mask i/folder) :bool (case (:bool-type shape) diff --git a/frontend/src/app/main/ui/components/shape_icon_refactor.cljs b/frontend/src/app/main/ui/components/shape_icon_refactor.cljs index 7068849596..190117dd51 100644 --- a/frontend/src/app/main/ui/components/shape_icon_refactor.cljs +++ b/frontend/src/app/main/ui/components/shape_icon_refactor.cljs @@ -13,7 +13,8 @@ (mf/defc element-icon-refactor - [{:keys [shape main-instance?] :as props}] + {::mf/wrap-props false} + [{:keys [shape main-instance?]}] (if (ctk/instance-head? shape) (if main-instance? i/component-refactor @@ -29,7 +30,7 @@ ;; TODO: GRID ICON :else - i/board-refactor) + i/board-refactor) ;; TODO -> THUMBNAIL ICON :image i/img-refactor :line i/path-refactor @@ -37,7 +38,7 @@ :path i/path-refactor :rect i/rectangle-refactor :text i/text-refactor - :group (if (:masked-group? shape) + :group (if (:masked-group shape) i/mask-refactor i/group-refactor) :bool (case (:bool-type shape) @@ -60,4 +61,4 @@ :text i/text-refactor :mask i/mask-refactor :group i/group-refactor - nil))) \ No newline at end of file + nil))) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 8d08610405..a960f6f18a 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -7,7 +7,7 @@ (ns app.main.ui.hooks "A collection of general purpose react hooks." (:require - [app.common.pages :as cp] + [app.common.pages.focus :as cpf] [app.main.broadcast :as mbc] [app.main.data.shortcuts :as dsc] [app.main.refs :as refs] @@ -248,16 +248,15 @@ (mf/set-ref-val! ref val)) (mf/ref-val ref))) +;; FIXME: rename to use-focus-objects (defn with-focus-objects ([objects] (let [focus (mf/deref refs/workspace-focus-selected)] (with-focus-objects objects focus))) ([objects focus] - (let [objects (mf/use-memo - (mf/deps focus objects) - #(cp/focus-objects objects focus))] - objects))) + (mf/with-memo [focus objects] + (cpf/focus-objects objects focus)))) (defn use-debounce [ms value] diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 49342608c4..4248870941 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.flex-layout :as gsl] [app.common.geom.shapes.points :as gpo] @@ -248,8 +249,8 @@ (mf/defc measurement [{:keys [bounds frame selected-shapes hover-shape zoom]}] (let [selected-ids (into #{} (map :id) selected-shapes) - selected-selrect (gsh/selection-rect selected-shapes) - hover-selrect (-> hover-shape :points gsh/points->selrect) + selected-selrect (gsh/shapes->rect selected-shapes) + hover-selrect (-> hover-shape :points grc/points->rect) bounds-selrect (bound->selrect bounds) hover-selected-shape? (not (contains? selected-ids (:id hover-shape)))] @@ -262,7 +263,7 @@ (if (or (not hover-shape) (not hover-selected-shape?)) (when (and frame (not= uuid/zero (:id frame))) - (let [frame-bb (-> (:points frame) (gsh/points->selrect))] + (let [frame-bb (-> (:points frame) (grc/points->rect))] [:g.hover-shapes [:& selection-rect {:type :hover :selrect frame-bb :zoom zoom}] [:& distance-display {:from frame-bb diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index a7b1425de2..64867e28e6 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -8,8 +8,10 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.bounds :as gsb] + [app.common.geom.shapes.text :as gst] [app.common.pages.helpers :as cph] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] @@ -50,15 +52,16 @@ selrect (if (cph/text-shape? shape) - (gsh/position-data-selrect shape) - (gsh/points->selrect (:points shape))) + (gst/shape->rect shape) + (grc/points->rect (:points shape))) bounding-box (-> selrect (update :x - (+ stroke-width margin)) (update :y - (+ stroke-width margin)) (update :width + (* 2 (+ stroke-width margin))) - (update :height + (* 2 (+ stroke-width margin))))] + (update :height + (* 2 (+ stroke-width margin))) + (grc/update-rect :position))] [:mask {:id stroke-mask-id :x (:x bounding-box) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 9999772822..7554de8b06 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -70,9 +70,9 @@ image? (= :image (:type shape)) text? (= :text (:type shape)) path? (= :path (:type shape)) - mask? (and group? (:masked-group? shape)) + mask? (and group? (:masked-group shape)) bool? (= :bool (:type shape)) - center (gsh/center-shape shape)] + center (gsh/shape->center shape)] (-> props (add! :name) (add! :blocked) @@ -137,8 +137,8 @@ (add! :typography-ref-file) (add! :component-file) (add! :component-id) - (add! :component-root?) - (add! :main-instance?) + (add! :component-root) + (add! :main-instance) (add! :shape-ref)))) (defn prefix-keys [m] diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 8bf5aacce1..f819e06597 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.frame (:require [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.common.types.shape.layout :as ctl] @@ -94,7 +95,7 @@ [props] (let [shape (unchecked-get props "shape") bounds (or (unchecked-get props "bounds") - (gsh/points->selrect (:points shape))) + (grc/points->rect (:points shape))) shape-id (:id shape) thumb (:thumbnail shape) @@ -107,6 +108,8 @@ {:id (dm/str "thumbnail-" shape-id) :href thumb :decoding "async" + ;; FIXME: ensure bounds is always a rect instance and + ;; dm/get-prop for static attr access :x (:x bounds) :y (:y bounds) :width (:width bounds) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index 3f921cc3ae..11b92af6d9 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -62,7 +62,7 @@ angle (+ (gpt/angle gradient-vec) 90) - bb-shape (gsh/selection-rect [shape]) + bb-shape (gsh/shapes->rect [shape]) ;; Paths don't have a transform in SVG because we transform the points ;; we need to compensate the difference between the original rectangle diff --git a/frontend/src/app/main/ui/shapes/grid_layout_viewer.cljs b/frontend/src/app/main/ui/shapes/grid_layout_viewer.cljs index 337a8a6096..6f7373cda2 100644 --- a/frontend/src/app/main/ui/shapes/grid_layout_viewer.cljs +++ b/frontend/src/app/main/ui/shapes/grid_layout_viewer.cljs @@ -21,7 +21,7 @@ (let [cell-origin (unchecked-get props "origin") cell-width (unchecked-get props "width") - text (unchecked-get props "text") + text (unchecked-get props "text") area-width (* 10 (count text)) area-height 25 @@ -54,12 +54,12 @@ (let [shape (unchecked-get props "shape") cell (unchecked-get props "cell") layout-data (unchecked-get props "layout-data") - + cell-bounds (gsg/cell-bounds layout-data cell) cell-origin (gpo/origin cell-bounds) cell-width (gpo/width-points cell-bounds) cell-height (gpo/height-points cell-bounds) - cell-center (gsh/center-points cell-bounds) + cell-center (gsh/points->center cell-bounds) cell-origin (gpt/transform cell-origin (gmt/transform-in cell-center (:transform-inverse shape)))] [:g.cell @@ -91,7 +91,7 @@ layout-data (gsg/calc-layout-data shape children (:points shape))] - [:g.cells + [:g.cells (for [cell (ctl/get-cells shape {:sort? true})] [:& grid-cell {:key (dm/str "cell-" (:id cell)) :shape shape diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index b4cab7f1a6..2be8a46ab1 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -22,7 +22,7 @@ childs (unchecked-get props "childs") objects (unchecked-get props "objects") render-id (mf/use-ctx muc/render-id) - masked-group? (:masked-group? shape) + masked-group? (:masked-group shape) [mask childs] (if masked-group? [(first childs) (rest childs)] diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index f74e3a6640..14c0dd22ef 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] + [app.common.geom.rect :as grc] [app.main.ui.context :as muc] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -52,7 +52,7 @@ svg-text? (and (= :text (:type mask)) (some? (:position-data mask))) mask-bb (:points mask) - mask-bb-rect (gsh/points->rect mask-bb)] + mask-bb-rect (grc/points->rect mask-bb)] [:defs [:filter {:id (filter-id render-id mask)} [:feFlood {:flood-color "white" diff --git a/frontend/src/app/main/ui/shapes/svg_defs.cljs b/frontend/src/app/main/ui/shapes/svg_defs.cljs index 1be7aade58..69ddbf4005 100644 --- a/frontend/src/app/main/ui/shapes/svg_defs.cljs +++ b/frontend/src/app/main/ui/shapes/svg_defs.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.bounds :as gsb] [app.util.svg :as usvg] @@ -92,7 +93,7 @@ (defn svg-def-bounds [svg-def shape transform] (let [{:keys [tag]} svg-def] (if (or (= tag :mask) (contains? usvg/filter-tags tag)) - (-> (gsh/make-rect (d/parse-double (get-in svg-def [:attrs :x])) + (-> (grc/make-rect (d/parse-double (get-in svg-def [:attrs :x])) (d/parse-double (get-in svg-def [:attrs :y])) (d/parse-double (get-in svg-def [:attrs :width])) (d/parse-double (get-in svg-def [:attrs :height]))) diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index 116b5dcce9..242da7cba1 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -8,6 +8,7 @@ (:require [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.main.data.comments :as dcm] [app.main.data.events :as ev] @@ -103,7 +104,7 @@ threads-map (mf/deref refs/comment-threads) frame-corner (mf/with-memo [frame] - (-> frame :points gsh/points->selrect gpt/point)) + (-> frame :points grc/points->rect gpt/point)) modifier1 (mf/with-memo [frame-corner] (-> (gmt/matrix) diff --git a/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs b/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs index 0172a429a4..51f3e3eb58 100644 --- a/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs @@ -64,7 +64,7 @@ [:li {:ref item-ref :class (dom/classnames :component (not (nil? (:component-id item))) - :masked (:masked-group? item) + :masked (:masked-group item) :selected selected?)} [:div.element-list-body {:class (dom/classnames :selected selected? diff --git a/frontend/src/app/main/ui/viewer/inspect/selection_feedback.cljs b/frontend/src/app/main/ui/viewer/inspect/selection_feedback.cljs index f62ed17d57..b53aba3ca1 100644 --- a/frontend/src/app/main/ui/viewer/inspect/selection_feedback.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/selection_feedback.cljs @@ -58,7 +58,7 @@ shapes (resolve-shapes objects [hover]) hover-shape (or (first shapes) frame) selected-shapes (resolve-shapes objects selected) - selrect (gsh/selection-rect selected-shapes)] + selrect (gsh/shapes->rect selected-shapes)] (when (d/not-empty? selected-shapes) [:g.selection-feedback {:pointer-events "none"} diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 0563116ecd..4dc8c8394c 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -80,7 +80,7 @@ [{:keys [selected? frame on-click index objects page-id thumbnail-data]}] (let [children-ids (cph/get-children-ids objects (:id frame)) - children-bounds (gsh/selection-rect (concat [frame] (->> children-ids (keep (d/getf objects)))))] + children-bounds (gsh/shapes->rect (concat [frame] (->> children-ids (keep (d/getf objects)))))] [:div.thumbnail-item {:on-click #(on-click % index)} [:div.thumbnail-preview {:class (dom/classnames :selected selected?)} diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 59c5d7ed66..f420dfb60d 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -255,7 +255,7 @@ has-frame? (->> shapes (d/seek cph/frame-shape?)) has-group? (->> shapes (d/seek cph/group-shape?)) has-bool? (->> shapes (d/seek cph/bool-shape?)) - has-mask? (->> shapes (d/seek :masked-group?)) + has-mask? (->> shapes (d/seek :masked-group)) is-group? (and single? has-group?) is-bool? (and single? has-bool?) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index 26878c5370..c856ebcc95 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.common.types.modifiers :as ctm] @@ -79,7 +80,7 @@ [node modifiers] (let [{:keys [x y width height]} - (-> (gsh/make-selrect + (-> (grc/make-rect (-> (dom/get-attribute node "data-old-x") d/parse-double) (-> (dom/get-attribute node "data-old-y") d/parse-double) (-> (dom/get-attribute node "data-old-width") d/parse-double) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index 2b14872f48..348497b65d 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.config :as cf] @@ -41,9 +42,9 @@ [rect node style-node] (let [{:keys [x y width height]} rect viewbox (dm/str x " " y " " width " " height) - + ;; Calculate the fixed width and height - ;; We don't want to generate thumbnails + ;; We don't want to generate thumbnails ;; bigger than 2000px [fixed-width fixed-height] (if (> width height) @@ -81,16 +82,22 @@ (refs/all-children-objects id)) all-children (mf/deref all-children-ref) - {:keys [x y width height] :as shape-bb} + ;; FIXME: performance rect + bounds (if (:show-content shape) - (gsh/selection-rect (concat [shape] all-children)) - (-> shape :points gsh/points->selrect)) + (gsh/shapes->rect (cons shape all-children)) + (-> shape :points grc/points->rect)) + + x (dm/get-prop bounds :x) + y (dm/get-prop bounds :y) + width (dm/get-prop bounds :width) + height (dm/get-prop bounds :height) svg-uri* (mf/use-state nil) bitmap-uri* (mf/use-state nil) observer* (mf/use-var nil) - shape-bb* (hooks/use-update-var shape-bb) + bounds* (hooks/use-update-var bounds) updates-s (mf/use-memo rx/subject) thumbnail-uri-ref (mf/with-memo [page-id id] @@ -134,7 +141,7 @@ (if (dom/has-children? node) ;; The frame-content need to have children in order to generate the thumbnail (let [style-node (dom/query (dm/str "#frame-container-" id " style")) - url (create-svg-blob-uri-from @shape-bb* node style-node)] + url (create-svg-blob-uri-from @bounds* node style-node)] (reset! svg-uri* url)) ;; Node not yet ready, we schedule a new generation @@ -228,7 +235,7 @@ [on-load-frame-dom @render-frame* (mf/html - [:& frame/frame-container {:bounds shape-bb :shape shape} + [:& frame/frame-container {:bounds bounds :shape shape} ;; Safari don't support filters so instead we add a rectangle around the thumbnail (when (and (cf/check-browser? :safari) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index d1d34b6c0c..511912a190 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -9,7 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.text :as gsht] + [app.common.geom.shapes.text :as gst] [app.common.math :as mth] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] @@ -24,7 +24,7 @@ [props] (let [shape (unchecked-get props "shape") zoom (mf/deref refs/selected-zoom) - bounding-box (gsht/position-data-selrect shape) + bounding-box (gst/shape->rect shape) ctx (js* "document.createElement(\"canvas\").getContext(\"2d\")")] [:g {:transform (gsh/transform-str shape)} [:rect {:x (:x bounding-box) @@ -66,22 +66,24 @@ ;; --- Text Wrapper for workspace (mf/defc text-wrapper {::mf/wrap-props false} - [props] - (let [shape (unchecked-get props "shape") + [{:keys [shape]}] + (let [shape-id (dm/get-prop shape :id) text-modifier-ref - (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + (mf/with-memo [shape-id] + (refs/workspace-text-modifier-by-id shape-id)) text-modifier (mf/deref text-modifier-ref) - shape (cond-> shape - (some? text-modifier) - (dwt/apply-text-modifier text-modifier))] + shape (if (some? shape) + (dwt/apply-text-modifier shape text-modifier) + shape)] [:> shape-container {:shape shape} - [:g.text-shape {:key (dm/str "text-" (:id shape))} + [:g.text-shape {:key (dm/str shape-id)} [:& text/text-shape {:shape shape}]] - (when (and (debug? :text-outline) (d/not-empty? (:position-data shape))) + (when (and ^boolean (debug? :text-outline) + ^boolean (seq (:position-data shape))) [:& debug-text-bounds {:shape shape}])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 64805ef19e..df8174edcc 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -10,7 +10,8 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.text :as gsht] + [app.common.geom.shapes.text :as gst] + [app.common.math :as mth] [app.common.text :as txt] [app.config :as cf] [app.main.data.workspace :as dw] @@ -24,9 +25,7 @@ [app.util.object :as obj] [app.util.text-editor :as ted] [goog.events :as events] - [rumext.v2 :as mf]) - (:import - goog.events.EventType)) + [rumext.v2 :as mf])) ;; --- Text Editor Rendering @@ -119,7 +118,7 @@ on-mount (fn [] - (let [keys [(events/listen js/document EventType.KEYUP on-key-up)]] + (let [keys [(events/listen js/document "keyup" on-key-up)]] (st/emit! (dwt/initialize-editor-state shape default-decorator) (dwt/select-all shape)) #(do @@ -260,22 +259,25 @@ (mf/defc text-editor-svg {::mf/wrap-props false} - [props] - (let [shape (obj/get props "shape") - modifiers (obj/get props "modifiers") - modifiers (get-in modifiers [(:id shape) :modifiers]) + [{:keys [shape modifiers]}] + (let [shape-id (dm/get-prop shape :id) + modifiers (dm/get-in modifiers [shape-id :modifiers]) - clip-id - (dm/str "text-edition-clip" (:id shape)) + clip-id (dm/str "text-edition-clip" shape-id) text-modifier-ref - (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + (mf/with-memo [shape-id] + (refs/workspace-text-modifier-by-id shape-id)) text-modifier (mf/deref text-modifier-ref) - ;; For Safari It's necesary to scale the editor with the zoom level to fix - ;; a problem with foreignObjects not scaling correctly with the viewbox + ;; For Safari It's necesary to scale the editor with the zoom + ;; level to fix a problem with foreignObjects not scaling + ;; correctly with the viewbox + ;; + ;; NOTE: this teoretically breaks hooks rules, but in practice + ;; it is imposible to really break it maybe-zoom (when (cf/check-browser? :safari) (mf/deref refs/selected-zoom)) @@ -287,12 +289,16 @@ (some? modifiers) (gsh/transform-shape modifiers)) - bounding-box (gsht/position-data-selrect shape) + bounds (gst/shape->rect shape) - x (min (:x bounding-box) (:x shape)) - y (min (:y bounding-box) (:y shape)) - width (max (:width bounding-box) (:width shape)) - height (max (:height bounding-box) (:height shape))] + x (mth/min (dm/get-prop bounds :x) + (dm/get-prop shape :x)) + y (mth/min (dm/get-prop bounds :y) + (dm/get-prop shape :y)) + width (mth/max (dm/get-prop bounds :width) + (dm/get-prop shape :width)) + height (mth/max (dm/get-prop bounds :height) + (dm/get-prop shape :height))] [:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id) :transform (dm/str (gsh/transform-matrix shape))} @@ -307,8 +313,11 @@ [:foreignObject {:x x :y y :width width :height height} [:div {:style {:position "fixed" :left 0 - :top (- (:y shape) y) + :top (- (dm/get-prop shape :y) y) :pointer-events "all" :transform-origin "top left" - :transform (when maybe-zoom (dm/fmt "scale(%)" maybe-zoom))}} - [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]])) + :transform (when (some? maybe-zoom) + (dm/fmt "scale(%)" maybe-zoom))}} + [:& text-shape-edit-html + {:shape shape + :key (dm/str shape-id)}]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index 9850953373..aea655edcf 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -5,9 +5,10 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.layer-item - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] @@ -28,33 +29,38 @@ [okulary.core :as l] [rumext.v2 :as mf])) - (mf/defc layer-item - [{:keys [index item selected objects sortable? filtered? recieved-depth parent-size component-child?] :as props}] - (let [id (:id item) - blocked? (:blocked item) - hidden? (:hidden item) + {::mf/wrap-props false} + [{:keys [index item selected objects sortable? filtered? depth parent-size component-child?]}] + (let [id (:id item) + name (:name item) + blocked? (:blocked item) + hidden? (:hidden item) + touched? (-> item :touched seq boolean) + has-shapes? (-> item :shapes seq boolean) - disable-drag (mf/use-state false) - scroll-to-middle? (mf/use-var true) - expanded-iref (mf/with-memo [id] - (-> (l/in [:expanded id]) - (l/derived refs/workspace-local))) + drag-disabled* (mf/use-state false) + drag-disabled? (deref drag-disabled*) - expanded? (mf/deref expanded-iref) - selected? (contains? selected id) - container? (or (cph/frame-shape? item) - (cph/group-shape? item)) - absolute? (ctl/layout-absolute? item) + scroll-to-middle? (mf/use-var true) + expanded-iref (mf/with-memo [id] + (-> (l/in [:expanded id]) + (l/derived refs/workspace-local))) + expanded? (mf/deref expanded-iref) - components-v2 (mf/use-ctx ctx/components-v2) - workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) - new-css-system (mf/use-ctx ctx/new-css-system) - main-instance? (if components-v2 - (:main-instance? item) - true) - parent-board? (and (= :frame (:type item)) - (= uuid/zero (:parent-id item))) + selected? (contains? selected id) + container? (or (cph/frame-shape? item) + (cph/group-shape? item)) + absolute? (ctl/layout-absolute? item) + + components-v2 (mf/use-ctx ctx/components-v2) + read-only? (mf/use-ctx ctx/workspace-read-only?) + new-css-system (mf/use-ctx ctx/new-css-system) + main-instance? (if components-v2 + (:main-instance item) + true) + parent-board? (and (cph/frame-shape? item) + (= uuid/zero (:parent-id item))) toggle-collapse (mf/use-fn (mf/deps expanded?) @@ -107,22 +113,22 @@ on-pointer-enter (mf/use-fn (mf/deps id) - (fn [_event] + (fn [_] (st/emit! (dw/highlight-shape id)))) on-pointer-leave (mf/use-fn (mf/deps id) - (fn [_event] + (fn [_] (st/emit! (dw/dehighlight-shape id)))) on-context-menu (mf/use-fn - (mf/deps item workspace-read-only?) + (mf/deps item read-only?) (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (when-not workspace-read-only? + (when-not read-only? (let [pos (dom/get-client-position event)] (st/emit! (dw/show-shape-context-menu {:position pos :shape item})))))) @@ -150,27 +156,37 @@ (when-not expanded? (st/emit! (dwc/toggle-collapse id))))) + zoom-to-selected + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! dw/zoom-to-selected-shape))) + [dprops dref] (hooks/use-sortable :data-type "penpot/layer" :on-drop on-drop :on-drag on-drag :on-hold on-hold - :disabled @disable-drag + :disabled drag-disabled? :detect-center? container? :data {:id (:id item) :index index :name (:name item)} - :draggable? (and sortable? (not workspace-read-only?))) + :draggable? (and sortable? (not read-only?))) - ref (mf/use-ref) - depth (+ recieved-depth 1) - component-tree? (or component-child? (:component-root? item))] + ref (mf/use-ref) + depth (+ depth 1) + component-tree? (or component-child? (:component-root item)) + + enable-drag (mf/use-fn #(reset! drag-disabled* true)) + disable-drag (mf/use-fn #(reset! drag-disabled* false))] (mf/with-effect [selected? selected] (let [single? (= (count selected) 1) - node (mf/ref-val ref) - parent-node (dom/get-parent (dom/get-parent node)) + node (mf/ref-val ref) + parent (dom/get-parent (dom/get-parent node)) subid (when (and single? selected?) @@ -178,101 +194,112 @@ (ts/schedule 100 #(if scroll-to - (dom/scroll-into-view! parent-node #js {:block "center" :behavior "smooth" :inline "start"}) + (dom/scroll-into-view! parent {:block "center" :behavior "smooth" :inline "start"}) (do - (dom/scroll-into-view-if-needed! parent-node #js {:block "center" :behavior "smooth" :inline "start"}) + (dom/scroll-into-view-if-needed! parent {:block "center" :behavior "smooth" :inline "start"}) (reset! scroll-to-middle? true))))))] #(when (some? subid) (rx/dispose! subid)))) - + (if new-css-system [:* [:div {:on-context-menu on-context-menu :ref dref :on-click select-shape :id id - :class (dom/classnames - (css :layer-row) true - (css :component) (not (nil? (:component-id item))) - (css :masked) (:masked-group? item) - (css :selected) selected? - (css :type-frame) (= :frame (:type item)) - (css :type-bool) (= :bool (:type item)) - (css :type-comp) component-tree? - (css :hidden) (:hidden item) + :class (stl/css-case + :layer-row true + :component (some? (:component-id item)) + :masked (:masked-group item) + :selected selected? + :type-frame (cph/frame-shape? item) + :type-bool (cph/bool-shape? item) + :type-comp component-tree? + :hidden hidden? :dnd-over (= (:over dprops) :center) :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot) :root-board parent-board?)} - [:span {:class (dom/classnames (css :tab-indentation) true - (css :filtered) filtered?) - :style #js {"--depth" depth}}] - [:div {:class (dom/classnames (css :element-list-body) true - (css :filtered) filtered? - (css :selected) selected? - (css :icon-layer) (= (:type item) :icon)) - :style #js {"--depth" depth} + [:span {:class (stl/css-case + :tab-indentation true + :filtered filtered?) + :style {"--depth" depth}}] + [:div {:class (stl/css-case + :element-list-body true + :filtered filtered? + :selected selected? + :icon-layer (= (:type item) :icon)) + :style {"--depth" depth} :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave - :on-double-click #(dom/stop-propagation %)} + :on-double-click dom/stop-propagation} (if (< 0 (count (:shapes item))) - [:div {:class (dom/classnames (css :button-content) true)} + [:div {:class (stl/css :button-content)} (when (not filtered?) - [:button {:class (dom/classnames (css :toggle-content) true - (css :inverse) expanded?) + [:button {:class (stl/css-case + :toggle-content true + :inverse expanded?) :on-click toggle-collapse} i/arrow-refactor]) - [:div {:class (dom/classnames (css :icon-shape) true) - :on-double-click #(do (dom/stop-propagation %) - (dom/prevent-default %) - (st/emit! dw/zoom-to-selected-shape))} + [:div {:class (stl/css :icon-shape) + :on-double-click zoom-to-selected} (when absolute? - [:div {:class (dom/classnames (css :absolute) true)} ]) - [:& sic/element-icon-refactor {:shape item - :main-instance? main-instance?}]]] - [:div {:class (dom/classnames (css :button-content) true)} - (when (not filtered?) - [:span {:class (dom/classnames (css :toggle-content) true)}]) - [:div {:class (dom/classnames (css :icon-shape) true) - :on-double-click #(do (dom/stop-propagation %) - (dom/prevent-default %) - (st/emit! dw/zoom-to-selected-shape))} - (when absolute? - [:div {:class (dom/classnames (css :absolute) true)} ]) - [:& sic/element-icon-refactor {:shape item - :main-instance? main-instance?}]]]) + [:div {:class (stl/css :absolute)}]) - [:& layer-name {:shape item - :name-ref ref - :disabled-double-click workspace-read-only? - :on-start-edit #(reset! disable-drag true) - :on-stop-edit #(reset! disable-drag false) + [:& sic/element-icon-refactor + {:shape item + :main-instance? main-instance?}]]] + + [:div {:class (stl/css :button-content)} + (when (not ^boolean filtered?) + [:span {:class (stl/css :toggle-content)}]) + [:div {:class (stl/css :icon-shape) + :on-double-click zoom-to-selected} + (when ^boolean absolute? + [:div {:class (stl/css :absolute)}]) + [:& sic/element-icon-refactor + {:shape item + :main-instance? main-instance?}]]]) + + [:& layer-name {:ref ref + :shape-id id + :shape-name name + :shape-touched? touched? + :disabled-double-click read-only? + :on-start-edit disable-drag + :on-stop-edit enable-drag :depth depth :parent-size parent-size :selected? selected? :type-comp component-tree? - :type-frame (= :frame (:type item)) - :hidden (:hidden item)}] - [:div {:class (dom/classnames (css :element-actions) true - (css :is-parent) (:shapes item) - (css :selected) (:hidden item) - (css :selected) (:blocked item))} - [:button {:class (dom/classnames (css :toggle-element) true - (css :selected) (:hidden item)) + :type-frame (cph/frame-shape? item) + :hidden? hidden?}] + [:div {:class (stl/css-case + :element-actions true + :is-parent has-shapes? + :selected hidden? + :selected blocked?)} + [:button {:class (stl/css-case + :toggle-element true + :selected hidden?) :on-click toggle-visibility} - (if (:hidden item) i/hide-refactor i/shown-refactor)] - [:button {:class (dom/classnames (css :block-element) true - (css :selected) (:blocked item)) + (if ^boolean hidden? i/hide-refactor i/shown-refactor)] + [:button {:class (stl/css-case + :block-element true + :selected blocked?) :on-click toggle-blocking} - (if (:blocked item) i/lock-refactor i/unlock-refactor)]]]] + (if ^boolean blocked? i/lock-refactor i/unlock-refactor)]]]] + (when (and (:shapes item) expanded?) - [:div {:class (dom/classnames (css :element-children) true - (css :parent-selected) selected? - :sticky-children parent-board?) - :data-id (when parent-board? (:id item))} + [:div {:class (stl/css-case + :element-children true + :parent-selected selected? + :sticky-children parent-board?) + :data-id (when ^boolean parent-board? id)} + (for [[index id] (reverse (d/enumerate (:shapes item)))] (when-let [item (get objects id)] [:& layer-item @@ -280,60 +307,68 @@ :selected selected :index index :objects objects - :key (:id item) + :key (dm/str id) :sortable? sortable? - :recieved-depth depth + :depth depth :parent-size parent-size :component-child? component-tree?}]))])] + + ;; ---- OLD CSS [:li {:on-context-menu on-context-menu :ref dref - :class (dom/classnames - :component (not (nil? (:component-id item))) - :masked (:masked-group? item) - :dnd-over (= (:over dprops) :center) + :class (stl/css-case* + :component (some? (:component-id item)) + :masked (:masked-group item) + :dnd-over (= (:over dprops) :center) :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot) - :selected selected? - :type-frame (= :frame (:type item)))} + :selected selected? + :type-frame (cph/frame-shape? item))} - [:div.element-list-body {:class (dom/classnames :selected selected? - :icon-layer (= (:type item) :icon)) + [:div.element-list-body {:class (stl/css-case* + :selected selected? + :icon-layer (= (:type item) :icon)) :on-click select-shape :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave - :on-double-click #(dom/stop-propagation %)} + :on-double-click dom/stop-propagation} - [:div.icon {:on-double-click #(do (dom/stop-propagation %) - (dom/prevent-default %) - (st/emit! dw/zoom-to-selected-shape))} - (when absolute? + [:div.icon {:on-double-click zoom-to-selected} + (when ^boolean absolute? [:div.absolute i/position-absolute]) - [:& si/element-icon {:shape item - :main-instance? main-instance?}]] - [:& layer-name {:shape item - :name-ref ref - :disabled-double-click workspace-read-only? - :on-start-edit #(reset! disable-drag true) - :on-stop-edit #(reset! disable-drag false) + [:& si/element-icon + {:shape item + :main-instance? main-instance?}]] + [:& layer-name {:ref ref + :parent-size parent-size + :shape-id id + :shape-name name + :shape-touched? touched? + :on-start-edit disable-drag + :on-stop-edit enable-drag + :disabled-double-click read-only? :selected? selected? :type-comp component-tree? - :type-frame (= :frame (:type item)) - :hidden (:hidden item)}] + :type-frame (cph/frame-shape? item) + :hidden? hidden?}] - [:div.element-actions {:class (when (:shapes item) "is-parent")} - [:div.toggle-element {:class (when (:hidden item) "selected") + [:div.element-actions {:class (when ^boolean has-shapes? "is-parent")} + [:div.toggle-element {:class (when ^boolean hidden? "selected") :on-click toggle-visibility} - (if (:hidden item) i/eye-closed i/eye)] - [:div.block-element {:class (when (:blocked item) "selected") + (if ^boolean hidden? i/eye-closed i/eye)] + [:div.block-element {:class (when ^boolean blocked? "selected") :on-click toggle-blocking} - (if (:blocked item) i/lock i/unlock)]] + (if ^boolean blocked? i/lock i/unlock)]] - (when (:shapes item) - (when (not filtered?) [:span.toggle-content - {:on-click toggle-collapse - :class (when expanded? "inverse")} - i/arrow-slide]))] - (when (and (:shapes item) expanded?) + (when ^boolean has-shapes? + (when (not ^boolean filtered?) + [:span.toggle-content + {:on-click toggle-collapse + :class (when ^boolean expanded? "inverse")} + i/arrow-slide]))] + + (when (and ^boolean has-shapes? + ^boolean expanded?) [:ul.element-children (for [[index id] (reverse (d/enumerate (:shapes item)))] (when-let [item (get objects id)] @@ -342,5 +377,5 @@ :selected selected :index index :objects objects - :key (:id item) - :sortable? sortable?}]))])]))) \ No newline at end of file + :key (dm/str id) + :sortable? sortable?}]))])]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs index b7d47067d4..77e7590510 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs @@ -5,8 +5,10 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.layer-name - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.data.workspace :as dw] [app.main.store :as st] [app.main.ui.context :as ctx] @@ -16,73 +18,94 @@ [okulary.core :as l] [rumext.v2 :as mf])) -(def shape-for-rename-ref - (l/derived (l/in [:workspace-local :shape-for-rename]) st/state)) +(def ^:private space-for-icons 110) + +(def lens:shape-for-rename + (-> (l/in [:workspace-local :shape-for-rename]) + (l/derived st/state))) (mf/defc layer-name - [{:keys [shape on-start-edit disabled-double-click on-stop-edit name-ref depth parent-size selected? type-comp type-frame hidden] :as props}] - (let [local (mf/use-state {}) - shape-for-rename (mf/deref shape-for-rename-ref) + {::mf/wrap-props false + ::mf/forward-ref true} + [{:keys [shape-id shape-name shape-touched? disabled-double-click + on-start-edit on-stop-edit depth parent-size selected? + type-comp type-frame hidden?]} external-ref] + (let [edition* (mf/use-state false) + edition? (deref edition*) + + local-ref (mf/use-ref) + ref (d/nilv external-ref local-ref) + + shape-for-rename (mf/deref lens:shape-for-rename) new-css-system (mf/use-ctx ctx/new-css-system) - start-edit (fn [] - (when (not disabled-double-click) - (on-start-edit) - (swap! local assoc :edition true) - (st/emit! (dw/start-rename-shape (:id shape))))) + start-edit + (mf/use-fn + (mf/deps disabled-double-click on-start-edit shape-id) + (fn [] + (when (not disabled-double-click) + (on-start-edit) + (reset! edition* true) + (st/emit! (dw/start-rename-shape shape-id))))) - accept-edit (fn [] - (let [name-input (mf/ref-val name-ref) - name (str/trim (dom/get-value name-input))] - (on-stop-edit) - (swap! local assoc :edition false) - (st/emit! (dw/end-rename-shape name)))) + accept-edit + (mf/use-fn + (mf/deps on-stop-edit) + (fn [] + (let [name-input (mf/ref-val ref) + name (str/trim (dom/get-value name-input))] + (on-stop-edit) + (reset! edition* false) + (st/emit! (dw/end-rename-shape name))))) - cancel-edit (fn [] - (on-stop-edit) - (swap! local assoc :edition false) - (st/emit! (dw/end-rename-shape nil))) + cancel-edit + (mf/use-fn + (mf/deps on-stop-edit) + (fn [] + (on-stop-edit) + (reset! edition* false) + (st/emit! (dw/end-rename-shape nil)))) - on-key-down (fn [event] - (when (kbd/enter? event) (accept-edit)) - (when (kbd/esc? event) (cancel-edit))) + on-key-down + (mf/use-fn + (mf/deps accept-edit cancel-edit) + (fn [event] + (when (kbd/enter? event) (accept-edit)) + (when (kbd/esc? event) (cancel-edit)))) - space-for-icons 110 - parent-size (str (- parent-size space-for-icons) "px")] + parent-size (dm/str (- parent-size space-for-icons) "px")] - (mf/with-effect [shape-for-rename] - (when (and (= shape-for-rename (:id shape)) - (not (:edition @local))) + (mf/with-effect [shape-for-rename edition? start-edit shape-id] + (when (and (= shape-for-rename shape-id) + (not ^boolean edition?)) (start-edit))) - (mf/with-effect [(:edition @local)] - (when (:edition @local) - (let [name-input (mf/ref-val name-ref)] - (dom/select-text! name-input) - nil))) + (mf/with-effect [edition?] + (when edition? + (some-> (mf/ref-val ref) dom/select-text!) + nil)) - (if (:edition @local) + (if ^boolean edition? [:input - {:class (if new-css-system - (dom/classnames (css :element-name-input) true) - (dom/classnames :element-name true)) - :style #js {"--depth" depth "--parent-size" parent-size} + {:class (stl/css new-css-system ::stl/element-name ::stl/element-name-input) + :style {"--depth" depth "--parent-size" parent-size} :type "text" - :ref name-ref + :ref ref :on-blur accept-edit :on-key-down on-key-down :auto-focus true - :default-value (:name shape "")}] + :default-value (d/nilv shape-name "")}] [:span - {:class (if new-css-system - (dom/classnames (css :element-name) true - (css :selected) selected? - (css :hidden) hidden - (css :type-comp) type-comp - (css :type-frame) type-frame) - (dom/classnames :element-name true)) - :style #js {"--depth" depth "--parent-size" parent-size} - :ref name-ref + {:class (if ^boolean new-css-system + (stl/css-case + ::stl/element-name true + ::stl/selected selected? + ::stl/hidden hidden? + ::stl/type-comp type-comp + ::stl/type-frame type-frame) + (stl/css* :element-name)) + :style {"--depth" depth "--parent-size" parent-size} + :ref ref :on-double-click start-edit} - (:name shape "") - (when (seq (:touched shape)) " *")]))) + (d/nilv shape-name "") + (when ^boolean shape-touched? " *")]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 99f24fe23b..311ea0c9c5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -5,10 +5,11 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.layers - (:require-macros [app.main.style :refer [css]]) + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.refs :as refs] @@ -27,33 +28,29 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) - ;; This components is a piece for sharding equality check between top ;; level frames and try to avoid rerender frames that are does not ;; affected by the selected set. (mf/defc frame-wrapper {::mf/wrap-props false - ::mf/wrap [mf/memo - #(mf/deferred % ts/idle-then-raf)]} + ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} [props] [:> layer-item props]) (mf/defc layers-tree - {::mf/wrap [#(mf/memo % =) - #(mf/throttle % 200)]} + {::mf/wrap [mf/memo #(mf/throttle % 200)] + ::mf/wrap-props false} [{:keys [objects filtered? parent-size] :as props}] - (let [selected (mf/deref refs/selected-shapes) - selected (hooks/use-equal-memo selected) - root (get objects uuid/zero) + (let [selected (mf/deref refs/selected-shapes) + selected (hooks/use-equal-memo selected) + root (get objects uuid/zero) new-css-system (mf/use-ctx ctx/new-css-system)] [:ul - {:class (if new-css-system - (dom/classnames (css :element-list) true) - (dom/classnames :element-list true))} + {:class (stl/css new-css-system ::stl/element-list)} [:& hooks/sortable-container {} (for [[index id] (reverse (d/enumerate (:shapes root)))] (when-let [obj (get objects id)] - (if (= (:type obj) :frame) + (if (cph/frame-shape? obj) [:& frame-wrapper {:item obj :selected selected @@ -63,7 +60,7 @@ :sortable? true :filtered? filtered? :parent-size parent-size - :recieved-depth -1}] + :depth -1}] [:& layer-item {:item obj :selected selected @@ -72,20 +69,18 @@ :key id :sortable? true :filtered? filtered? - :recieved-depth -1 + :depth -1 :parent-size parent-size}])))]])) (mf/defc filters-tree - {::mf/wrap [#(mf/memo % =) - #(mf/throttle % 200)]} - [{:keys [objects parent-size] :as props}] - (let [selected (mf/deref refs/selected-shapes) - selected (hooks/use-equal-memo selected) - root (get objects uuid/zero) + {::mf/wrap [mf/memo #(mf/throttle % 200)] + ::mf/wrap-props false} + [{:keys [objects parent-size]}] + (let [selected (mf/deref refs/selected-shapes) + selected (hooks/use-equal-memo selected) + root (get objects uuid/zero) new-css-system (mf/use-ctx ctx/new-css-system)] - [:ul {:class (if new-css-system - (dom/classnames (css :element-list) true) - (dom/classnames :element-list true))} + [:ul {:class (stl/css new-css-system ::stl/element-list)} (for [[index id] (d/enumerate (:shapes root))] (when-let [obj (get objects id)] [:& layer-item @@ -96,12 +91,11 @@ :key id :sortable? false :filtered? true - :recieved-depth -1 + :depth -1 :parent-size parent-size}]))])) (defn calc-reparented-objects [objects] - (let [reparented-objects (d/mapm (fn [_ val] (assoc val :parent-id uuid/zero :shapes nil)) @@ -116,314 +110,385 @@ ;; --- Layers Toolbox +;; FIXME: optimize +(defn- match-filters? + [state [id shape]] + (let [search (:search-text state) + filters (:filters state) + filters (cond-> filters + (contains? filters :shape) + (conj :rect :circle :path :bool))] + (or (= uuid/zero id) + (and (or (str/includes? (str/lower (:name shape)) (str/lower search)) + (str/includes? (dm/str (:id shape)) (str/lower search))) + (or (empty? filters) + (and (contains? filters :component) + (contains? shape :component-id)) + (let [direct-filters (into #{} (filter #{:frame :rect :circle :path :bool :image :text}) filters)] + (contains? direct-filters (:type shape))) + (and (contains? filters :group) + (and (cph/group-shape? shape) + (not (contains? shape :component-id)) + (or (not (contains? shape :masked-group)) + (false? (:masked-group shape))))) + (and (contains? filters :mask) + (true? (:masked-group shape)))))))) + (defn use-search [page objects] - (let [filter-state (mf/use-state {:show-search-box false - :show-filters-menu false - :search-text "" - :active-filters #{} - :num-items 100}) - new-css-system (mf/use-ctx ctx/new-css-system) + (let [new-css-system (mf/use-ctx ctx/new-css-system) + state* (mf/use-state + {:show-search false + :show-menu false + :search-text "" + :filters #{} + :num-items 100}) + state (deref state*) + + current-filters (:filters state) + current-items (:num-items state) + current-search (:search-text state) + show-menu? (:show-menu state) + show-search? (:show-search state) + clear-search-text - (mf/use-callback - (fn [] - (swap! filter-state assoc :search-text "" :num-items 100))) - - update-search-text (mf/use-fn - (mf/deps new-css-system) - (fn [event] - ;; NOTE: When old-css-system is removed this function will recibe value and event - ;; Let won't be necessary any more - (let [value (if new-css-system - event - (dom/get-target-val event))] - (swap! filter-state assoc :search-text value :num-items 100)))) + #(swap! state* assoc :search-text "" :num-items 100)) - toggle-search - (mf/use-callback - (fn [event] - (let [node (dom/get-current-target event)] - (swap! filter-state assoc :search-text "") - (swap! filter-state assoc :active-filters #{}) - (swap! filter-state assoc :show-filters-menu false) - (swap! filter-state assoc :num-items 100) - (swap! filter-state update :show-search-box not) - (dom/blur! node)))) toggle-filters - (mf/use-callback - (fn [] - (swap! filter-state update :show-filters-menu not))) + (mf/use-fn + #(swap! state* update :show-menu not)) + + update-search-text-v1 + (mf/use-fn + (fn [event] + (let [value (-> event dom/get-target dom/get-value)] + (swap! state* assoc :search-text value :num-items 100)))) + + update-search-text-v2 + (mf/use-fn + (fn [value _event] + (swap! state* assoc :search-text value :num-items 100))) + + toggle-search + (mf/use-fn + (fn [event] + (let [node (dom/get-current-target event)] + (dom/blur! node) + (swap! state* (fn [state] + (-> state + (assoc :search-text "") + (assoc :filters #{}) + (assoc :show-menu false) + (assoc :num-items 100) + (update :show-search not))))))) remove-filter - (mf/use-callback - (mf/deps @filter-state) - (fn [key] - (fn [_] - (swap! filter-state update :active-filters disj key) - (swap! filter-state assoc :num-items 100)))) + (mf/use-fn + (fn [event] + (let [fkey (-> (dom/get-current-target event) + (dom/get-data "filter") + (keyword))] + (swap! state* (fn [state] + (-> state + (update :filters disj fkey) + (assoc :num-items 100))))))) add-filter - (mf/use-callback - (mf/deps @filter-state (:show-filters-menu @filter-state)) - (fn [key] - (fn [_] - (swap! filter-state update :active-filters conj key) - (swap! filter-state assoc :num-items 100) - (toggle-filters)))) + (mf/use-fn + (fn [event] + (let [key (-> (dom/get-current-target event) + (dom/get-data "filter") + (keyword))] + (swap! state* (fn [state] + (-> state + (update :filters conj key) + (update :show-menu not) + (assoc :num-items 100))))))) active? - (and - (:show-search-box @filter-state) - (or (d/not-empty? (:search-text @filter-state)) - (d/not-empty? (:active-filters @filter-state)))) + (and ^boolean show-search? + (or ^boolean (d/not-empty? current-search) + ^boolean (d/not-empty? current-filters))) - search-and-filters - (fn [[id shape]] - (let [search (:search-text @filter-state) - filters (:active-filters @filter-state) - filters (cond-> filters - (some #{:shape} filters) - (conj :rect :circle :path :bool))] - (or - (= uuid/zero id) - (and - (or (str/includes? (str/lower (:name shape)) (str/lower search)) - (str/includes? (dm/str (:id shape)) (str/lower search))) - (or - (empty? filters) - (and - (some #{:component} filters) - (contains? shape :component-id)) - (let [direct_filters (filter #{:frame :rect :circle :path :bool :image :text} filters)] - (some #{(:type shape)} direct_filters)) - (and - (some #{:group} filters) - (and (= :group (:type shape)) - (not (contains? shape :component-id)) - (or (not (contains? shape :masked-group?)) (false? (:masked-group? shape))))) - (and - (some #{:mask} filters) - (true? (:masked-group? shape)))))))) + filtered-objects-all + (mf/with-memo [active? objects state] + (when active? + (into [] (filter (partial match-filters? state)) objects))) filtered-objects-total - (mf/use-memo - (mf/deps objects active? @filter-state) - #(when active? - ;; filterv so count is constant time - (filterv search-and-filters objects))) + (count filtered-objects-all) filtered-objects - (mf/use-memo - (mf/deps filtered-objects-total) - #(when active? - (calc-reparented-objects - (into {} - (take (:num-items @filter-state)) - filtered-objects-total)))) + (mf/with-memo [active? filtered-objects-all current-items] + (when active? + (->> filtered-objects-all + (into {} (take current-items)) + (calc-reparented-objects)))) handle-show-more - (fn [] - (when (<= (:num-items @filter-state) (count filtered-objects-total)) - (swap! filter-state update :num-items + 100))) + (mf/use-fn + (mf/deps filtered-objects-total current-items) + (fn [_] + (when (<= current-items filtered-objects-total) + (swap! state* update :num-items + 100)))) handle-key-down - (mf/use-callback + (mf/use-fn (fn [event] - (let [enter? (kbd/enter? event) - esc? (kbd/esc? event) - node (dom/event->target event)] - (when ^boolean enter? (dom/blur! node)) - (when ^boolean esc? (dom/blur! node)))))] + (when-let [node (dom/event->target event)] + (when (kbd/enter? event) + (dom/blur! node)) + (when (kbd/esc? event) + (dom/blur! node)))))] [filtered-objects handle-show-more - (mf/html - (if (:show-search-box @filter-state) - [:* - [:div {:class (if new-css-system - (dom/classnames (css :tool-window-bar) true - (css :search) true) - (dom/classnames :tool-window-bar true - :search true))} - (if new-css-system - [:& search-bar - {:on-change update-search-text - :value (:search-text @filter-state) - :on-clear clear-search-text - :placeholder (tr "workspace.sidebar.layers.search")} - [:button - {:on-click toggle-filters - :class (dom/classnames :active active? - (css :filter-button) true)} - i/filter-refactor]] + #(mf/html + (if show-search? + [:* + [:div {:class (stl/css new-css-system ::stl/tool-window-bar ::stl/search)} + (if ^boolean new-css-system + [:& search-bar + {:on-change update-search-text-v2 + :value current-search + :on-clear clear-search-text + :placeholder (tr "workspace.sidebar.layers.search")} + [:button + {:on-click toggle-filters + :class (stl/css-case + ::stl/filters-button true + :active active?)} + i/filter-refactor]] - [:span.search-box - [:button.filter - {:on-click toggle-filters - :class (dom/classnames :active active?)} - i/icon-filter] - [:div - [:input {:on-change update-search-text - :value (:search-text @filter-state) - :auto-focus (:show-search-box @filter-state) - :placeholder (tr "workspace.sidebar.layers.search") - :on-key-down handle-key-down}] - (when (not (= "" (:search-text @filter-state))) - [:button.clear {:on-click clear-search-text} - i/exclude])]]) + [:span.search-box + [:button.filter + {:on-click toggle-filters + :class (stl/css-case :active active?)} + i/icon-filter] + [:div + [:input {:on-change update-search-text-v1 + :value current-search + :auto-focus show-search? + :placeholder (tr "workspace.sidebar.layers.search") + :on-key-down handle-key-down}] + (when (not (= "" current-search)) + [:button.clear {:on-click clear-search-text} i/exclude])]]) - [:button {:class (dom/classnames (css :close-search) new-css-system) - :on-click toggle-search} - (if new-css-system - i/close-refactor - i/cross)]] - [:div {:class (if new-css-system - (dom/classnames (css :active-filters) true) - (dom/classnames :active-filters true))} - (for [f (:active-filters @filter-state)] - (let [name (case f - :frame (tr "workspace.sidebar.layers.frames") - :group (tr "workspace.sidebar.layers.groups") - :mask (tr "workspace.sidebar.layers.masks") - :component (tr "workspace.sidebar.layers.components") - :text (tr "workspace.sidebar.layers.texts") - :image (tr "workspace.sidebar.layers.images") - :shape (tr "workspace.sidebar.layers.shapes") - (tr f))] - (if new-css-system - [:button {:class (dom/classnames (css :layer-filter) true) - :on-click (remove-filter f)} - [:span {:class (dom/classnames (css :layer-filter-icon) true)} - [:& sic/element-icon-refactor-by-type {:type f - :main-instance? (= f :component)}]] - [:span {:class (dom/classnames (css :layer-filter-name) true)} - name] - [:span {:class (dom/classnames (css :layer-filter-close) true)} - i/close-small-refactor]] - [:span {:on-click (remove-filter f)} - name i/cross])))] + [:button {:class (stl/css-case ::stl/close-search new-css-system) + :on-click toggle-search} + (if ^boolean new-css-system + i/close-refactor + i/cross)]] - (when (:show-filters-menu @filter-state) - (if new-css-system - [:ul {:class (dom/classnames (css :filters-container) true)} - [:li {:key "frames-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :frame)) - :on-click (add-filter :frame)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/board-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.frames")]] - (when (contains? (:active-filters @filter-state) :frame) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "groups-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :group)) - :on-click (add-filter :group)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/group-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.groups")]] - (when (contains? (:active-filters @filter-state) :group) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "masks-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :mask)) - :on-click (add-filter :mask)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/mask-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.masks")]] - (when (contains? (:active-filters @filter-state) :mask) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "components-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :component)) - :on-click (add-filter :component)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/component-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.components")]] - (when (contains? (:active-filters @filter-state) :component) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "texts-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :text)) - :on-click (add-filter :text)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/text-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.texts")]] - (when (contains? (:active-filters @filter-state) :text) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "images-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :image)) - :on-click (add-filter :image)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/img-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.images")]] - (when (contains? (:active-filters @filter-state) :image) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])] - [:li {:key "shapes-filter-item" - :class (dom/classnames (css :filter-menu-item) true - (css :selected) (contains? (:active-filters @filter-state) :shape)) - :on-click (add-filter :shape)} - [:div {:class (dom/classnames (css :filter-menu-item-name-wrapper) true)} - [:span {:class (dom/classnames (css :filter-menu-item-icon) true)} - i/path-refactor] - [:span {:class (dom/classnames (css :filter-menu-item-name) true)} - (tr "workspace.sidebar.layers.shapes")]] - (when (contains? (:active-filters @filter-state) :shape) - [:span {:class (dom/classnames (css :filter-menu-item-tick) true)} - i/tick-refactor])]] + [:div {:class (stl/css new-css-system ::stl/active-filters)} + (for [fkey current-filters] + (let [fname (d/name fkey) + name (case fkey + :frame (tr "workspace.sidebar.layers.frames") + :group (tr "workspace.sidebar.layers.groups") + :mask (tr "workspace.sidebar.layers.masks") + :component (tr "workspace.sidebar.layers.components") + :text (tr "workspace.sidebar.layers.texts") + :image (tr "workspace.sidebar.layers.images") + :shape (tr "workspace.sidebar.layers.shapes") + (tr fkey))] + (if ^boolean new-css-system + [:button {:class (stl/css ::stl/layer-filter) + :key fname + :data-filter fname + :on-click remove-filter} + [:span {:class (stl/css ::stl/layer-filter-icon)} + [:& sic/element-icon-refactor-by-type + {:type fkey + :main-instance? (= fkey :component)}]] + [:span {:class (stl/css ::stl/layer-filter-name)} + name] + [:span {:class (stl/css ::stl/layer-filter-close)} + i/close-small-refactor]] - [:div.filters-container - [:span {:on-click (add-filter :frame)} i/artboard (tr "workspace.sidebar.layers.frames")] - [:span {:on-click (add-filter :group)} i/folder (tr "workspace.sidebar.layers.groups")] - [:span {:on-click (add-filter :mask)} i/mask (tr "workspace.sidebar.layers.masks")] - [:span {:on-click (add-filter :component)} i/component (tr "workspace.sidebar.layers.components")] - [:span {:on-click (add-filter :text)} i/text (tr "workspace.sidebar.layers.texts")] - [:span {:on-click (add-filter :image)} i/image (tr "workspace.sidebar.layers.images")] - [:span {:on-click (add-filter :shape)} i/curve (tr "workspace.sidebar.layers.shapes")]]))] + [:span {:on-click remove-filter + :data-filter fname + :key fname} + name i/cross])))] + + (when ^boolean show-menu? + (if ^boolean new-css-system + [:ul {:class (stl/css ::stl/filters-container)} + [:li {:class (stl/css-case + ::stl/filter-menu-item true + ::stl/selected (contains? current-filters :frame)) + :data-filter "frame" + :on-click add-filter} + [:div {:class (stl/css ::stl/filter-menu-item-name-wrapper)} + [:span {:class (stl/css ::stl/filter-menu-item-icon)} i/board-refactor] + [:span {:class (stl/css ::stl/filter-menu-item-name)} + (tr "workspace.sidebar.layers.frames")]] + (when (contains? current-filters :frame) + [:span {:class (stl/css ::stl/filter-menu-item-tick)} i/tick-refactor])] + + [:li {:class (stl/css-case + ::stl/filter-menu-item true + ::stl/selected (contains? current-filters :group)) + :data-filter "group" + :on-click add-filter} + [:div {:class (stl/css ::stl/filter-menu-item-name-wrapper)} + [:span {:class (stl/css ::stl/filter-menu-item-icon)} i/group-refactor] + [:span {:class (stl/css ::stl/filter-menu-item-name)} + (tr "workspace.sidebar.layers.groups")]] + + (when (contains? current-filters :group) + [:span {:class (stl/css ::stl/filter-menu-item-tick)} i/tick-refactor])] + + [:li {:class (stl/css-case + :filter-menu-item true + :selected (contains? current-filters :mask)) + :data-filter "mask" + :on-click add-filter} + [:div {:class (stl/css ::stl/filter-menu-item-name-wrapper)} + [:span {:class (stl/css ::stl/filter-menu-item-icon)} i/mask-refactor] + [:span {:class (stl/css ::stl/filter-menu-item-name)} + (tr "workspace.sidebar.layers.masks")]] + (when (contains? current-filters :mask) + [:span {:class (stl/css ::stl/filter-menu-item-tick)} i/tick-refactor])] + + [:li {:class (stl/css-case + :filter-menu-item true + :selected (contains? current-filters :component)) + :data-filter "component" + :on-click add-filter} + [:div {:class (stl/css ::stl/filter-menu-item-name-wrapper)} + [:span {:class (stl/css ::stl/filter-menu-item-icon)} i/component-refactor] + [:span {:class (stl/css ::stl/filter-menu-item-name)} + (tr "workspace.sidebar.layers.components")]] + (when (contains? current-filters :component) + [:span {:class (stl/css ::stl/filter-menu-item-tick)} i/tick-refactor])] + [:li {:class (stl/css-case + :filter-menu-item true + :selected (contains? current-filters :text)) + :data-filter "text" + :on-click add-filter} + [:div {:class (stl/css ::stl/filter-menu-item-name-wrapper)} + [:span {:class (stl/css ::stl/filter-menu-item-icon)} i/text-refactor] + [:span {:class (stl/css ::stl/filter-menu-item-name)} + (tr "workspace.sidebar.layers.texts")]] + (when (contains? current-filters :text) + [:span {:class (stl/css ::stl/filter-menu-item-tick)} i/tick-refactor])] + + [:li {:class (stl/css-case + :filter-menu-item true + :selected (contains? current-filters :image)) + :data-filter "image" + :on-click add-filter} + [:div {:class (stl/css ::stl/filter-menu-item-name-wrapper)} + [:span {:class (stl/css ::stl/filter-menu-item-icon)} i/img-refactor] + [:span {:class (stl/css ::stl/filter-menu-item-name)} + (tr "workspace.sidebar.layers.images")]] + (when (contains? current-filters :image) + [:span {:class (stl/css ::stl/filter-menu-item-tick)} i/tick-refactor])] + [:li {:class (stl/css-case + :filter-menu-item true + :selected (contains? current-filters :shape)) + :data-filter "shape" + :on-click add-filter} + [:div {:class (stl/css ::stl/filter-menu-item-name-wrapper)} + [:span {:class (stl/css ::stl/filter-menu-item-icon)} i/path-refactor] + [:span {:class (stl/css ::stl/filter-menu-item-name)} + (tr "workspace.sidebar.layers.shapes")]] + (when (contains? current-filters :shape) + [:span {:class (stl/css ::stl/filter-menu-item-tick)} i/tick-refactor])]] + + [:div.filters-container + [:span {:data-filter "frame" + :on-click add-filter} + i/artboard (tr "workspace.sidebar.layers.frames")] + [:span {:data-filter "group" + :on-click add-filter} + i/folder (tr "workspace.sidebar.layers.groups")] + [:span {:data-filter "mask" + :on-click add-filter} + i/mask (tr "workspace.sidebar.layers.masks")] + [:span {:data-filter "component" + :on-click add-filter} + i/component (tr "workspace.sidebar.layers.components")] + [:span {:data-filter "text" + :on-click add-filter} + i/text (tr "workspace.sidebar.layers.texts")] + [:span {:data-filter "image" + :on-click add-filter} + i/image (tr "workspace.sidebar.layers.images")] + [:span {:data-filter "shape" + :on-click add-filter} + i/curve (tr "workspace.sidebar.layers.shapes")]]))] + + (if ^boolean new-css-system + [:div {:class (stl/css :tool-window-bar)} + [:& title-bar {:collapsable? false + :title (:name page) + :on-btn-click toggle-search + :btn-children i/search-refactor}]] + + [:div.tool-window-bar + [:span.page-name + (:name page)] + [:button.icon-search {:on-click toggle-search} + i/search]])))])) + + +(defn- on-scroll + [event] + (let [children (dom/get-elements-by-class "sticky-children") + length (alength children)] + (when (pos? length) + (let [target (dom/get-target event) + target-top (:top (dom/get-bounding-rect target)) + frames (dom/get-elements-by-class "root-board") + + last-hidden-frame + (->> frames + (filter #(<= (- (:top (dom/get-bounding-rect %)) target-top) 0)) + last) + + frame-id (dom/get-attribute last-hidden-frame "id") + + last-hidden-children + (->> children + (filter #(< (- (:top (dom/get-bounding-rect %)) target-top) 0)) + last) + + is-children-shown? + (and last-hidden-children + (> (- (:bottom (dom/get-bounding-rect last-hidden-children)) target-top) 0)) + + children-frame-id (dom/get-attribute last-hidden-children "data-id") + + ;; We want to check that root-board is out of view but its children are not. + ;; only in that case we make root board sticky. + sticky? (and last-hidden-frame + is-children-shown? + (= frame-id children-frame-id))] + + (run! #(dom/remove-class! % "sticky") frames) + + (when sticky? + (dom/add-class! last-hidden-frame "sticky")))))) - (if new-css-system - [:div {:class (dom/classnames (css :tool-window-bar) true)} - [:& title-bar {:collapsable? false - :title (:name page) - :on-btn-click toggle-search - :btn-children i/search-refactor}]] - [:div.tool-window-bar - [:span.page-name - (:name page)] - [:button.icon-search {:on-click toggle-search} - i/search]])))])) (mf/defc layers-toolbox - {:wrap [mf/memo]} - [{:keys [size-parent] :as props}] - (let [page (mf/deref refs/workspace-page) - focus (mf/deref refs/workspace-focus-selected) - objects (hooks/with-focus-objects (:objects page) focus) - title (when (= 1 (count focus)) (get-in objects [(first focus) :name])) + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [size-parent]}] + (let [page (mf/deref refs/workspace-page) + focus (mf/deref refs/workspace-focus-selected) + + objects (hooks/with-focus-objects (:objects page) focus) + title (when (= 1 (count focus)) + (dm/get-in objects [(first focus) :name])) + new-css-system (mf/use-ctx ctx/new-css-system) - observer-var (mf/use-var nil) - lazy-load-ref (mf/use-ref nil) + observer-var (mf/use-var nil) + lazy-load-ref (mf/use-ref nil) [filtered-objects show-more filter-component] (use-search page objects) @@ -434,103 +499,61 @@ on-render-container (fn [element] - (let [options #js {:root element} - lazy-el (mf/ref-val lazy-load-ref)] + (when-let [lazy-node (mf/ref-val lazy-load-ref)] (cond (and (some? element) (not (some? @observer-var))) - (let [observer (js/IntersectionObserver. intersection-callback options)] - (.observe observer lazy-el) + (let [observer (js/IntersectionObserver. intersection-callback + #js {:root element})] + (.observe observer lazy-node) (reset! observer-var observer)) (and (nil? element) (some? @observer-var)) - (do (.disconnect @observer-var) - (reset! observer-var nil))))) + (do (.disconnect ^js @observer-var) + (reset! observer-var nil)))))] - on-scroll - (fn [event] - (let [children (dom/get-elements-by-class "sticky-children") - length (.-length children)] - (when (< 0 length) - (let [target (dom/get-target event) - target-top (:top (dom/get-bounding-rect target)) - frames (dom/get-elements-by-class "root-board") - - last-hidden-frame (->> frames - (filter #(<= (- (:top (dom/get-bounding-rect %)) target-top) 0)) - last) - frame-id (dom/get-attribute last-hidden-frame "id") - - last-hidden-children (->> children - (filter #(< (- (:top (dom/get-bounding-rect %)) target-top) 0)) - last) - - is-children-shown? (and last-hidden-children - (> (- (:bottom (dom/get-bounding-rect last-hidden-children)) target-top) 0)) - - children-frame-id (dom/get-attribute last-hidden-children "data-id") - ;; We want to check that root-board is out of view but its children are not. - ;; only in that case we make root board sticky. - sticky? (and last-hidden-frame - is-children-shown? - (= frame-id children-frame-id))] - (doseq [frame frames] - (dom/remove-class! frame "sticky")) - - (when sticky? - (dom/add-class! last-hidden-frame "sticky"))))))] [:div#layers - {:class (if new-css-system - (dom/classnames (css :layers) true) - (dom/classnames :tool-window true))} + {:class (if ^boolean new-css-system + (stl/css ::stl/layers) + (stl/css* :tool-window))} (if (d/not-empty? focus) [:div - {:class (if new-css-system - (dom/classnames (css :tool-window-bar) true) - (dom/classnames :tool-window-bar true))} - [:button {:class (if new-css-system - (dom/classnames (css :focus-title) true) - (dom/classnames :focus-title true)) + {:class (stl/css new-css-system ::stl/tool-window-bar)} + [:button {:class (stl/css new-css-system :focus-title) :on-click #(st/emit! (dw/toggle-focus-mode))} - [:span {:class (if new-css-system - (dom/classnames (css :back-button) true) - (dom/classnames :back-button true))} - (if new-css-system + [:span {:class (stl/css new-css-system ::stl/back-button)} + (if ^boolean new-css-system i/arrow-refactor i/arrow-slide)] - [:div {:class (if new-css-system - (dom/classnames (css :focus-name) true) - (dom/classnames :focus-name true))} + + [:div {:class (stl/css new-css-system ::stl/focus-name)} (or title (tr "workspace.sidebar.layers"))] - (if new-css-system - [:div {:class (dom/classnames (css :focus-mode-tag-wrapper) true)} - [:div {:class (dom/classnames (css :focus-mode-tag) true)} (tr "workspace.focus.focus-mode")]] + + (if ^boolean new-css-system + [:div {:class (stl/css ::stl/focus-mode-tag-wrapper)} + [:div {:class (stl/css ::stl/focus-mode-tag)} (tr "workspace.focus.focus-mode")]] [:div.focus-mode (tr "workspace.focus.focus-mode")])]] - filter-component) + + (filter-component)) + (if (some? filtered-objects) [:* - [:div {:class (if new-css-system - (dom/classnames (css :tool-window-content) true) - (dom/classnames :tool-window-content true)) - :ref on-render-container :key "filters"} + [:div {:class (stl/css new-css-system ::stl/tool-window-content) + :ref on-render-container} [:& filters-tree {:objects filtered-objects :key (dm/str (:id page)) :parent-size size-parent}] [:div.lazy {:ref lazy-load-ref - :key "lazy-load" :style {:min-height 16}}]] [:div {:on-scroll on-scroll - :class (if new-css-system - (dom/classnames (css :tool-window-content) true) - (dom/classnames :tool-window-content true)) + :class (stl/css new-css-system ::stl/tool-window-content) :style {:display (when (some? filtered-objects) "none")}} [:& layers-tree {:objects filtered-objects :key (dm/str (:id page)) :filtered? true :parent-size size-parent}]]] + [:div {:on-scroll on-scroll - :class (if new-css-system - (dom/classnames (css :tool-window-content) true) - (dom/classnames :tool-window-content true)) + :class (stl/css new-css-system ::stl/tool-window-content) :style {:display (when (some? filtered-objects) "none")}} [:& layers-tree {:objects objects :key (dm/str (:id page)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 8ff2ebdb3a..68a2fe390e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -22,12 +22,12 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) -(def component-attrs [:component-id :component-file :shape-ref :main-instance? :annotation]) +(def component-attrs [:component-id :component-file :shape-ref :main-instance :annotation]) (mf/defc component-annotation [{:keys [id values shape component] :as props}] - (let [main-instance? (:main-instance? values) + (let [main-instance? (:main-instance values) component-id (:component-id values) annotation (:annotation component) editing? (mf/use-state false) @@ -140,8 +140,6 @@ (when (or @editing? creating?) [:div.counter (str @size "/300")])]]))) - - (mf/defc component-menu [{:keys [ids values shape] :as props}] (let [current-file-id (mf/use-ctx ctx/current-file-id) @@ -160,9 +158,11 @@ library-id (:component-file values) show? (some? component-id) main-instance? (if components-v2 - (:main-instance? values) + (:main-instance values) true) - main-component? (:main-instance? values) + main-component? (:main-instance values) + lacks-annotation? (nil? (:annotation values)) + local-component? (= library-id current-file-id) workspace-data (deref refs/workspace-data) workspace-libraries (deref refs/workspace-libraries) @@ -170,7 +170,6 @@ (ctkl/get-component workspace-data component-id) (ctf/get-component workspace-libraries library-id component-id)) is-dangling? (nil? component) - lacks-annotation? (nil? (:annotation component)) lib-exists? (and (not local-component?) (some? (get workspace-libraries library-id))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs index a6089f49d0..a65f6b1252 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.menus.constraints (:require [app.common.data :as d] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] @@ -32,7 +33,8 @@ shapes (as-> old-shapes $ (map gsh/translate-to-frame $ frames)) - values (let [{:keys [x y]} (-> shapes first :points gsh/points->selrect)] + ;; FIXME: performance rect + values (let [{:keys [x y]} (-> shapes first :points grc/points->rect)] (cond-> values (not= (:x values) :multiple) (assoc :x x) (not= (:y values) :multiple) (assoc :y y))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index d6e82eeea3..94631c9d4c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -8,7 +8,7 @@ (:require [app.common.colors :as clr] [app.common.data :as d] - [app.common.pages :as cp] + [app.common.types.shape.attrs :refer [default-color]] [app.main.data.workspace.colors :as dc] [app.main.store :as st] [app.main.ui.hooks :as h] @@ -57,7 +57,7 @@ (mf/use-callback (mf/deps ids) (fn [_] - (st/emit! (dc/add-fill ids {:color cp/default-color + (st/emit! (dc/add-fill ids {:color default-color :opacity 1})))) on-change @@ -77,7 +77,7 @@ on-remove (fn [index] (fn [] - (st/emit! (dc/remove-fill ids {:color cp/default-color + (st/emit! (dc/remove-fill ids {:color default-color :opacity 1} index)))) on-remove-all (fn [_] @@ -151,7 +151,7 @@ :on-remove (on-remove index) :disable-drag disable-drag :on-focus on-focus - :data-select-on-focus (not @disable-drag) + :select-on-focus (not @disable-drag) :on-blur on-blur}])]) (when (or (= type :frame) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs index aba9e6c672..1ee194d91e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.sidebar.options.menus.frame-grid (:require + [app.common.geom.grid :as gg] [app.main.data.workspace.grid :as dw] [app.main.refs :as refs] [app.main.store :as st] @@ -16,7 +17,6 @@ [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row input-row-v2]] - [app.util.geom.grid :as gg] [app.util.i18n :as i18n :refer [tr]] [okulary.core :as l] [rumext.v2 :as mf])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 101830e0f8..0a1703388b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -69,7 +69,9 @@ ;; -- User/drawing coords (mf/defc measures-menu - [{:keys [ids ids-with-children values type all-types shape] :as props}] + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [ids ids-with-children values type all-types shape]}] (let [options (if (= type :multiple) (reduce #(union %1 %2) (map #(get type->options %) all-types)) (get type->options type)) @@ -86,16 +88,16 @@ selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) selection-parents (mf/deref selection-parents-ref) - flex-child? (->> selection-parents (some ctl/flex-layout?)) - absolute? (ctl/layout-absolute? shape) - flex-container? (ctl/flex-layout? shape) - flex-auto-width? (ctl/auto-width? shape) - flex-fill-width? (ctl/fill-width? shape) + flex-child? (->> selection-parents (some ctl/flex-layout?)) + absolute? (ctl/layout-absolute? shape) + flex-container? (ctl/flex-layout? shape) + flex-auto-width? (ctl/auto-width? shape) + flex-fill-width? (ctl/fill-width? shape) flex-auto-height? (ctl/auto-height? shape) flex-fill-height? (ctl/fill-height? shape) - disabled-position-x? (and flex-child? (not absolute?)) - disabled-position-y? (and flex-child? (not absolute?)) + disabled-position-x? (and flex-child? (not absolute?)) + disabled-position-y? (and flex-child? (not absolute?)) disabled-width-sizing? (and (or flex-child? flex-container?) (or flex-auto-width? flex-fill-width?) (not absolute?)) @@ -112,7 +114,7 @@ ;; For rotated or stretched shapes, the origin point we show in the menu ;; is not the (:x :y) shape attribute, but the top left coordinate of the ;; wrapping rectangle. - values (let [{:keys [x y]} (gsh/selection-rect [(first shapes)])] + values (let [{:keys [x y]} (gsh/shapes->rect [(first shapes)])] (cond-> values (not= (:x values) :multiple) (assoc :x x) (not= (:y values) :multiple) (assoc :y y) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index dba1339cca..a00e21a3ed 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -189,6 +189,6 @@ :on-reorder (handle-reorder index) :disable-drag disable-drag :on-focus on-focus - :data-select-on-focus (not @disable-drag) + :select-on-focus (not @disable-drag) :on-blur on-blur :disable-stroke-style disable-stroke-style}])])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index bea7507b74..1780103db9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -461,7 +461,7 @@ :max 200 :step 0.1 :default "1.2" - :klass (css :line-height-input) + :class (css :line-height-input) :value (attr->string line-height) :placeholder (tr "settings.multiple") :nillable line-height-nillable @@ -477,7 +477,7 @@ {:min -200 :max 200 :step 0.1 - :klass (css :letter-spacing-input) + :class (css :letter-spacing-input) :value (attr->string letter-spacing) :placeholder (tr "settings.multiple") :on-change #(handle-change % :letter-spacing) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index dd9341be88..e5a9a92ad6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -17,31 +17,23 @@ [rumext.v2 :as mf])) (mf/defc options - {::mf/wrap [mf/memo]} + {::mf/wrap [mf/memo] + ::mf/wrap-props false} [] - (let [options (mf/deref refs/workspace-page-options) - - on-change - (fn [value] - (st/emit! (dw/change-canvas-color value))) - - on-open - (fn [] - (st/emit! (dwu/start-undo-transaction :options))) - - on-close - (fn [] - (st/emit! (dwu/commit-undo-transaction :options)))] - + (let [options (mf/deref refs/workspace-page-options) + on-change (mf/use-fn #(st/emit! (dw/change-canvas-color %))) + on-open (mf/use-fn #(st/emit! (dwu/start-undo-transaction :options))) + on-close (mf/use-fn #(st/emit! (dwu/commit-undo-transaction :options)))] [:div.element-set [:div.element-set-title (tr "workspace.options.canvas-background")] [:div.element-set-content - [:& color-row {:disable-gradient true - :disable-opacity true - :title (tr "workspace.options.canvas-background") - :color {:color (get options :background clr/canvas) - :opacity 1} - :on-change on-change - :on-open on-open - :on-close on-close}]]])) + [:& color-row + {:disable-gradient true + :disable-opacity true + :title (tr "workspace.options.canvas-background") + :color {:color (get options :background clr/canvas) + :opacity 1} + :on-change on-change + :on-open on-open + :on-close on-close}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 7bd25df76a..c5317b97cc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.pages :as cp] + [app.common.types.shape.attrs :refer [default-color]] [app.main.data.modal :as modal] [app.main.data.workspace.libraries :as dwl] [app.main.refs :as refs] @@ -41,7 +41,7 @@ (mf/defc color-row [{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove - disable-drag on-focus on-blur select-only data-select-on-focus]}] + disable-drag on-focus on-blur select-only select-on-focus]}] (let [current-file-id (mf/use-ctx ctx/current-file-id) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) @@ -98,7 +98,7 @@ (fn [color event] (let [color (cond (uc/multiple? color) - {:color cp/default-color + {:color default-color :opacity 1} (= :multiple (:opacity color)) @@ -207,7 +207,7 @@ {:class (dom/classnames :percentail (not= (:opacity color) :multiple))} [:> numeric-input {:value (-> color :opacity opacity->string) :placeholder (tr "settings.multiple") - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :on-focus on-focus :on-blur on-blur :on-change handle-opacity-change diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs index 4fde23197b..bdb6c7d7e5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs @@ -12,7 +12,8 @@ [app.util.object :as obj] [rumext.v2 :as mf])) -(mf/defc input-row [{:keys [label options value class min max on-change type placeholder default nillable on-focus data-select-on-focus]}] +(mf/defc input-row + [{:keys [label options value class min max on-change type placeholder default nillable on-focus select-on-focus]}] [:div.row-flex.input-row [:span.element-set-subtitle label] [:div.input-element {:class class} @@ -47,7 +48,7 @@ :nillable nillable :on-change on-change :on-focus on-focus - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :value (or value "")}])]]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index 7f30ce0394..3318fd65f9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -49,7 +49,7 @@ (mf/defc stroke-row {::mf/wrap-props false} - [{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag on-focus on-blur disable-stroke-style data-select-on-focus]}] + [{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag on-focus on-blur disable-stroke-style select-on-focus]}] (let [start-caps-state (mf/use-state {:open? false :top 0 :left 0}) @@ -88,7 +88,7 @@ :on-remove (on-remove index) :disable-drag disable-drag :on-focus on-focus - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :on-blur on-blur}] ;; Stroke Width, Alignment & Style @@ -103,7 +103,7 @@ :placeholder (tr "settings.multiple") :on-change (on-stroke-width-change index) :on-focus on-focus - :data-select-on-focus data-select-on-focus + :select-on-focus select-on-focus :on-blur on-blur}]] [:select#style.input-select {:value (enum->string (:stroke-alignment stroke)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 30943b7111..c07a01bab2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -9,8 +9,8 @@ [app.common.attrs :as attrs] [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.common.pages.common :as cpc] [app.common.text :as txt] + [app.common.types.shape.attrs :refer [editable-attrs]] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] @@ -210,7 +210,7 @@ extract-attrs (fn [[ids values] {:keys [id type content] :as shape}] (let [read-mode (get-in type->read-mode [type attr-group]) - editable-attrs (filter (get cpc/editable-attrs (:type shape)) attrs)] + editable-attrs (filter (get editable-attrs (:type shape)) attrs)] (case read-mode :ignore [ids values] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs index 545a7e9b7e..d2ddecc6d7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs @@ -23,44 +23,51 @@ [rumext.v2 :as mf])) (mf/defc options - {::mf/wrap [mf/memo]} - [{:keys [shape] :as props}] - (let [ids [(:id shape)] - type (:type shape) - measure-values (select-measure-keys shape) - layer-values (select-keys shape layer-attrs) - constraint-values (select-keys shape constraint-attrs) - fill-values (select-keys shape fill-attrs) - stroke-values (select-keys shape stroke-attrs) - layout-item-values (select-keys shape layout-item-attrs) - layout-container-values (select-keys shape layout-container-flex-attrs) + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [{:keys [shape]}] + (let [shape-id (:id shape) + ids (hooks/use-equal-memo [shape-id]) + type (:type shape) + measure-values (select-measure-keys shape) + layer-values (select-keys shape layer-attrs) + constraint-values (select-keys shape constraint-attrs) + fill-values (select-keys shape fill-attrs) + stroke-values (select-keys shape stroke-attrs) + layout-item-values (select-keys shape layout-item-attrs) + layout-container-values (select-keys shape layout-container-flex-attrs) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref) + is-layout-child* (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) + is-layout-child? (mf/deref is-layout-child*) - is-flex-parent-ref (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) - is-flex-parent? (mf/deref is-flex-parent-ref) + is-flex-parent* (mf/use-memo (mf/deps ids) #(refs/flex-layout-child? ids)) + is-flex-parent? (mf/deref is-flex-parent*) + + is-grid-parent* (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) + is-grid-parent? (mf/deref is-grid-parent*) - is-grid-parent-ref (mf/use-memo (mf/deps ids) #(refs/grid-layout-child? ids)) - is-grid-parent? (mf/deref is-grid-parent-ref) is-layout-child-absolute? (ctl/layout-absolute? shape) - ids (hooks/use-equal-memo ids) - parents-by-ids-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) - parents (mf/deref parents-by-ids-ref)] + parents-by-ids* (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) + parents (mf/deref parents-by-ids*)] + [:* [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] - [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] + [:& layout-container-menu + {:type type + :ids ids + :values layout-container-values + :multiple false}] (when (and (= (count ids) 1) is-layout-child? is-grid-parent?) [:& grid-cell/options {:shape (first parents) :cell (ctl/get-cell-by-shape-id (first parents) (first ids))}]) - (when is-layout-child? + (when ^boolean is-layout-child? [:& layout-item-menu {:ids ids :type type @@ -70,7 +77,8 @@ :is-grid-parent? is-grid-parent? :shape shape}]) - (when (or (not is-layout-child?) is-layout-child-absolute?) + (when (or (not ^boolean is-layout-child?) + ^boolean is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 5b173616a7..dc70ebd6f7 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -337,7 +337,7 @@ (defn on-pointer-move [move-stream] (let [last-position (mf/use-var nil)] - (mf/use-callback + (mf/use-fn (fn [event] (let [raw-pt (dom/get-client-position event) pt (uwvv/point->viewport raw-pt) @@ -347,6 +347,7 @@ delta (if @last-position (gpt/subtract raw-pt @last-position) (gpt/point 0 0))] + (rx/push! move-stream pt) (reset! last-position raw-pt) (st/emit! (ms/->PointerEvent :delta delta @@ -424,7 +425,7 @@ asset-type (dnd/get-data event "text/asset-type")] (cond (dnd/has-type? event "penpot/shape") - (let [shape (dnd/get-data event "penpot/shape") + (let [shape (dnd/get-data event "penpot/shape") final-x (- (:x viewport-coord) (/ (:width shape) 2)) final-y (- (:y viewport-coord) (/ (:height shape) 2))] (st/emit! (dw/add-shape (-> shape diff --git a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs index 37a7265863..a73354b7db 100644 --- a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs @@ -8,13 +8,13 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] + [app.common.geom.grid :as gg] + [app.common.geom.rect :as grc] [app.common.math :as mth] [app.common.pages.helpers :as cph] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.refs :as refs] - [app.util.geom.grid :as gg] [rumext.v2 :as mf])) (mf/defc square-grid [{:keys [frame zoom grid] :as props}] @@ -120,7 +120,7 @@ (and (not (cph/root? parent)) (cph/frame-shape? parent) (not (:show-content parent))) - (gsh/clip-selrect (:selrect parent)))) + (grc/clip-rect (:selrect parent)))) selrect parents)) diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs index 948e6d9282..194a838dc8 100644 --- a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs @@ -283,7 +283,7 @@ cell-origin (gpo/origin cell-bounds) cell-width (gpo/width-points cell-bounds) cell-height (gpo/height-points cell-bounds) - cell-center (gsh/center-points cell-bounds) + cell-center (gsh/points->center cell-bounds) cell-origin (gpt/transform cell-origin (gmt/transform-in cell-center (:transform-inverse shape))) handle-pointer-enter diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 4c4cdfdaf2..2ae9b2ceaa 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -8,8 +8,9 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.pages :as cp] + [app.common.pages.focus :as cpf] [app.common.pages.helpers :as cph] [app.common.types.component :as ctk] [app.common.types.shape-tree :as ctt] @@ -137,7 +138,8 @@ (mf/deps page-id) (fn [point] (let [zoom (mf/ref-val zoom-ref) - rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))] + rect (grc/center->rect point (/ 5 zoom) (/ 5 zoom))] + (if (mf/ref-val hover-disabled-ref) (rx/of nil) (->> (uw/ask-buffered! @@ -151,20 +153,20 @@ (rx/filter some?)))))) over-shapes-stream - (mf/use-memo - (fn [] - (rx/merge - ;; This stream works to "refresh" the outlines when the control is pressed - ;; but the mouse has not been moved from its position. - (->> mod-str - (rx/observe-on :async) - (rx/map #(deref last-point-ref)) - (rx/merge-map query-point)) + (mf/with-memo [move-stream mod-str] + (rx/merge + ;; This stream works to "refresh" the outlines when the control is pressed + ;; but the mouse has not been moved from its position. + (->> mod-str + (rx/observe-on :async) + (rx/map #(deref last-point-ref)) + (rx/filter some?) + (rx/merge-map query-point)) - (->> move-stream - (rx/tap #(reset! last-point-ref %)) - ;; When transforming shapes we stop querying the worker - (rx/merge-map query-point)))))] + (->> move-stream + (rx/tap #(reset! last-point-ref %)) + ;; When transforming shapes we stop querying the worker + (rx/merge-map query-point))))] ;; Refresh the refs on a value change (mf/use-effect @@ -247,7 +249,7 @@ (remove remove-id?) (remove (partial cph/hidden-parent? objects)) (remove #(and mod? (no-fill-nested-frames? %))) - (filter #(or (empty? focus) (cp/is-in-focus? objects focus %))) + (filter #(or (empty? focus) (cpf/is-in-focus? objects focus %))) (first) (get objects))] diff --git a/frontend/src/app/main/ui/workspace/viewport/rules.cljs b/frontend/src/app/main/ui/workspace/viewport/rules.cljs index 6eca1a7ec8..7ab650a9ad 100644 --- a/frontend/src/app/main/ui/workspace/viewport/rules.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/rules.cljs @@ -265,7 +265,7 @@ (mf/use-memo (mf/deps selected-shapes) #(when (d/not-empty? selected-shapes) - (gsh/selection-rect selected-shapes)))] + (gsh/shapes->rect selected-shapes)))] (when (some? vbox) [:g.rules {:pointer-events "none"} diff --git a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs index 8a346f1fea..7bee6ce909 100644 --- a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.viewport.scroll-bars (:require [app.common.colors :as clr] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.main.data.workspace :as dw] @@ -53,7 +54,7 @@ base-objects-rect (mf/with-memo [objects] (-> objects (cph/get-immediate-children) - (gsh/selection-rect))) + (gsh/shapes->rect))) inv-zoom (/ 1 zoom) vbox-height (- (:height vbox) (* inv-zoom scroll-height)) @@ -163,9 +164,11 @@ :y2 (+ vbox-y (:height vbox)) :width (:width vbox) :height (:height vbox)} - containing-rect (gsh/join-selrects [base-objects-rect vbox-rect]) - height-factor (/ (:height containing-rect) vbox-height) - width-factor (/ (:width containing-rect) vbox-width)] + + containing-rect (grc/join-rects [base-objects-rect vbox-rect]) + height-factor (/ (:height containing-rect) vbox-height) + width-factor (/ (:width containing-rect) vbox-width)] + (mf/set-ref-val! start-ref start-pt) (mf/set-ref-val! v-scrollbar-y-padding-ref v-scrollbar-y-padding) (mf/set-ref-val! h-scrollbar-x-padding-ref h-scrollbar-x-padding) diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index b7d419f32f..4ee872a59a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -180,7 +180,7 @@ (let [layout (mf/deref refs/workspace-layout) scale-text (:scale-text layout) cursor (if (#{:top-left :bottom-right} position) - (if scale-text (cur/get-dynamic "scale-nesw" rotation) (cur/get-dynamic "resize-nesw" rotation)) + (if scale-text (cur/get-dynamic "scale-nesw" rotation) (cur/get-dynamic "resize-nesw" rotation)) (if scale-text (cur/get-dynamic "scale-nwse" rotation) (cur/get-dynamic "resize-nwse" rotation))) {cx' :x cy' :y} (gpt/transform (gpt/point cx cy) transform)] @@ -361,11 +361,11 @@ (mf/defc multiple-handlers [{:keys [shapes selected zoom color disable-handlers] :as props}] - (let [shape (mf/use-memo - (mf/deps shapes) - #(->> shapes - (gsh/selection-rect) - (cts/setup-shape))) + (let [shape (mf/with-memo [shapes] + (-> shapes + (gsh/shapes->rect) + (assoc :type :multiple) + (cts/setup-shape))) on-resize (fn [current-position _initial-position event] (when (dom/left-mouse? event) @@ -388,11 +388,11 @@ (mf/defc multiple-selection [{:keys [shapes zoom color disable-handlers on-move-selected on-context-menu] :as props}] - (let [shape (mf/use-memo - (mf/deps shapes) - #(->> shapes - (gsh/selection-rect) - (cts/setup-shape)))] + (let [shape (mf/with-memo [shapes] + (-> shapes + (gsh/shapes->rect) + (assoc :type :multiple) + (cts/setup-shape)))] [:& controls-selection {:shape shape diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs index 185d50e74d..f94a40e219 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs @@ -7,6 +7,8 @@ (ns app.main.ui.workspace.viewport.snap-distances (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.types.shape.layout :as ctl] @@ -23,7 +25,7 @@ (def ^:private segment-gap-side 5) (defn selected->cross-selrec [frame selrect coord] - (let [areas (gsh/selrect->areas (:selrect frame) selrect)] + (let [areas (gsh/get-areas (:selrect frame) selrect)] (if (= :x coord) [(gsh/pad-selrec (:left areas)) (gsh/pad-selrec (:right areas))] @@ -33,12 +35,12 @@ (defn half-point "Calculates the middle point of the overlap between two selrects in the opposite axis" [coord sr1 sr2] - (let [c1 (max (get sr1 (if (= :x coord) :y1 :x1)) - (get sr2 (if (= :x coord) :y1 :x1))) - c2 (min (get sr1 (if (= :x coord) :y2 :x2)) - (get sr2 (if (= :x coord) :y2 :x2))) - half-point (+ c1 (/ (- c2 c1) 2))] - half-point)) + (let [c1 (mth/max (get sr1 (if (= :x coord) :y1 :x1)) + (get sr2 (if (= :x coord) :y1 :x1))) + c2 (mth/min (get sr1 (if (= :x coord) :y2 :x2)) + (get sr2 (if (= :x coord) :y2 :x2)))] + + (+ c1 (/ (- c2 c1) 2)))) (def pill-text-width-letter 6) (def pill-text-width-margin 6) @@ -50,10 +52,10 @@ (mf/defc shape-distance-segment "Displays a segment between two selrects with the distance between them" [{:keys [sr1 sr2 coord zoom]}] - (let [from-c (min (get sr1 (if (= :x coord) :x2 :y2)) - (get sr2 (if (= :x coord) :x2 :y2))) - to-c (max (get sr1 (if (= :x coord) :x1 :y1)) - (get sr2 (if (= :x coord) :x1 :y1))) + (let [from-c (mth/min (get sr1 (if (= :x coord) :x2 :y2)) + (get sr2 (if (= :x coord) :x2 :y2))) + to-c (mth/max (get sr1 (if (= :x coord) :x1 :y1)) + (get sr2 (if (= :x coord) :x1 :y1))) distance (- to-c from-c) distance-str (fmt/format-number distance) @@ -218,9 +220,9 @@ (let [lt-side (if (= coord :x) :left :top) gt-side (if (= coord :x) :right :bottom) - vbox (gsh/rect->selrect @refs/vbox) - areas (gsh/selrect->areas - (or (gsh/clip-selrect (:selrect frame) vbox) vbox) + vbox (deref refs/vbox) + areas (gsh/get-areas + (or (grc/clip-rect (dm/get-prop frame :selrect) vbox) vbox) selrect) query-side (fn [side] @@ -266,7 +268,7 @@ selected-shapes (unchecked-get props "selected-shapes") frame-id (-> selected-shapes first :frame-id) frame (mf/deref (refs/object-by-id frame-id)) - selrect (gsh/selection-rect selected-shapes)] + selrect (gsh/shapes->rect selected-shapes)] (when-not (ctl/any-layout? frame) [:g.distance diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index a614d83543..d23bf259f4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -9,10 +9,10 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] + [app.common.geom.snap :as sp] [app.common.pages.helpers :as cph] [app.common.types.shape.layout :as ctl] [app.main.snap :as snap] - [app.util.geom.snap-points :as sp] [beicon.core :as rx] [rumext.v2 :as mf])) @@ -52,13 +52,13 @@ (defn get-snap [coord {:keys [shapes page-id remove-snap? zoom]}] - (let [bounds (gsh/selection-rect shapes) + (let [bounds (gsh/shapes->rect shapes) frame-id (snap/snap-frame-id shapes)] (->> (rx/of bounds) (rx/flat-map (fn [bounds] - (->> (sp/selrect-snap-points bounds) + (->> (sp/rect->snap-points bounds) (map #(vector frame-id %))))) (rx/flat-map diff --git a/frontend/src/app/util/code_gen/style_css_values.cljs b/frontend/src/app/util/code_gen/style_css_values.cljs index edfc9b1c4f..53805f28f2 100644 --- a/frontend/src/app/util/code_gen/style_css_values.cljs +++ b/frontend/src/app/util/code_gen/style_css_values.cljs @@ -48,7 +48,7 @@ [selrect _ _] (-> (:points shape) - (gsh/transform-points (gsh/center-shape parent) (:transform-inverse parent)) + (gsh/transform-points (gsh/shape->center parent) (:transform-inverse parent)) (gsh/calculate-geometry)) ;;shape (gsh/transform-shape) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 0e670e05d1..c9babc26d3 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.logging :as log] [app.common.media :as cm] [app.util.globals :as globals] @@ -16,7 +17,21 @@ [app.util.webapi :as wapi] [cuerdas.core :as str] [goog.dom :as dom] - [promesa.core :as p])) + [promesa.core :as p]) + (:import goog.events.BrowserEvent)) + +(extend-type BrowserEvent + cljs.core/IDeref + (-deref [it] (.getBrowserEvent it))) + + +(defn browser-event? + [o] + (instance? BrowserEvent o)) + +(defn native-event? + [o] + (instance? js/Event o)) (log/set-level! :warn) @@ -337,11 +352,19 @@ y (.-offsetY event)] (gpt/point x y)))) +(defn get-delta-position + [event] + (let [e (if (browser-event? event) + (deref event) + event) + x (.-deltaX ^js e) + y (.-deltaY ^js e)] + (gpt/point x y))) + (defn get-client-size [^js node] (when (some? node) - {:width (.-clientWidth ^js node) - :height (.-clientHeight ^js node)})) + (grc/make-rect 0 0 (.-clientWidth node) (.-clientHeight node)))) (defn get-bounding-rect [node] diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs deleted file mode 100644 index a6120d50fd..0000000000 --- a/frontend/src/app/util/geom/snap_points.cljs +++ /dev/null @@ -1,49 +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) KALEIDOS INC - -(ns app.util.geom.snap-points - (:require - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] - [app.common.types.shape-tree :as ctst])) - -(defn selrect-snap-points [{:keys [x y width height] :as selrect}] - #{(gpt/point x y) - (gpt/point (+ x width) y) - (gpt/point (+ x width) (+ y height)) - (gpt/point x (+ y height)) - (gsh/center-selrect selrect)}) - -(defn frame-snap-points [{:keys [x y width height blocked hidden] :as selrect}] - (when (and (not blocked) (not hidden)) - (into (selrect-snap-points selrect) - #{(gpt/point (+ x (/ width 2)) y) - (gpt/point (+ x width) (+ y (/ height 2))) - (gpt/point (+ x (/ width 2)) (+ y height)) - (gpt/point x (+ y (/ height 2)))}))) - -(defn shape-snap-points - [{:keys [hidden blocked] :as shape}] - (when (and (not blocked) (not hidden)) - (case (:type shape) - :frame (-> shape :points gsh/points->selrect frame-snap-points) - (into #{(gsh/center-shape shape)} (:points shape))))) - -(defn guide-snap-points - [guide frame] - - (cond - (and (some? frame) - (not (ctst/rotated-frame? frame)) - (not (cph/root-frame? frame))) - #{} - - (= :x (:axis guide)) - #{(gpt/point (:position guide) 0)} - - :else - #{(gpt/point 0 (:position guide))})) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 0da933bea9..0dcf039d66 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -413,10 +413,10 @@ :component-file component-file) component-root? - (assoc :component-root? component-root?) + (assoc :component-root component-root?) main-instance? - (assoc :main-instance? main-instance?) + (assoc :main-instance main-instance?) (some? shape-ref) (assoc :shape-ref shape-ref)))) @@ -538,7 +538,7 @@ (let [mask? (get-meta node :masked-group str->bool)] (cond-> props mask? - (assoc :masked-group? true)))) + (assoc :masked-group true)))) (defn add-bool-data [props node] diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index 6020411717..e972a2396e 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -73,6 +73,11 @@ (unchecked-set obj key value) obj) +(defn unset! + [obj key] + (js-delete obj key) + obj) + (defn update! [obj key f & args] (let [found (get obj key ::not-found)] diff --git a/frontend/src/app/util/perf.cljs b/frontend/src/app/util/perf.cljs index f962e993eb..d1c6b81ae0 100644 --- a/frontend/src/app/util/perf.cljs +++ b/frontend/src/app/util/perf.cljs @@ -107,26 +107,40 @@ children))) (defn benchmark - [& {:keys [f warmup iterations name] + [& {:keys [run-fn chk-fn iterations name gc] :or {iterations 10000}}] - (let [end-mark (str name ":end")] + (let [end-mark (str name ":end") + blackhole (volatile! nil)] (println "=> benchmarking:" name) - (println "--> warming up:" iterations) - (loop [i iterations] + (when gc + (println "-> force gc: true")) + + (println "--> warming up: " (* iterations 2)) + (when (fn? gc) (gc)) + (loop [i (* iterations 2)] (when (pos? i) - (f) + (vreset! blackhole (run-fn)) (recur (dec i)))) (println "--> benchmarking:" iterations) + (when (fn? gc) (gc)) (js/performance.mark name) (loop [i iterations] (when (pos? i) - (f) + (vreset! blackhole (run-fn)) (recur (dec i)))) (js/performance.measure end-mark name) + + (when (fn? chk-fn) + (when-not (chk-fn @blackhole) + (println "--> EE: failed chk-fn"))) + + (let [[result] (js/performance.getEntriesByName end-mark) duration (mth/precision (.-duration ^js result) 4) avg (mth/precision (/ duration iterations) 4)] - (println "--> TOTAL:" (str duration "ms") "AVG:" (str avg "ms")) + (println "--> TOTAL:" (str duration " ms")) + (println "--> AVG :" (str avg " ms")) + (println "") (js/performance.clearMarks name) (js/performance.clearMeasures end-mark) #js {:duration duration diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index f9cf779fad..197aa4377d 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -10,13 +10,13 @@ https://en.wikipedia.org/wiki/Range_tree" (:require [app.common.data :as d] + [app.common.geom.grid :as gg] + [app.common.geom.snap :as snap] [app.common.pages.diff :as diff] [app.common.pages.helpers :as cph] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] - [app.util.geom.grid :as gg] - [app.util.geom.snap-points :as snap] [app.util.range-tree :as rt])) (def snap-attrs [:frame-id :x :y :width :height :hidden :selrect :grids]) @@ -72,12 +72,15 @@ (defn- add-frame [objects page-data frame] - (let [frame-id (:id frame) - parent-id (:parent-id frame) - frame-data (->> (snap/shape-snap-points frame) - (mapv #(array-map :type :shape - :id frame-id - :pt %))) + (let [frame-id (:id frame) + parent-id (:parent-id frame) + + frame-data (if (:blocked frame) + [] + (->> (snap/shape->snap-points frame) + (mapv #(array-map :type :shape + :id frame-id + :pt %)))) grid-x-data (get-grids-snap-points frame :x) grid-y-data (get-grids-snap-points frame :y)] @@ -101,7 +104,9 @@ (defn- add-shape [objects page-data shape] (let [frame-id (:frame-id shape) - snap-points (snap/shape-snap-points shape) + snap-points (if (:blocked shape) + [] + (snap/shape->snap-points shape)) shape-data (->> snap-points (mapv #(array-map :type :shape @@ -119,7 +124,7 @@ [objects page-data guide] (let [frame (get objects (:frame-id guide)) - guide-data (->> (snap/guide-snap-points guide frame) + guide-data (->> (snap/guide->snap-points guide frame) (mapv #(array-map :type :guide :id (:id guide) diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs index 3e9d020925..bb793064cf 100644 --- a/frontend/src/app/util/time.cljs +++ b/frontend/src/app/util/time.cljs @@ -239,7 +239,7 @@ (when v (let [v (if (datetime? v) (format v :date) v) locale (obj/get locales locale) - f (.date (.-formatLong locale) v)] + f (.date (.-formatLong ^js locale) v)] (->> #js {:locale locale} (dateFnsFormat v f)))))) diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 479f40ce5e..aef836849f 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -206,7 +206,7 @@ (dissoc :stroke-color-ref-id :stroke-color-ref-file) (not= file-id (get-component-ref-file objects shape)) - (dissoc :component-id :component-file :shape-ref :component-root?) + (dissoc :component-id :component-file :shape-ref :component-root) (= :text (:type shape)) (update :content detach-text))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 5a561cfac7..0ade2f7407 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -9,7 +9,7 @@ (:require ["jszip" :as zip] [app.common.data :as d] - [app.common.file-builder :as fb] + [app.common.files.builder :as fb] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gpa] [app.common.logging :as log] diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 5a4d6563a1..bdf827b618 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -7,8 +7,9 @@ (ns app.worker.selection (:require [app.common.data :as d] + [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.text :as gte] + [app.common.geom.shapes.text :as gst] [app.common.pages :as cp] [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] @@ -17,6 +18,8 @@ [clojure.set :as set] [okulary.core :as l])) +;; FIXME: performance shape & rect static props + (def ^:const padding-percent 0.10) (defonce state (l/atom {})) @@ -26,13 +29,14 @@ (fn [index shape] (let [{:keys [x y width height]} (cond - (and (= :text (:type shape)) - (some? (:position-data shape)) - (d/not-empty? (:position-data shape))) - (gte/position-data-bounding-box shape) + (and ^boolean (cph/text-shape? shape) + ^boolean (some? (:position-data shape)) + ^boolean (d/not-empty? (:position-data shape))) + (gst/shape->bounds shape) :else - (gsh/points->selrect (:points shape))) + (grc/points->rect (:points shape))) + shape-bound #js {:x x :y y :width width :height height} parents (get parents-index (:id shape)) @@ -55,7 +59,7 @@ (-> objects (dissoc uuid/zero) vals - gsh/selection-rect)) + gsh/shapes->rect)) (defn add-padding-bounds "Adds a padding to the bounds defined as a percent in the constant `padding-percent`. @@ -79,7 +83,7 @@ clip-parents-index (cp/create-clip-index objects parents-index) root-shapes (cph/get-immediate-children objects uuid/zero) - bounds (-> root-shapes gsh/selection-rect add-padding-bounds) + bounds (-> root-shapes gsh/shapes->rect add-padding-bounds) index-shape (make-index-shape objects parents-index clip-parents-index) initial-quadtree (qdt/create (clj->js bounds)) @@ -176,7 +180,7 @@ ;; we can update the index. Otherwise we need to ;; re-create it. (if (and (some? index) - (gsh/contains-selrect? old-bounds new-bounds)) + (grc/contains-rect? old-bounds new-bounds)) (update-index index old-objects new-objects) (create-index new-objects))))) nil) diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs index 8eeb04d622..c9dccd96b2 100644 --- a/frontend/src/app/worker/snaps.cljs +++ b/frontend/src/app/worker/snaps.cljs @@ -6,7 +6,7 @@ (ns app.worker.snaps (:require - [app.common.geom.shapes.rect :as gpr] + [app.common.geom.rect :as grc] [app.util.snap-data :as sd] [app.worker.impl :as impl] [okulary.core :as l])) @@ -28,7 +28,7 @@ [{:keys [page-id frame-id axis ranges bounds] :as message}] (let [match-bounds? (fn [[_ data]] - (some #(gpr/contains-point? bounds %) (map :pt data)))] + (some #(grc/contains-point? bounds %) (map :pt data)))] (->> (into [] (comp (mapcat #(sd/query @state page-id frame-id axis %)) (distinct)) diff --git a/frontend/test/frontend_tests/helpers/events.cljs b/frontend/test/frontend_tests/helpers/events.cljs index e398e04206..0b0558c446 100644 --- a/frontend/test/frontend_tests/helpers/events.cljs +++ b/frontend/test/frontend_tests/helpers/events.cljs @@ -19,21 +19,28 @@ ;; ---- Helpers to manage global events +(defn on-error + [cause] + (js/console.log "[CAUSE]:" (.-stack cause)) + (js/console.log "[DATA]:" (pr-str (ex-data cause)))) (defn prepare-store "Create a store with the given initial state. Wait until a :the/end event occurs, and then call the function with the final state at this point." [state done completed-cb] - (let [store (ptk/store {:state state}) + (let [store (ptk/store {:state state :on-error on-error}) stream (ptk/input-stream store) stream (->> stream (rx/take-until (rx/filter #(= :the/end %) stream)) (rx/last) - (rx/do - (fn [] - (completed-cb @store))) - (rx/subs done #(throw %)))] + (rx/do (fn [] + (completed-cb @store))) + (rx/subs (fn [_] (done)) + (fn [cause] + (js/console.log "[error]:" cause)) + (fn [_] + (js/console.log "[complete]"))))] store)) ;; Remove definitely when we ensure that the above method works diff --git a/frontend/test/frontend_tests/helpers/libraries.cljs b/frontend/test/frontend_tests/helpers/libraries.cljs index 359667a314..2dd021c295 100644 --- a/frontend/test/frontend_tests/helpers/libraries.cljs +++ b/frontend/test/frontend_tests/helpers/libraries.cljs @@ -19,18 +19,18 @@ [shape] (t/is (nil? (:shape-ref shape))) (t/is (some? (:component-id shape))) - (t/is (= (:component-root? shape) true))) + (t/is (= (:component-root shape) true))) (defn is-main-instance-subroot [shape] (t/is (some? (:component-id shape))) ; shape-ref may or may be not nil - (t/is (= (:component-root? shape) true))) + (t/is (= (:component-root shape) true))) (defn is-main-instance-child [shape] (t/is (nil? (:component-id shape))) ; shape-ref may or may be not nil (t/is (nil? (:component-file shape))) - (t/is (nil? (:component-root? shape)))) + (t/is (nil? (:component-root shape)))) (defn is-main-instance-inner [shape] @@ -42,20 +42,20 @@ [shape] (t/is (some? (:shape-ref shape))) (t/is (some? (:component-id shape))) - (t/is (= (:component-root? shape) true))) + (t/is (= (:component-root shape) true))) (defn is-instance-subroot [shape] (t/is (some? (:shape-ref shape))) (t/is (some? (:component-id shape))) - (t/is (nil? (:component-root? shape)))) + (t/is (nil? (:component-root shape)))) (defn is-instance-child [shape] (t/is (some? (:shape-ref shape))) (t/is (nil? (:component-id shape))) (t/is (nil? (:component-file shape))) - (t/is (nil? (:component-root? shape)))) + (t/is (nil? (:component-root shape)))) (defn is-instance-inner [shape] @@ -68,8 +68,8 @@ (t/is (nil? (:shape-ref shape))) (t/is (nil? (:component-id shape))) (t/is (nil? (:component-file shape))) - (t/is (nil? (:component-root? shape))) - (t/is (nil? (:remote-synced? shape))) + (t/is (nil? (:component-root shape))) + (t/is (nil? (:remote-synced shape))) (t/is (nil? (:touched shape)))) (defn is-from-file @@ -109,7 +109,7 @@ ([state root-inst-id subinstance?] (let [page (thp/current-page state) root-inst (ctn/get-shape page root-inst-id) - main-instance? (:main-instance? root-inst) + main-instance? (:main-instance root-inst) libs (wsh/get-libraries state) component (ctf/get-component libs (:component-id root-inst)) diff --git a/frontend/test/frontend_tests/helpers/pages.cljs b/frontend/test/frontend_tests/helpers/pages.cljs index b4c76dbe5b..837037699d 100644 --- a/frontend/test/frontend_tests/helpers/pages.cljs +++ b/frontend/test/frontend_tests/helpers/pages.cljs @@ -80,7 +80,7 @@ ([state label type props] (let [page (current-page state) frame (cph/get-frame (:objects page)) - shape (cts/make-shape type {:x 0 :y 0 :width 1 :height 1} props)] + shape (cts/setup-shape (merge {:type type :x 0 :y 0 :width 1 :height 1} props))] (swap! idmap assoc label (:id shape)) (update state :workspace-data cp/process-changes diff --git a/frontend/test/frontend_tests/state_components_sync_test.cljs b/frontend/test/frontend_tests/state_components_sync_test.cljs index d8191c93e7..bbae762dfd 100644 --- a/frontend/test/frontend_tests/state_components_sync_test.cljs +++ b/frontend/test/frontend_tests/state_components_sync_test.cljs @@ -22,7 +22,7 @@ (t/use-fixtures :each {:before thp/reset-idmap!}) -; === Test touched ====================== +;; === Test touched ====================== (t/deftest test-touched (t/async done @@ -47,19 +47,277 @@ ;; (get new-state :current-page-id) ;; (get new-state :workspace-libraries) ;; false true) - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; - ; [Rect 1] - ; page1 / Rect 1 - ; + ;; Expected shape tree: + ;;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;;; + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1))] + + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) #{:fill-group})) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/white)) + (t/is (= (:fill-opacity c-shape1) 1)) + )))] + + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + :the/end)))) + +(t/deftest test-touched-children-add + (t/async done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1"})) + + instance1 (thp/get-shape state :instance1) + shape2 (thp/get-shape state :shape2) + + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1* #--> Rect 1 + ;; #{:shapes-group} + ;; Circle 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[group shape1 shape2] [c-group c-shape1] _component] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1))] + + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) #{:shapes-group})) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)))))] + + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) + :the/end)))) + +(t/deftest test-touched-children-delete + (t/async done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) + + [_group1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) + + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Component 1 #--> Component 1 + ;; Rect 1* ---> Rect 1 + ;; #{:visibility-group} + ;; Rect 2 ---> Rect 2 + ;;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[group shape1 shape2] [c-group c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1))] + + (t/is (= (:name group) "Component 1")) + (t/is (= (:touched group) nil)) + (t/is (not= (:shape-ref group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:hidden shape1) true)) ; Instance shapes are not deleted but hidden + (t/is (= (:touched shape1) #{:visibility-group})) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)))))] + + (ptk/emit! + store + (dwsh/delete-shapes #{(:id shape1')}) + :the/end)))) + +(t/deftest test-touched-children-move + (t/async done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/sample-shape :shape3 :rect + {:name "Rect 3"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2) + (thp/id :shape3)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) + + [group1' shape1'] + (thl/resolve-instance state (thp/id :instance1)) + + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Rect 3 + ;; Component 1* #--> Component 1 + ;; #{:shapes-group} + ;; Rect 2 ---> Rect 2 + ;; Rect 1 ---> Rect 1 + ;; Rect 3 ---> Rect 3 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[group shape1 shape2 shape3] + [c-group c-shape1 c-shape2 c-shape3] _component] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1))] + + (t/is (= (:name group) "Component 1")) + (t/is (= (:touched group) #{:shapes-group})) + (t/is (= (:name shape1) "Rect 2")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + (t/is (= (:name shape3) "Rect 3")) + (t/is (= (:touched shape3) nil)) + (t/is (not= (:shape-ref shape3) nil)) + + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)) + (t/is (= (:name c-shape3) "Rect 3")) + (t/is (= (:touched c-shape3) nil)) + (t/is (= (:shape-ref c-shape3) nil)))))] + + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape1')} (:id group1') 2) + :the/end)))) + +(t/deftest test-touched-from-lib + (t/async + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page) + (thp/instantiate-component :instance1 + (thp/id :component1) + (thp/id :lib1))) + + [_group1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) + + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;; (let [[[group shape1] [c-group c-shape1] _component] (thl/resolve-instance-and-main new-state @@ -87,2191 +345,1932 @@ :fill-opacity 0.5}))) :the/end)))) -(t/deftest test-touched-children-add - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) - - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) - - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1* #--> Rect 1 - ; #{:shapes-group} - ; Circle 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[group shape1 shape2] [c-group c-shape1] _component] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1))] - - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) #{:shapes-group})) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)))))] - - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - :the/end)))) - -(t/deftest test-touched-children-delete - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) - - [_group1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Component 1 #--> Component 1 - ; Rect 1* ---> Rect 1 - ; #{:visibility-group} - ; Rect 2 ---> Rect 2 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[group shape1 shape2] [c-group c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1))] - - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:hidden shape1) true)) ; Instance shapes are not deleted but hidden - (t/is (= (:touched shape1) #{:visibility-group})) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)))))] - - (ptk/emit! - store - (dwsh/delete-shapes #{(:id shape1')}) - :the/end)))) - -(t/deftest test-touched-children-move - (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) - - [group1' shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Rect 3 - ; Component 1* #--> Component 1 - ; #{:shapes-group} - ; Rect 2 ---> Rect 2 - ; Rect 1 ---> Rect 1 - ; Rect 3 ---> Rect 3 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[group shape1 shape2 shape3] - [c-group c-shape1 c-shape2 c-shape3] _component] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1))] - - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) #{:shapes-group})) - (t/is (= (:name shape1) "Rect 2")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (not= (:shape-ref shape3) nil)) - - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)) - (t/is (= (:name c-shape3) "Rect 3")) - (t/is (= (:touched c-shape3) nil)) - (t/is (= (:shape-ref c-shape3) nil)))))] - - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id group1') 2) - :the/end)))) - -(t/deftest test-touched-from-lib - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance1 - (thp/id :component1) - (thp/id :lib1))) - - [_group1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) - - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] - - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) #{:fill-group})) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/white)) - (t/is (= (:fill-opacity c-shape1) 1)))))] - - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - :the/end)))) - (t/deftest test-touched-nested-upper (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [_instance2 _instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 _instance1 shape1' _shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1* ---> Circle 1 - ; #{:fill-group} - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1* ---> Circle 1 + ;; #{:fill-group} + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) #{:fill-group})) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) #{:fill-group})) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/white)) + (t/is (= (:fill-opacity shape2) 1)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + :the/end)))) (t/deftest test-touched-nested-lower-near (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :instance2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [_instance2 _instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 _instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) #{:fill-group})) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) #{:fill-group})) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + :the/end)))) (t/deftest test-touched-nested-lower-remote (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :instance2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [instance2 _instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 _instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) #{:fill-group})) + )))] - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) #{:fill-group})) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)))))] + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance2)) + :the/end)))) - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance2)) - :the/end)))) - -;; ; === Test reset changes ====================== +;; === Test reset changes ====================== (t/deftest test-reset-changes (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (:id instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main + new-state + (:id instance1))] - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/white)) - (t/is (= (:fill-opacity shape1) 1)) - (t/is (= (:touched shape1) nil)) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/white)) + (t/is (= (:fill-opacity shape1) 1)) + (t/is (= (:touched shape1) nil)) - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/white)) - (t/is (= (:fill-opacity c-shape1) 1)) - (t/is (= (:touched c-shape1) nil)))))] + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:fill-color c-shape1) clr/white)) + (t/is (= (:fill-opacity c-shape1) 1)) + (t/is (= (:touched c-shape1) nil)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/reset-component (:id instance1)) + :the/end)))) (t/deftest test-reset-children-add (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1"})) - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) + instance1 (thp/get-shape state :instance1) + shape2 (thp/get-shape state :shape2) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1))] - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) nil)) + (t/is (not= (:shape-ref group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)))))] + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) + (dwl/reset-component (:id instance1)) + :the/end)))) (t/deftest test-reset-children-delete (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Component 1 #--> Component 1 - ; Rect 1 ---> Rect 1 - ; Rect 2 ---> Rect 2 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[group shape1 shape2] - [c-group c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[group shape1 shape2] + [c-group c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1))] - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) + (t/is (= (:name group) "Component 1")) + (t/is (= (:touched group) nil)) + (t/is (not= (:shape-ref group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)))))] + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)))))] - (ptk/emit! - store - (dwsh/delete-shapes #{(:id shape1')}) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dwsh/delete-shapes #{(:id shape1')}) + (dwl/reset-component (:id instance1)) + :the/end)))) (t/deftest test-reset-children-move (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/sample-shape :shape3 :rect + {:name "Rect 3"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2) + (thp/id :shape3)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Rect 3 - ; Component 1 #--> Component 1 - ; Rect 1 ---> Rect 1 - ; Rect 2 ---> Rect 2 - ; Rect 3 ---> Rect 3 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[group shape1 shape2 shape3] [c-group c-shape1 c-shape2 c-shape3] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Rect 3 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; Rect 3 ---> Rect 3 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[group shape1 shape2 shape3] [c-group c-shape1 c-shape2 c-shape3] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1))] - (t/is (= (:name group) "Component 1")) - (t/is (= (:touched group) nil)) - (t/is (not= (:shape-ref group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:touched shape1) nil)) - (t/is (not= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (not= (:shape-ref shape3) nil)) + (t/is (= (:name group) "Component 1")) + (t/is (= (:touched group) nil)) + (t/is (not= (:shape-ref group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:touched shape1) nil)) + (t/is (not= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + (t/is (= (:name shape3) "Rect 3")) + (t/is (= (:touched shape3) nil)) + (t/is (not= (:shape-ref shape3) nil)) - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:shape-ref c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:shape-ref c-shape1) nil)) - (t/is (= (:name c-shape2) "Rect 2")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:shape-ref c-shape2) nil)) - (t/is (= (:name c-shape3) "Rect 3")) - (t/is (= (:touched c-shape3) nil)) - (t/is (= (:shape-ref c-shape3) nil)))))] + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:shape-ref c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:shape-ref c-shape1) nil)) + (t/is (= (:name c-shape2) "Rect 2")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:shape-ref c-shape2) nil)) + (t/is (= (:name c-shape3) "Rect 3")) + (t/is (= (:touched c-shape3) nil)) + (t/is (= (:shape-ref c-shape3) nil)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) + (dwl/reset-component (:id instance1)) + :the/end)))) (t/deftest test-reset-from-lib (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :instance1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance2 - (thp/id :component1) - (thp/id :lib1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :instance1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page) + (thp/instantiate-component :instance2 + (thp/id :component1) + (thp/id :lib1))) - [instance2 shape2] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 shape2] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - (let [[[group shape1] [c-group c-shape1] _component] - (thl/resolve-instance-and-main - new-state - (:id instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + (let [[[group shape1] [c-group c-shape1] _component] + (thl/resolve-instance-and-main + new-state + (:id instance2))] - (t/is (= (:name group) "Rect 1")) - (t/is (= (:touched group) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/white)) - (t/is (= (:fill-opacity shape1) 1)) - (t/is (= (:touched shape1) nil)) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:touched group) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/white)) + (t/is (= (:fill-opacity shape1) 1)) + (t/is (= (:touched shape1) nil)) - (t/is (= (:name c-group) "Rect 1")) - (t/is (= (:touched c-group) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/white)) - (t/is (= (:fill-opacity c-shape1) 1)) - (t/is (= (:touched c-shape1) nil)))))] + (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:touched c-group) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:fill-color c-shape1) clr/white)) + (t/is (= (:fill-opacity c-shape1) 1)) + (t/is (= (:touched c-shape1) nil)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2)] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance2)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2)] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/reset-component (:id instance2)) + :the/end)))) (t/deftest test-reset-nested-upper (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [instance2 _instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 _instance1 shape1' _shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/white)) + (t/is (= (:fill-opacity shape2) 1)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/reset-component (:id instance2)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/reset-component (:id instance2)) + :the/end)))) (t/deftest test-reset-nested-lower-near (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :instance2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/white)) + (t/is (= (:fill-opacity shape2) 1)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance1)) - (dwl/reset-component (:id instance2)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance1)) + (dwl/reset-component (:id instance2)) + :the/end)))) (t/deftest test-reset-nested-lower-remote (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :instance2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :instance2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2))) - [instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:fill-group} - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; (remote-synced) - ; Rect 1 ---> Rect 1 - ; (remote-synced) - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:fill-group} + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; (remote-synced) + ;; Rect 1 ---> Rect 1 + ;; (remote-synced) + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/white)) + (t/is (= (:fill-opacity shape2) 1)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) #{:fill-group})) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)))))] + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) #{:fill-group})) + (t/is (= (:fill-color c-shape2) clr/test)) + (t/is (= (:fill-opacity c-shape2) 0.5)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance2)) - (dwl/reset-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance2)) + (dwl/reset-component (:id instance1)) + :the/end)))) -;; ; === Test update component ====================== +;;; === Test update component ====================== (t/deftest test-update-component (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 <== (not updated) - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 <== (not updated) + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[main1 shape1] [c-main1 c-shape1] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape2] [c-instance1 c-shape2] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape3] [c-instance2 c-shape3] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/white)) - (t/is (= (:fill-opacity shape3) 1)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:fill-color shape3) clr/white)) + (t/is (= (:fill-opacity shape3) 1)) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape2 c-shape1)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape3 c-shape1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance1)) + :the/end)))) (t/deftest test-update-component-and-sync (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - [_instance2 _shape1''] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 _shape1''] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[main1 shape1] [c-main1 c-shape1] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape2] [c-instance1 c-shape2] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape3] [c-instance2 c-shape3] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:fill-opacity shape3) 0.5)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:fill-color shape3) clr/test)) + (t/is (= (:fill-opacity shape3) 0.5)) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape2 c-shape1)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape3 c-shape1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-preserve-touched (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - [_instance2 shape1''] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 shape1''] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1* ---> Rect 1 - ; #{:stroke-group} - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[main1 shape1] [c-main1 c-shape1] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1* ---> Rect 1 + ;; #{:stroke-group} + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[main1 shape1] [c-main1 c-shape1] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape2] [c-instance1 c-shape2] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape2] [c-instance1 c-shape2] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape3] [c-instance2 c-shape3] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape3] [c-instance2 c-shape3] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:stroke-width shape1) 0.5)) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:stroke-width shape1) 0.5)) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:stroke-width shape2) 0.5)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) (:id c-shape1))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:stroke-width shape2) 0.5)) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) (:id c-shape1))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:stroke-width shape3) 0.2)) - (t/is (= (:touched shape3) #{:stroke-group})) - (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:fill-color shape3) clr/test)) + (t/is (= (:stroke-width shape3) 0.2)) + (t/is (= (:touched shape3) #{:stroke-group})) + (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape2 c-shape1)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape3 c-shape1)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape2 c-shape1)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape3 c-shape1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :stroke-width 0.5}))) - (dch/update-shapes [(:id shape1'')] - (fn [shape] - (merge shape {:stroke-width 0.2}))) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :stroke-width 0.5}))) + (dch/update-shapes [(:id shape1'')] + (fn [shape] + (merge shape {:stroke-width 0.2}))) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-children-add (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1"})) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1"})) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - instance1 (thp/get-shape state :instance1) - shape2 (thp/get-shape state :shape2) + instance1 (thp/get-shape state :instance1) + shape2 (thp/get-shape state :shape2) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Circle 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Circle 1 ---> Circle 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Circle 1 ---> Circle 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[main1 shape1 shape2] - [c-main1 c-shape1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Circle 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[main1 shape1 shape2] + [c-main1 c-shape1 c-shape2] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape3 shape4] - [c-instance1 c-shape3 c-shape4] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape3 shape4] + [c-instance1 c-shape3 c-shape4] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape5 shape6] - [c-instance2 c-shape5 c-shape6] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape5 shape6] + [c-instance2 c-shape5 c-shape6] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Rect 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) + (t/is (= (:name main1) "Rect 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape3) "Circle 1")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name shape4) "Rect 1")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:shape-ref shape4) (:id c-shape2))) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape5) "Circle 1")) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape1))) - (t/is (= (:name shape6) "Rect 1")) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape5) "Circle 1")) + (t/is (= (:touched shape5) nil)) + (t/is (= (:shape-ref shape5) (:id c-shape1))) + (t/is (= (:name shape6) "Rect 1")) + (t/is (= (:touched shape6) nil)) + (t/is (= (:shape-ref shape4) (:id c-shape2))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape3 c-shape1)) - (t/is (= c-shape4 c-shape2)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape5 c-shape1)) - (t/is (= c-shape6 c-shape2)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-shape2 shape2)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape3 c-shape1)) + (t/is (= c-shape4 c-shape2)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape5 c-shape1)) + (t/is (= c-shape6 c-shape2)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape2)} (:id instance1) 0) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-children-delete (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1' _shape2'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect 2 - ; Component 1 #--> Component 1 - ; Rect 1 ---> Rect 1 - ; Rect 2 ---> Rect 2 - ; Component 1 #--> Component 1 - ; Rect 1 ---> Rect 1 - ; Rect 2 ---> Rect 2 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[main1 shape1 shape2] - [c-main1 c-shape1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect 2 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; Component 1 #--> Component 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 2 ---> Rect 2 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[main1 shape1 shape2] + [c-main1 c-shape1 c-shape2] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape3 shape4] - [c-instance1 c-shape3 c-shape4] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape3 shape4] + [c-instance1 c-shape3 c-shape4] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape5 shape6] - [c-instance2 c-shape5 c-shape6] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape5 shape6] + [c-instance2 c-shape5 c-shape6] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Component 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:hidden shape1) true)) ; Instance shapes are not deleted but hidden - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 2")) - (t/is (= (:hidden shape2) nil)) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) + (t/is (= (:name main1) "Component 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:hidden shape1) true)) ;; Instance shapes are not deleted but hidden + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 2")) + (t/is (= (:hidden shape2) nil)) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) nil)) - (t/is (= (:name instance1) "Component 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape3) "Rect 1")) - (t/is (= (:hidden shape3) true)) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) (:id c-shape1))) - (t/is (= (:name shape4) "Rect 2")) - (t/is (= (:hidden shape4) nil)) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape2))) + (t/is (= (:name instance1) "Component 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape3) "Rect 1")) + (t/is (= (:hidden shape3) true)) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) (:id c-shape1))) + (t/is (= (:name shape4) "Rect 2")) + (t/is (= (:hidden shape4) nil)) + (t/is (= (:touched shape4) nil)) + (t/is (= (:shape-ref shape4) (:id c-shape2))) - (t/is (= (:name instance2) "Component 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape5) "Rect 1")) - (t/is (= (:hidden shape5) true)) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape1))) - (t/is (= (:name shape6) "Rect 2")) - (t/is (= (:hidden shape6) nil)) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape6) (:id c-shape2))) + (t/is (= (:name instance2) "Component 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape5) "Rect 1")) + (t/is (= (:hidden shape5) true)) + (t/is (= (:touched shape5) nil)) + (t/is (= (:shape-ref shape5) (:id c-shape1))) + (t/is (= (:name shape6) "Rect 2")) + (t/is (= (:hidden shape6) nil)) + (t/is (= (:touched shape6) nil)) + (t/is (= (:shape-ref shape6) (:id c-shape2))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape3 c-shape1)) - (t/is (= c-shape4 c-shape2)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape5 c-shape1)) - (t/is (= c-shape6 c-shape2)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-shape2 shape2)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape3 c-shape1)) + (t/is (= c-shape4 c-shape2)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape5 c-shape1)) + (t/is (= c-shape6 c-shape2)))))] - (ptk/emit! - store - (dwsh/delete-shapes #{(:id shape1')}) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dwsh/delete-shapes #{(:id shape1')}) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-children-move (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect 2"}) - (thp/sample-shape :shape3 :rect - {:name "Rect 3"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1) - (thp/id :shape2) - (thp/id :shape3)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/instantiate-component :instance2 - (thp/id :component1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect 2"}) + (thp/sample-shape :shape3 :rect + {:name "Rect 3"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1) + (thp/id :shape2) + (thp/id :shape3)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/instantiate-component :instance2 + (thp/id :component1))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance1 shape1' _shape2' _shape3'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1' _shape2' _shape3'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 2 - ; Rect 1 - ; Rect 3 - ; Component 1 #--> Component 1 - ; Rect 2 ---> Rect 2 - ; Rect 1 ---> Rect 1 - ; Rect 3 ---> Rect 3 - ; Component 1 #--> Component 1 - ; Rect 2 ---> Rect 2 - ; Rect 1 ---> Rect 1 - ; Rect 3 ---> Rect 3 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [[[main1 shape1 shape2 shape3] - [c-main1 c-shape1 c-shape2 c-shape3] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 2 + ;; Rect 1 + ;; Rect 3 + ;; Component 1 #--> Component 1 + ;; Rect 2 ---> Rect 2 + ;; Rect 1 ---> Rect 1 + ;; Rect 3 ---> Rect 3 + ;; Component 1 #--> Component 1 + ;; Rect 2 ---> Rect 2 + ;; Rect 1 ---> Rect 1 + ;; Rect 3 ---> Rect 3 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [[[main1 shape1 shape2 shape3] + [c-main1 c-shape1 c-shape2 c-shape3] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1)) - [[instance1 shape4 shape5 shape6] - [c-instance1 c-shape4 c-shape5 c-shape6] component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + [[instance1 shape4 shape5 shape6] + [c-instance1 c-shape4 c-shape5 c-shape6] component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape7 shape8 shape9] - [c-instance2 c-shape7 c-shape8 c-shape9] component3] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape7 shape8 shape9] + [c-instance2 c-shape7 c-shape8 c-shape9] component3] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name main1) "Component 1")) - (t/is (= (:touched main1) nil)) - (t/is (= (:shape-ref main1) nil)) - (t/is (= (:name shape1) "Rect 2")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:shape-ref shape1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:shape-ref shape2) nil)) - (t/is (= (:name shape3) "Rect 3")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:shape-ref shape3) nil)) + (t/is (= (:name main1) "Component 1")) + (t/is (= (:touched main1) nil)) + (t/is (= (:shape-ref main1) nil)) + (t/is (= (:name shape1) "Rect 2")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:shape-ref shape1) nil)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:shape-ref shape2) nil)) + (t/is (= (:name shape3) "Rect 3")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:shape-ref shape3) nil)) - (t/is (= (:name instance1) "Component 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:shape-ref instance1) (:id c-main1))) - (t/is (= (:name shape4) "Rect 2")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:shape-ref shape4) (:id c-shape1))) - (t/is (= (:name shape5) "Rect 1")) - (t/is (= (:touched shape5) nil)) - (t/is (= (:shape-ref shape5) (:id c-shape2))) - (t/is (= (:name shape6) "Rect 3")) - (t/is (= (:touched shape6) nil)) - (t/is (= (:shape-ref shape6) (:id c-shape3))) + (t/is (= (:name instance1) "Component 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:shape-ref instance1) (:id c-main1))) + (t/is (= (:name shape4) "Rect 2")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:shape-ref shape4) (:id c-shape1))) + (t/is (= (:name shape5) "Rect 1")) + (t/is (= (:touched shape5) nil)) + (t/is (= (:shape-ref shape5) (:id c-shape2))) + (t/is (= (:name shape6) "Rect 3")) + (t/is (= (:touched shape6) nil)) + (t/is (= (:shape-ref shape6) (:id c-shape3))) - (t/is (= (:name instance2) "Component 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:shape-ref instance2) (:id c-main1))) - (t/is (= (:name shape7) "Rect 2")) - (t/is (= (:touched shape7) nil)) - (t/is (= (:shape-ref shape7) (:id c-shape1))) - (t/is (= (:name shape8) "Rect 1")) - (t/is (= (:touched shape8) nil)) - (t/is (= (:shape-ref shape8) (:id c-shape2))) - (t/is (= (:name shape9) "Rect 3")) - (t/is (= (:touched shape9) nil)) - (t/is (= (:shape-ref shape9) (:id c-shape3))) + (t/is (= (:name instance2) "Component 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:shape-ref instance2) (:id c-main1))) + (t/is (= (:name shape7) "Rect 2")) + (t/is (= (:touched shape7) nil)) + (t/is (= (:shape-ref shape7) (:id c-shape1))) + (t/is (= (:name shape8) "Rect 1")) + (t/is (= (:touched shape8) nil)) + (t/is (= (:shape-ref shape8) (:id c-shape2))) + (t/is (= (:name shape9) "Rect 3")) + (t/is (= (:touched shape9) nil)) + (t/is (= (:shape-ref shape9) (:id c-shape3))) - (t/is (= component1 component2 component3)) - (t/is (= c-main1 main1)) - (t/is (= c-shape1 shape1)) - (t/is (= c-shape2 shape2)) - (t/is (= c-shape3 shape3)) - (t/is (= c-instance1 c-main1)) - (t/is (= c-shape4 c-shape4)) - (t/is (= c-shape5 c-shape5)) - (t/is (= c-shape6 c-shape6)) - (t/is (= c-instance2 c-main1)) - (t/is (= c-shape7 c-shape7)) - (t/is (= c-shape8 c-shape8)) - (t/is (= c-shape9 c-shape9)))))] + (t/is (= component1 component2 component3)) + (t/is (= c-main1 main1)) + (t/is (= c-shape1 shape1)) + (t/is (= c-shape2 shape2)) + (t/is (= c-shape3 shape3)) + (t/is (= c-instance1 c-main1)) + (t/is (= c-shape4 c-shape4)) + (t/is (= c-shape5 c-shape5)) + (t/is (= c-shape6 c-shape6)) + (t/is (= c-instance2 c-main1)) + (t/is (= c-shape7 c-shape7)) + (t/is (= c-shape8 c-shape8)) + (t/is (= c-shape9 c-shape9)))))] - (ptk/emit! - store - (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dw/relocate-shapes #{(:id shape1')} (:id instance1) 2) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) (t/deftest test-update-from-lib (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance1 - (thp/id :component1) - (thp/id :lib1)) - (thp/instantiate-component :instance2 - (thp/id :component1) - (thp/id :lib1))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page) + (thp/instantiate-component :instance1 + (thp/id :component1) + (thp/id :lib1)) + (thp/instantiate-component :instance2 + (thp/id :component1) + (thp/id :lib1))) - [instance1 shape1'] - (thl/resolve-instance state (thp/id :instance1)) + [instance1 shape1'] + (thl/resolve-instance state (thp/id :instance1)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - (let [[[instance1 shape1] [c-instance1 c-shape1] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + (let [[[instance1 shape1] [c-instance1 c-shape1] _component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - [[instance2 shape2] [_c-instance2 _c-shape2] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2))] + [[instance2 shape2] [_c-instance2 _c-shape2] _component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2))] - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:touched shape1) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:touched shape1) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:fill-color c-shape1) clr/test)) - (t/is (= (:fill-opacity c-shape1) 0.5)) - (t/is (= (:touched c-shape1) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:fill-color c-shape1) clr/test)) + (t/is (= (:fill-opacity c-shape1) 0.5)) + (t/is (= (:touched c-shape1) nil)) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:touched shape2) nil)))))] + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:touched shape2) nil)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance1) (thp/id :lib1)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component-sync (:id instance1) (thp/id :lib1)) + :the/end)))) (t/deftest test-update-nested-upper (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2)) - (thp/instantiate-component :instance3 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2)) + (thp/instantiate-component :instance3 + (thp/id :component2))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance2 _instance1 shape1' _shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 _instance1 shape1' _shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2)) - [[instance4 instance3 shape3 shape4] - [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance3))] + [[instance4 instance3 shape3 shape4] + [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance3))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/test)) - (t/is (= (:fill-opacity shape1) 0.5)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/white)) - (t/is (= (:fill-opacity shape2) 1)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/test)) + (t/is (= (:fill-opacity shape1) 0.5)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/white)) + (t/is (= (:fill-opacity shape2) 1)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/test)) - (t/is (= (:fill-opacity c-shape1) 0.5)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/white)) - (t/is (= (:fill-opacity c-shape2) 1)) + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/test)) + (t/is (= (:fill-opacity c-shape1) 0.5)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/white)) + (t/is (= (:fill-opacity c-shape2) 1)) - (t/is (= (:name instance4) "Board")) - (t/is (= (:touched instance4) nil)) - (t/is (= (:name instance3) "Rect 1")) - (t/is (= (:touched instance3) nil)) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:fill-color shape3) clr/test)) - (t/is (= (:fill-opacity shape3) 0.5)) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:fill-color shape4) clr/white)) - (t/is (= (:fill-opacity shape4) 1)))))] + (t/is (= (:name instance4) "Board")) + (t/is (= (:touched instance4) nil)) + (t/is (= (:name instance3) "Rect 1")) + (t/is (= (:touched instance3) nil)) + (t/is (= (:name shape3) "Circle 1")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:fill-color shape3) clr/test)) + (t/is (= (:fill-opacity shape3) 0.5)) + (t/is (= (:name shape4) "Rect 1")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:fill-color shape4) clr/white)) + (t/is (= (:fill-opacity shape4) 1)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape1')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance2) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape1')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component-sync (:id instance2) (:id file)) + :the/end)))) (t/deftest test-update-nested-lower-near (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2)) - (thp/instantiate-component :instance3 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2)) + (thp/instantiate-component :instance3 + (thp/id :component2))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [instance2 instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2)) - [[instance4 instance3 shape3 shape4] - [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance3))] + [[instance4 instance3 shape3 shape4] + [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance3))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)) + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/test)) + (t/is (= (:fill-opacity c-shape2) 0.5)) - (t/is (= (:name instance4) "Board")) - (t/is (= (:touched instance4) nil)) - (t/is (= (:name instance3) "Rect 1")) - (t/is (= (:touched instance3) nil)) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:fill-color shape3) clr/black)) - (t/is (= (:fill-opacity shape3) 0)) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:fill-color shape4) clr/test)) - (t/is (= (:fill-opacity shape4) 0.5)))))] + (t/is (= (:name instance4) "Board")) + (t/is (= (:touched instance4) nil)) + (t/is (= (:name instance3) "Rect 1")) + (t/is (= (:touched instance3) nil)) + (t/is (= (:name shape3) "Circle 1")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:fill-color shape3) clr/black)) + (t/is (= (:fill-opacity shape3) 0)) + (t/is (= (:name shape4) "Rect 1")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:fill-color shape4) clr/test)) + (t/is (= (:fill-opacity shape4) 0.5)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component (:id instance1)) - (dwl/update-component-sync (:id instance2) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component (:id instance1)) + (dwl/update-component-sync (:id instance2) (:id file)) + :the/end)))) (t/deftest test-update-nested-lower-remote (t/async done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1" - :fill-color clr/white - :fill-opacity 1}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1)) - (thp/sample-shape :shape2 :circle - {:name "Circle 1" - :fill-color clr/black - :fill-opacity 0}) - (thp/frame-shapes :frame1 - [(thp/id :instance1) - (thp/id :shape2)]) - (thp/make-component :main2 :component2 - [(thp/id :frame1)]) - (thp/instantiate-component :instance2 - (thp/id :component2)) - (thp/instantiate-component :instance3 - (thp/id :component2))) + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1" + :fill-color clr/white + :fill-opacity 1}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1)) + (thp/sample-shape :shape2 :circle + {:name "Circle 1" + :fill-color clr/black + :fill-opacity 0}) + (thp/frame-shapes :frame1 + [(thp/id :instance1) + (thp/id :shape2)]) + (thp/make-component :main2 :component2 + [(thp/id :frame1)]) + (thp/instantiate-component :instance2 + (thp/id :component2)) + (thp/instantiate-component :instance3 + (thp/id :component2))) - file (wsh/get-local-file state) + file (wsh/get-local-file state) - [_instance2 instance1 _shape1' shape2'] - (thl/resolve-instance state (thp/id :instance2)) + [_instance2 instance1 _shape1' shape2'] + (thl/resolve-instance state (thp/id :instance2)) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; (remote-synced) - ; Rect 1 ---> Rect 1 - ; (remote-synced) - ; Circle 1 ---> Circle 1 - ; Group #--> Group - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Circle 1 ---> Circle 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [[[instance2 instance1 shape1 shape2] - [c-instance2 c-instance1 c-shape1 c-shape2] _component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance2)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; (remote-synced) + ;; Rect 1 ---> Rect 1 + ;; (remote-synced) + ;; Circle 1 ---> Circle 1 + ;; Group #--> Group + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Circle 1 ---> Circle 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[instance2 instance1 shape1 shape2] + [c-instance2 c-instance1 c-shape1 c-shape2] _component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance2)) - [[instance4 instance3 shape3 shape4] - [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] - (thl/resolve-instance-and-main - new-state - (thp/id :instance3))] + [[instance4 instance3 shape3 shape4] + [_c-instance4 _c-instance3 _c-shape3 _c-shape4] _component2] + (thl/resolve-instance-and-main + new-state + (thp/id :instance3))] - (t/is (= (:name instance2) "Board")) - (t/is (= (:touched instance2) nil)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (= (:name shape1) "Circle 1")) - (t/is (= (:touched shape1) nil)) - (t/is (= (:fill-color shape1) clr/black)) - (t/is (= (:fill-opacity shape1) 0)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (= (:fill-color shape2) clr/test)) - (t/is (= (:fill-opacity shape2) 0.5)) + (t/is (= (:name instance2) "Board")) + (t/is (= (:touched instance2) nil)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (= (:name shape1) "Circle 1")) + (t/is (= (:touched shape1) nil)) + (t/is (= (:fill-color shape1) clr/black)) + (t/is (= (:fill-opacity shape1) 0)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (= (:fill-color shape2) clr/test)) + (t/is (= (:fill-opacity shape2) 0.5)) - (t/is (= (:name c-instance2) "Board")) - (t/is (= (:touched c-instance2) nil)) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:touched c-instance1) nil)) - (t/is (= (:name c-shape1) "Circle 1")) - (t/is (= (:touched c-shape1) nil)) - (t/is (= (:fill-color c-shape1) clr/black)) - (t/is (= (:fill-opacity c-shape1) 0)) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:touched c-shape2) nil)) - (t/is (= (:fill-color c-shape2) clr/test)) - (t/is (= (:fill-opacity c-shape2) 0.5)) + (t/is (= (:name c-instance2) "Board")) + (t/is (= (:touched c-instance2) nil)) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:touched c-instance1) nil)) + (t/is (= (:name c-shape1) "Circle 1")) + (t/is (= (:touched c-shape1) nil)) + (t/is (= (:fill-color c-shape1) clr/black)) + (t/is (= (:fill-opacity c-shape1) 0)) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:touched c-shape2) nil)) + (t/is (= (:fill-color c-shape2) clr/test)) + (t/is (= (:fill-opacity c-shape2) 0.5)) - (t/is (= (:name instance4) "Board")) - (t/is (= (:touched instance4) nil)) - (t/is (= (:name instance3) "Rect 1")) - (t/is (= (:touched instance3) nil)) - (t/is (= (:name shape3) "Circle 1")) - (t/is (= (:touched shape3) nil)) - (t/is (= (:fill-color shape3) clr/black)) - (t/is (= (:fill-opacity shape3) 0)) - (t/is (= (:name shape4) "Rect 1")) - (t/is (= (:touched shape4) nil)) - (t/is (= (:fill-color shape4) clr/test)) - (t/is (= (:fill-opacity shape4) 0.5)))))] + (t/is (= (:name instance4) "Board")) + (t/is (= (:touched instance4) nil)) + (t/is (= (:name instance3) "Rect 1")) + (t/is (= (:touched instance3) nil)) + (t/is (= (:name shape3) "Circle 1")) + (t/is (= (:touched shape3) nil)) + (t/is (= (:fill-color shape3) clr/black)) + (t/is (= (:fill-opacity shape3) 0)) + (t/is (= (:name shape4) "Rect 1")) + (t/is (= (:touched shape4) nil)) + (t/is (= (:fill-color shape4) clr/test)) + (t/is (= (:fill-opacity shape4) 0.5)))))] - (ptk/emit! - store - (dch/update-shapes [(:id shape2')] - (fn [shape] - (merge shape {:fill-color clr/test - :fill-opacity 0.5}))) - (dwl/update-component-sync (:id instance1) (:id file)) - :the/end)))) + (ptk/emit! + store + (dch/update-shapes [(:id shape2')] + (fn [shape] + (merge shape {:fill-color clr/test + :fill-opacity 0.5}))) + (dwl/update-component-sync (:id instance1) (:id file)) + :the/end)))) diff --git a/frontend/test/frontend_tests/state_components_test.cljs b/frontend/test/frontend_tests/state_components_test.cljs index 36286c18cb..d07a9354a9 100644 --- a/frontend/test/frontend_tests/state_components_test.cljs +++ b/frontend/test/frontend_tests/state_components_test.cljs @@ -15,6 +15,9 @@ [linked.core :as lks] [potok.core :as ptk])) +(.on js/process "uncaughtException" (fn [cause] + (js/console.log "EE" cause))) + (t/use-fixtures :each {:before thp/reset-idmap!}) @@ -28,91 +31,91 @@ {:name "Rect 1"})) store (the/prepare-store state done - (fn [new-state] - ;; Uncomment to debug - ;; (ctf/dump-tree (get new-state :workspace-data) - ;; (get new-state :current-page-id) - ;; (get new-state :workspace-libraries) - ;; false true) + (fn [new-state] + ;; Uncomment to debug + ;; (ctf/dump-tree (get new-state :workspace-data) + ;; (get new-state :current-page-id) + ;; (get new-state :workspace-libraries) + ;; false true) - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; Rect 1 - ; Rect 1 - ; - (let [shape1 (thp/get-shape new-state :shape1) + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; Rect 1 + ;; Rect 1 + ;; + (let [shape1 (thp/get-shape new-state :shape1) - [[group shape1] [c-group c-shape1] component] - (thl/resolve-instance-and-main - new-state - (:parent-id shape1)) + [[group shape1] [c-group c-shape1] component] + (thl/resolve-instance-and-main + new-state + (:parent-id shape1)) - file (wsh/get-local-file new-state)] + file (wsh/get-local-file new-state)] - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name group) "Rect 1")) - (t/is (= (:name component) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-group) "Rect 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name group) "Rect 1")) + (t/is (= (:name component) "Rect 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-group) "Rect 1")) - (thl/is-from-file group file))))] + (thl/is-from-file group file))))] (ptk/emit! - store - (dw/select-shape (thp/id :shape1)) - (dwl/add-component) - :the/end))))) + store + (dw/select-shape (thp/id :shape1)) + (dwl/add-component) + :the/end))))) (t/deftest test-add-component-from-several-shapes (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect-2"})) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect-2"})) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; [Page] - ; Root Frame - ; Component 1 - ; Rect 1 - ; Rect-2 - ; - ; [Component 1] - ; page1 / Component 1 - ; - (let [shape1 (thp/get-shape new-state :shape1) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; [Page] + ;; Root Frame + ;; Component 1 + ;; Rect 1 + ;; Rect-2 + ;; + ;; [Component 1] + ;; page1 / Component 1 + ;; + (let [shape1 (thp/get-shape new-state :shape1) - [[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (:parent-id shape1)) + [[group shape1 shape2] + [c-group c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + (:parent-id shape1)) - file (wsh/get-local-file new-state)] + file (wsh/get-local-file new-state)] - (t/is (= (:name group) "Component 1")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect-2")) - (t/is (= (:name component) "Component 1")) - (t/is (= (:name c-group) "Component 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect-2")) + (t/is (= (:name group) "Component 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect-2")) + (t/is (= (:name component) "Component 1")) + (t/is (= (:name c-group) "Component 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect-2")) - (thl/is-from-file group file))))] + (thl/is-from-file group file))))] - (ptk/emit! + (ptk/emit! store (dw/select-shapes (lks/set (thp/id :shape1) (thp/id :shape2))) @@ -121,50 +124,50 @@ (t/deftest test-add-component-from-frame (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/sample-shape :shape2 :rect - {:name "Rect-2"}) - (thp/frame-shapes :frame1 - [(thp/id :shape1) - (thp/id :shape2)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/sample-shape :shape2 :rect + {:name "Rect-2"}) + (thp/frame-shapes :frame1 + [(thp/id :shape1) + (thp/id :shape2)])) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Group - ; Rect 1 - ; Rect-2 - ; - ; [Group] - ; page1 / Group - ; - (let [[[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (thp/id :frame1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Group + ;; Rect 1 + ;; Rect-2 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [[[group shape1 shape2] + [c-group c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + (thp/id :frame1)) - file (wsh/get-local-file new-state)] + file (wsh/get-local-file new-state)] - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect-2")) - (t/is (= (:name group) "Board")) - (t/is (= (:name component) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect-2")) - (t/is (= (:name c-group) "Board")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect-2")) + (t/is (= (:name group) "Board")) + (t/is (= (:name component) "Board")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect-2")) + (t/is (= (:name c-group) "Board")) - (thl/is-from-file group file))))] + (thl/is-from-file group file))))] - (ptk/emit! + (ptk/emit! store (dw/select-shape (thp/id :frame1)) (dwl/add-component) @@ -172,60 +175,60 @@ (t/deftest test-add-component-from-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)])) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[instance1 shape1] - [c-instance1 c-shape1] - component1] - (thl/resolve-instance-and-main - new-state - (thp/id :main1) - true) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[instance1 shape1] + [c-instance1 c-shape1] + component1] + (thl/resolve-instance-and-main + new-state + (thp/id :main1) + true) - [[instance2 instance1' shape1'] - [c-instance2 c-instance1' c-shape1'] - component2] - (thl/resolve-instance-and-main - new-state - (:parent-id instance1))] + [[instance2 instance1' shape1'] + [c-instance2 c-instance1' c-shape1'] + component2] + (thl/resolve-instance-and-main + new-state + (:parent-id instance1))] - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name component1) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name component1) "Rect 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name shape1') "Rect 1")) - (t/is (= (:name instance1') "Rect 1")) - (t/is (= (:name instance2) "Rect 1")) - (t/is (= (:name component2) "Rect 1")) - (t/is (= (:name c-shape1') "Rect 1")) - (t/is (= (:name c-instance1') "Rect 1")) - (t/is (= (:name c-instance2) "Rect 1")))))] + (t/is (= (:name shape1') "Rect 1")) + (t/is (= (:name instance1') "Rect 1")) + (t/is (= (:name instance2) "Rect 1")) + (t/is (= (:name component2) "Rect 1")) + (t/is (= (:name c-shape1') "Rect 1")) + (t/is (= (:name c-instance1') "Rect 1")) + (t/is (= (:name c-instance2) "Rect 1")))))] - (ptk/emit! + (ptk/emit! store (dw/select-shape (thp/id :main1)) (dwl/add-component) @@ -233,358 +236,356 @@ (t/deftest test-rename-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)])) - main1 (thp/get-shape state :main1) + main1 (thp/get-shape state :main1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; - ; [Renamed component] - ; page1 / Rect 1 - ; - (let [libs (wsh/get-libraries new-state) - component (ctf/get-component libs - (:component-file main1) - (:component-id main1))] - (t/is (= (:name component) - "Renamed component")))))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Renamed component] + ;; page1 / Rect 1 + ;; + (let [libs (wsh/get-libraries new-state) + component (ctf/get-component libs + (:component-file main1) + (:component-id main1))] + (t/is (= (:name component) + "Renamed component")))))] - (ptk/emit! - store - (dwl/rename-component (:component-id main1) "Renamed component") - :the/end)))) + (ptk/emit! + store + (dwl/rename-component (:component-id main1) "Renamed component") + :the/end)))) (t/deftest test-duplicate-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)])) - main1 (thp/get-shape state :main1) - component-id (:component-id main1) + main1 (thp/get-shape state :main1) + component-id (:component-id main1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [new-component-id (->> (get-in new-state - [:workspace-data - :components]) - (keys) - (filter #(not= % component-id)) - (first)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [new-component-id (->> (get-in new-state + [:workspace-data + :components]) + (keys) + (filter #(not= % component-id)) + (first)) - [[_instance1 _shape1] - [_c-instance1 _c-shape1] - _component1] - (thl/resolve-instance-and-main - new-state - (:id main1)) + [[_instance1 _shape1] + [_c-instance1 _c-shape1] + _component1] + (thl/resolve-instance-and-main + new-state + (:id main1)) - [[_c-component2 _c-shape2] - component2] - (thl/resolve-component - new-state - new-component-id)] + [[_c-component2 _c-shape2] + component2] + (thl/resolve-component + new-state + new-component-id)] - (t/is (= (:name component2) "Rect 1")))))] + (t/is (= (:name component2) "Rect 1")))))] - (ptk/emit! + (ptk/emit! store (dwl/duplicate-component thp/current-file-id component-id) :the/end)))) (t/deftest test-delete-component - (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + (t/async done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect {:name "Rect 1"}) + (thp/make-component :main1 :component1 [(thp/id :shape1)]) + (thp/instantiate-component :instance1 (thp/id :component1))) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> ? - ; Rect 1 ---> ? - ; - (let [[main1 shape1] - (thl/resolve-noninstance - new-state - (thp/id :main1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> ? + ;; Rect 1 ---> ? + ;;; + (let [ + [main1 shape1] + (thl/resolve-noninstance + new-state + (thp/id :main1)) - [[instance1 shape2] [c-instance1 c-shape2] component1] - (thl/resolve-instance-and-main-allow-dangling - new-state - (thp/id :instance1)) + [[instance1 shape2] [c-instance1 c-shape2] component1] + (thl/resolve-instance-and-main-allow-dangling + new-state + (thp/id :instance1)) - file (wsh/get-local-file new-state) - component2 (ctkl/get-component file (thp/id :component1)) - component3 (ctkl/get-deleted-component file (thp/id :component1)) + file (wsh/get-local-file new-state) + component2 (ctkl/get-component file (thp/id :component1)) + component3 (ctkl/get-deleted-component file (thp/id :component1)) - saved-objects (:objects component3) - saved-main1 (get saved-objects (:shape-ref instance1)) - saved-shape2 (get saved-objects (:shape-ref shape2))] + saved-objects (:objects component3) + saved-main1 (get saved-objects (:shape-ref instance1)) + saved-shape2 (get saved-objects (:shape-ref shape2))] - (t/is (nil? main1)) - (t/is (nil? shape1)) + (t/is (nil? main1)) + (t/is (nil? shape1)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:touched instance1) nil)) - (t/is (not= (:shape-ref instance1) nil)) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:touched shape2) nil)) - (t/is (not= (:shape-ref shape2) nil)) - (t/is (nil? c-instance1)) - (t/is (nil? c-shape2)) - (t/is (nil? component1)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:touched instance1) nil)) + (t/is (not= (:shape-ref instance1) nil)) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:touched shape2) nil)) + (t/is (not= (:shape-ref shape2) nil)) + (t/is (nil? c-instance1)) + (t/is (nil? c-shape2)) + (t/is (nil? component1)) - (t/is (nil? component2)) + (t/is (nil? component2)) - (t/is (= (:name component3) "Rect 1")) - (t/is (= (:deleted component3) true)) - (t/is (some? (:objects component3))) + (t/is (= (:name component3) "Rect 1")) + (t/is (= (:deleted component3) true)) + (t/is (some? (:objects component3))) - (t/is (= (:name saved-main1) "Rect 1")) - (t/is (= (:name saved-shape2) "Rect 1")))))] + (t/is (= (:name saved-main1) "Rect 1")) + (t/is (= (:name saved-shape2) "Rect 1")) - (ptk/emit! - store - (dwl/delete-component {:id (thp/id :component1)}) - :the/end)))) + ))) + ] + (ptk/emit! store + (dwl/delete-component {:id (thp/id :component1)}) + :the/end)))) (t/deftest test-restore-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; Rect 1 - ; Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[[instance1 shape2] [c-instance1 c-shape2] component1] - (thl/resolve-instance-and-main - new-state - (thp/id :instance1)) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[[instance1 shape2] [c-instance1 c-shape2] component1] + (thl/resolve-instance-and-main + new-state + (thp/id :instance1)) - file (wsh/get-local-file new-state) - component2 (ctkl/get-component file (thp/id :component1)) + file (wsh/get-local-file new-state) + component2 (ctkl/get-component file (thp/id :component1)) - saved-objects (:objects component2)] + saved-objects (:objects component2)] - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")) - (t/is (some? component1)) - (t/is (some? component2)) - (t/is (nil? saved-objects)))))] + (t/is (some? component1)) + (t/is (some? component2)) + (t/is (nil? saved-objects)))))] - (ptk/emit! - store - (dwl/delete-component {:id (thp/id :component1)}) - (dwl/restore-component thp/current-file-id (thp/id :component1)) - :the/end)))) + (ptk/emit! + store + (dwl/delete-component {:id (thp/id :component1)}) + (dwl/restore-component thp/current-file-id (thp/id :component1)) + :the/end)))) (t/deftest test-instantiate-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)])) - file (wsh/get-local-file state) - component-id (thp/id :component1) - main1 (thp/get-shape state :main1) + file (wsh/get-local-file state) + component-id (thp/id :component1) + main1 (thp/get-shape state :main1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [new-instance-id (-> new-state + wsh/lookup-selected + first) - [[instance1 shape2] - [c-instance1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] + [[instance1 shape2] + [c-instance1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + new-instance-id)] - (t/is (not= (:id main1) (:id instance1))) - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file instance1) - thp/current-file-id)))))] + (t/is (not= (:id main1) (:id instance1))) + (t/is (= (:id component) component-id)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:component-file instance1) + thp/current-file-id)))))] - (ptk/emit! - store - (dwl/instantiate-component (:id file) - component-id - (gpt/point 100 100)) - :the/end)))) + (ptk/emit! + store + (dwl/instantiate-component (:id file) + component-id + (gpt/point 100 100)) + :the/end)))) (t/deftest test-instantiate-component-from-lib (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page)) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page)) - library-id (thp/id :lib1) - component-id (thp/id :component1) + library-id (thp/id :lib1) + component-id (thp/id :component1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + (let [new-instance-id (-> new-state + wsh/lookup-selected + first) - [[instance1 shape2] - [c-instance1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] + [[instance1 shape2] + [c-instance1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + new-instance-id)] - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file instance1) library-id)))))] + (t/is (= (:id component) component-id)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:component-file instance1) library-id)))))] - (ptk/emit! - store - (dwl/instantiate-component library-id - component-id - (gpt/point 100 100)) - :the/end)))) + (ptk/emit! + store + (dwl/instantiate-component library-id + component-id + (gpt/point 100 100)) + :the/end)))) (t/deftest test-detach-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/instantiate-component :instance1 - (thp/id :component1))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/instantiate-component :instance1 + (thp/id :component1))) - instance1 (thp/get-shape state :instance1) + instance1 (thp/get-shape state :instance1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 - ; Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [[instance2 shape1] - (thl/resolve-noninstance - new-state - (:id instance1))] + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [[instance2 shape1] + (thl/resolve-noninstance + new-state + (:id instance1))] - (t/is (some? instance2)) - (t/is (some? shape1)))))] + (t/is (some? instance2)) + (t/is (some? shape1)))))] - (ptk/emit! - store - (dwl/detach-component (:id instance1)) - :the/end)))) + (ptk/emit! + store + (dwl/detach-component (:id instance1)) + :the/end)))) (t/deftest test-add-nested-component (t/async @@ -595,167 +596,167 @@ {:name "Rect 1"})) store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Group - ; Rect 1 - ; Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [page (thp/current-page new-state) - shape1 (thp/get-shape new-state :shape1) - parent1 (ctn/get-shape page (:parent-id shape1)) + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Group + ;; Rect 1 + ;; Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [page (thp/current-page new-state) + shape1 (thp/get-shape new-state :shape1) + parent1 (ctn/get-shape page (:parent-id shape1)) - [[group shape1 shape2] - [c-group c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - (:parent-id parent1))] + [[group shape1 shape2] + [c-group c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + (:parent-id parent1))] - (t/is (= (:name group) "Board")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name component) "Board")) - (t/is (= (:name c-group) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")))))] + (t/is (= (:name group) "Board")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name component) "Board")) + (t/is (= (:name c-group) "Board")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")))))] (ptk/emit! - store - (dw/select-shape (thp/id :shape1)) - (dwl/add-component) - (dwsh/create-artboard-from-selection) - (dwl/add-component) - :the/end)))) + store + (dw/select-shape (thp/id :shape1)) + (dwl/add-component) + (dwsh/create-artboard-from-selection) + (dwl/add-component) + :the/end)))) (t/deftest test-instantiate-nested-component (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/make-component :main2 :component-2 - [(thp/id :main1)])) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/make-component :main2 :component-2 + [(thp/id :main1)])) - file (wsh/get-local-file state) - main1 (thp/get-shape state :main1) - main2 (thp/get-shape state :main2) - component-id (:component-id main2) + file (wsh/get-local-file state) + main1 (thp/get-shape state :main1) + main2 (thp/get-shape state :main2) + component-id (:component-id main2) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Rect 1 - ; Rect 1 - ; Rect 1 - ; Rect 1 #--> Rect 1 - ; Rect 1 @--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - ; [Rect 1] - ; page1 / Rect 1 - ; - (let [new-instance-id (-> new-state - wsh/lookup-selected - first) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 + ;; Rect 1 #--> Rect 1 + ;; Rect 1 @--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + ;; [Rect 1] + ;; page1 / Rect 1 + ;; + (let [new-instance-id (-> new-state + wsh/lookup-selected + first) - [[instance1 shape1 shape2] - [c-instance1 c-shape1 c-shape2] - component] - (thl/resolve-instance-and-main - new-state - new-instance-id)] + [[instance1 shape1 shape2] + [c-instance1 c-shape1 c-shape2] + component] + (thl/resolve-instance-and-main + new-state + new-instance-id)] - ; TODO: get and check the instance inside component [Rect-2] + ;; TODO: get and check the instance inside component [Rect-2] - (t/is (not= (:id main1) (:id instance1))) - (t/is (= (:id component) component-id)) - (t/is (= (:name instance1) "Rect 1")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-instance1) "Rect 1")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")))))] + (t/is (not= (:id main1) (:id instance1))) + (t/is (= (:id component) component-id)) + (t/is (= (:name instance1) "Rect 1")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-instance1) "Rect 1")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")))))] - (ptk/emit! - store - (dwl/instantiate-component (:id file) - (:component-id main2) - (gpt/point 100 100)) - :the/end)))) + (ptk/emit! + store + (dwl/instantiate-component (:id file) + (:component-id main2) + (gpt/point 100 100)) + :the/end)))) (t/deftest test-instantiate-nested-component-from-lib (t/async - done - (let [state (-> thp/initial-state - (thp/sample-page) - (thp/sample-shape :shape1 :rect - {:name "Rect 1"}) - (thp/make-component :main1 :component1 - [(thp/id :shape1)]) - (thp/move-to-library :lib1 "Library 1") - (thp/sample-page) - (thp/instantiate-component :instance1 - (thp/id :component1) - (thp/id :lib1))) + done + (let [state (-> thp/initial-state + (thp/sample-page) + (thp/sample-shape :shape1 :rect + {:name "Rect 1"}) + (thp/make-component :main1 :component1 + [(thp/id :shape1)]) + (thp/move-to-library :lib1 "Library 1") + (thp/sample-page) + (thp/instantiate-component :instance1 + (thp/id :component1) + (thp/id :lib1))) - file (wsh/get-local-file state) - library-id (thp/id :lib1) + file (wsh/get-local-file state) + library-id (thp/id :lib1) - store (the/prepare-store state done - (fn [new-state] - ; Expected shape tree: - ; - ; [Page] - ; Root Frame - ; Group - ; Rect 1 #--> Rect 1 - ; Rect 1 ---> Rect 1 - ; - ; [Group] - ; page1 / Group - ; - (let [instance1 (thp/get-shape new-state :instance1) + store (the/prepare-store state done + (fn [new-state] + ;; Expected shape tree: + ;; + ;; [Page] + ;; Root Frame + ;; Group + ;; Rect 1 #--> Rect 1 + ;; Rect 1 ---> Rect 1 + ;; + ;; [Group] + ;; page1 / Group + ;; + (let [instance1 (thp/get-shape new-state :instance1) - [[group1 shape1 shape2] [c-group1 c-shape1 c-shape2] _component] - (thl/resolve-instance-and-main - new-state - (:parent-id instance1))] + [[group1 shape1 shape2] [c-group1 c-shape1 c-shape2] _component] + (thl/resolve-instance-and-main + new-state + (:parent-id instance1))] - (t/is (= (:name group1) "Board")) - (t/is (= (:name shape1) "Rect 1")) - (t/is (= (:name shape2) "Rect 1")) - (t/is (= (:name c-group1) "Board")) - (t/is (= (:name c-shape1) "Rect 1")) - (t/is (= (:name c-shape2) "Rect 1")) - (t/is (= (:component-file group1) thp/current-file-id)) - (t/is (= (:component-file shape1) library-id)) - (t/is (= (:component-file shape2) nil)) - (t/is (= (:component-file c-group1) (:id file))) - (t/is (= (:component-file c-shape1) library-id)) - (t/is (= (:component-file c-shape2) nil)))))] + (t/is (= (:name group1) "Board")) + (t/is (= (:name shape1) "Rect 1")) + (t/is (= (:name shape2) "Rect 1")) + (t/is (= (:name c-group1) "Board")) + (t/is (= (:name c-shape1) "Rect 1")) + (t/is (= (:name c-shape2) "Rect 1")) + (t/is (= (:component-file group1) thp/current-file-id)) + (t/is (= (:component-file shape1) library-id)) + (t/is (= (:component-file shape2) nil)) + (t/is (= (:component-file c-group1) (:id file))) + (t/is (= (:component-file c-shape1) library-id)) + (t/is (= (:component-file c-shape2) nil)))))] - (ptk/emit! - store - (dw/select-shape (thp/id :instance1)) - (dwsh/create-artboard-from-selection) - (dwl/add-component) - :the/end)))) + (ptk/emit! + store + (dw/select-shape (thp/id :instance1)) + (dwsh/create-artboard-from-selection) + (dwl/add-component) + :the/end)))) diff --git a/frontend/test/frontend_tests/util_snap_data_test.cljs b/frontend/test/frontend_tests/util_snap_data_test.cljs index f0a656cd08..2eafa7a874 100644 --- a/frontend/test/frontend_tests/util_snap_data_test.cljs +++ b/frontend/test/frontend_tests/util_snap_data_test.cljs @@ -6,8 +6,9 @@ (ns frontend-tests.util-snap-data-test (:require - [app.common.file-builder :as fb] + [app.common.files.builder :as fb] [app.common.uuid :as uuid] + [app.common.types.shape :as cts] [app.util.snap-data :as sd] [cljs.pprint :refer [pprint]] [cljs.test :as t :include-macros true])) @@ -180,17 +181,17 @@ (fb/close-artboard)) shape-id (:last-id file) - page (fb/get-current-page file) + page (fb/get-current-page file) ;; frame-id (:last-id file) - data (-> (sd/make-snap-data) - (sd/add-page page)) + data (-> (sd/make-snap-data) + (sd/add-page page)) - file (-> file - (fb/delete-object shape-id)) + file (-> file + (fb/delete-object shape-id)) new-page (fb/get-current-page file) - data (sd/update-page data page new-page) + data (sd/update-page data page new-page) result-x (sd/query data (:id page) uuid/zero :x [0 100]) result-y (sd/query data (:id page) uuid/zero :y [0 100])] @@ -332,18 +333,20 @@ :height 100}) (fb/close-artboard)) - frame-id (:last-id file) - page (fb/get-current-page file) - data (-> (sd/make-snap-data) (sd/add-page page)) + frame-id (:last-id file) + page (fb/get-current-page file) + data (-> (sd/make-snap-data) (sd/add-page page)) - frame (fb/lookup-shape file frame-id) + frame (fb/lookup-shape file frame-id) new-frame (-> frame - (assoc :x 200 :y 200)) + (dissoc :selrect :points) + (assoc :x 200 :y 200) + (cts/setup-shape)) - file (fb/update-object file frame new-frame) - new-page (fb/get-current-page file) + file (fb/update-object file frame new-frame) + new-page (fb/get-current-page file) - data (sd/update-page data page new-page) + data (sd/update-page data page new-page) result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100]) @@ -371,6 +374,7 @@ shape (fb/lookup-shape file shape-id) new-shape (-> shape + (dissoc :selrect :points) (assoc :x 200 :y 200)) file (fb/update-object file shape new-shape)