Compare commits

...
32 Commits
Author SHA1 Message Date
Andrey Antukh b3ae54775b 📎 Update changelog 2025-10-14 12:26:56 +02:00
Andrey Antukh 11ff64b362 Merge tag '2.10.1-RC3' 2025-10-06 12:12:27 +02:00
Alonso Torres d5b743c604 🐛 Fix problem with text in plugins (#7446) 2025-10-06 11:19:33 +02:00
Andrey Antukh e38dd21307 ⬆️ Update exported dependencies 2025-10-06 10:56:04 +02:00
Andrey Antukh 8c91109c63 📚 Update changelog 2025-10-06 09:38:01 +02:00
Andrey Antukh c3eabbdb25 📎 Enable the fdata/objects-map feature by default
Using config flags
2025-10-06 09:38:01 +02:00
Andrey Antukh 67661674e2 Make deleted fonts fixer to run with more granular stragegy
Instead of running it on all the file, only run it to local library
and the current page, reducing considerably the overhead of analyzing
the whole file on each file load.

It stills executes for page each time the page is loaded, and add
some kind of local cache for not doing repeated work each time page
loads is pending to be implemented in other commit.
2025-10-06 09:38:01 +02:00
Andrey Antukh c70e7f3876 Add logging to frontend repo namespace 2025-10-06 09:38:01 +02:00
Andrey Antukh 0295f0f7c8 Add better workspace file indexing strategy
Improve file indexes initialization on workspace.

Instead of initialize indexes for all pages only initialize
indexes for the loaded page.
2025-10-06 09:38:01 +02:00
Andrey Antukh 54bb879cb6 📎 Add several missing imports on repl related namespaces 2025-10-06 09:38:01 +02:00
Andrey Antukh b72704e54b Set explicit no-buffering for sse responses 2025-10-06 09:19:48 +02:00
Yamila Moreno 6c6ec7a620 Merge pull request #7433 from penpot/yms-improve-self-host-documentation
📚 Improve self-host documentation
2025-10-03 13:24:37 +02:00
Yamila Moreno 21a7d30c5e 📚 Improve self-host documentation 2025-10-03 13:21:06 +02:00
Andrey Antukh b90aba0f95 Merge tag '2.10.0' 2025-10-02 12:37:58 +02:00
Andrey Antukh 01ef55e4f4 Revert " Add minor improvement to cljs impl logging"
This reverts commit 960b76f760.
2025-10-01 10:48:24 +02:00
Andrey Antukh 3b81c1d750 Revert "♻️ Make the exporter build as esm module"
This reverts commit d0f34f06a9.
2025-10-01 10:47:47 +02:00
Andrey Antukh d0f34f06a9 ♻️ Make the exporter build as esm module 2025-10-01 08:10:37 +02:00
Andrey Antukh 23d5bdd20b 🐛 Add missing poppler-tools dependency on devenv 2025-10-01 08:10:37 +02:00
Andrey Antukh 9f2dc06c95 Add missing srepl helper for disable objects-map feat 2025-10-01 08:10:37 +02:00
Andrey Antukh 62563d28d0 📎 Bump library version to 1.0.9
Mainly fixes dependencies declaration on package.json file
2025-09-30 21:55:17 +02:00
Andrey Antukh 21e2ee9904 🐛 Fix dependencies on library 2025-09-30 21:53:04 +02:00
Andrey Antukh c363d4d937 📎 Bump library version 2025-09-29 13:44:14 +02:00
Andrey Antukh 2d364dde5c Add several minor enhacements to features subsystem
Mainly fixes the team non-inheritable features handling and
removes unnecesary/duplicate checks.
2025-09-29 13:23:16 +02:00
Andrey Antukh c892a9f254 Integrate objects-map usage on backend and frontend 2025-09-29 13:23:16 +02:00
Andrey Antukh aaae35fb51 🎉 Add multiplatform impl of ObjectsMap
The new type get influentiated by the ObjectsMap impl on backend
code but with simplier implementation that no longer restricts keys
to UUID type but preserves the same performance characteristics.

This type encodes and decodes correctly both in fressian (backend)
and transit (backend and frontend).

This is an initial implementation and several memory usage
optimizations are still missing.
2025-09-29 13:23:16 +02:00
Andrey Antukh 960b76f760 Add minor improvement to cljs impl logging
Mainly reduce the emmited code, that will contribute to reduce the
bundle size and also adds timestamp to the default output.
2025-09-29 13:23:16 +02:00
Andrey Antukh d921e7eaa3 📎 Add not-empty generator to schema generator ns 2025-09-29 13:23:16 +02:00
andrés gonzález d1607fbe54 💄 Update Help Center images (#7266) 2025-09-29 11:54:47 +02:00
andrés gonzález cce1dd86a2 💄 Change variants video source to peertube (#7387) 2025-09-26 10:21:41 +02:00
andrés gonzález 58c6c94cb8 📚 Update boards info at the user guide (#7383) 2025-09-25 16:36:35 +02:00
andrés gonzález ecee7ecfc7 📚 Update workspace info at the user guide (#7376) 2025-09-25 12:24:59 +02:00
andrés gonzález b770145436 💄 Update variants video at the user guide (#7363) 2025-09-24 13:41:20 +02:00
82 changed files with 1271 additions and 552 deletions

No files matched your search

+13 -1
View File
@@ -1,6 +1,18 @@
# CHANGELOG # CHANGELOG
## 2.10.0 (Unreleased) ## 2.10.1
### :sparkles: New features & Enhancements
- Improve workpace file loading [Github 7366](https://github.com/penpot/penpot/pull/7366)
### :bug: Bugs fixed
- Fix regression with text shapes creation with Plugins API [Taiga #12244](https://tree.taiga.io/project/penpot/issue/12244)
## 2.10.0
### :rocket: Epics and highlights ### :rocket: Epics and highlights
+1
View File
@@ -35,6 +35,7 @@
[app.util.blob :as blob] [app.util.blob :as blob]
[clj-async-profiler.core :as prof] [clj-async-profiler.core :as prof]
[clojure.contrib.humanize :as hum] [clojure.contrib.humanize :as hum]
[clojure.datafy :refer [datafy]]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.pprint :refer [pprint print-table]] [clojure.pprint :refer [pprint print-table]]
[clojure.repl :refer :all] [clojure.repl :refer :all]
+19 -6
View File
@@ -12,12 +12,13 @@
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.files.migrations :as fmg] [app.common.files.migrations :as fmg]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.types.objects-map :as omap]
[app.common.types.path :as path] [app.common.types.path :as path]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.storage :as sto] [app.storage :as sto]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.objects-map :as omap] [app.util.objects-map :as omap.legacy]
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[app.worker :as wrk] [app.worker :as wrk]
[promesa.exec :as px])) [promesa.exec :as px]))
@@ -38,10 +39,7 @@
[file & _opts] [file & _opts]
(let [update-page (let [update-page
(fn [page] (fn [page]
(if (and (pmap/pointer-map? page) (update page :objects omap/wrap))
(not (pmap/loaded? page)))
page
(update page :objects omap/wrap)))
update-data update-data
(fn [fdata] (fn [fdata]
@@ -51,6 +49,20 @@
(update :data update-data) (update :data update-data)
(update :features conj "fdata/objects-map")))) (update :features conj "fdata/objects-map"))))
(defn disable-objects-map
[file & _opts]
(let [update-page
(fn [page]
(update page :objects #(into {} %)))
update-data
(fn [fdata]
(update fdata :pages-index d/update-vals update-page))]
(-> file
(update :data update-data)
(update :features disj "fdata/objects-map"))))
(defn process-objects (defn process-objects
"Apply a function to all objects-map on the file. Usualy used for convert "Apply a function to all objects-map on the file. Usualy used for convert
the objects-map instances to plain maps" the objects-map instances to plain maps"
@@ -60,7 +72,8 @@
(fn [page] (fn [page]
(update page :objects (update page :objects
(fn [objects] (fn [objects]
(if (omap/objects-map? objects) (if (or (omap/objects-map? objects)
(omap.legacy/objects-map? objects))
(update-fn objects) (update-fn objects)
objects))))) objects)))))
fdata)) fdata))
+2 -1
View File
@@ -44,7 +44,8 @@
(def default-headers (def default-headers
{"Content-Type" "text/event-stream;charset=UTF-8" {"Content-Type" "text/event-stream;charset=UTF-8"
"Cache-Control" "no-cache, no-store, max-age=0, must-revalidate" "Cache-Control" "no-cache, no-store, max-age=0, must-revalidate"
"Pragma" "no-cache"}) "Pragma" "no-cache"
"X-Accel-Buffering" "no"})
(defn response (defn response
[handler & {:keys [buf] :or {buf 32} :as opts}] [handler & {:keys [buf] :or {buf 32} :as opts}]
+18 -8
View File
@@ -342,14 +342,24 @@
(cfeat/check-client-features! (:features params)) (cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file))) (cfeat/check-file-features! (:features file)))
;; This operation is needed for backward comapatibility with frontends that (as-> file file
;; does not support pointer-map resolution mechanism; this just resolves the ;; This operation is needed for backward comapatibility with
;; pointers on backend and return a complete file. ;; frontends that does not support pointer-map resolution
(if (and (contains? (:features file) "fdata/pointer-map") ;; mechanism; this just resolves the pointers on backend and
(not (contains? (:features params) "fdata/pointer-map"))) ;; return a complete file
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (if (and (contains? (:features file) "fdata/pointer-map")
(update file :data feat.fdata/process-pointers deref)) (not (contains? (:features params) "fdata/pointer-map")))
file)))) (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(update file :data feat.fdata/process-pointers deref))
file)
;; This operation is needed for backward comapatibility with
;; frontends that does not support objects-map mechanism; this
;; just converts all objects map instaces to plain maps
(if (and (contains? (:features file) "fdata/objects-map")
(not (contains? (:features params) "fdata/objects-map")))
(update file :data feat.fdata/process-objects (partial into {}))
file)))))
;; --- COMMAND QUERY: get-file-fragment (by id) ;; --- COMMAND QUERY: get-file-fragment (by id)
@@ -112,14 +112,15 @@
;; FIXME: IMPORTANT: this code can have race conditions, because ;; FIXME: IMPORTANT: this code can have race conditions, because
;; we have no locks for updating team so, creating two files ;; we have no locks for updating team so, creating two files
;; concurrently can lead to lost team features updating ;; concurrently can lead to lost team features updating
(when-let [features (-> features (when-let [features (-> features
(set/difference (:features team)) (set/difference (:features team))
(set/difference cfeat/no-team-inheritable-features) (set/difference cfeat/no-team-inheritable-features)
(not-empty))] (not-empty))]
(let [features (->> features (let [features (-> features
(set/union (:features team)) (set/union (:features team))
(db/create-array conn "text"))] (set/difference cfeat/no-team-inheritable-features)
(into-array))]
(db/update! conn :team (db/update! conn :team
{:features features} {:features features}
{:id (:id team)} {:id (:id team)}
@@ -160,7 +160,6 @@
tpoint (ct/tpoint)] tpoint (ct/tpoint)]
(when (not= (:vern params) (when (not= (:vern params)
(:vern file)) (:vern file))
(ex/raise :type :validation (ex/raise :type :validation
@@ -183,15 +182,15 @@
(set/difference (:features team)) (set/difference (:features team))
(set/difference cfeat/no-team-inheritable-features) (set/difference cfeat/no-team-inheritable-features)
(not-empty))] (not-empty))]
(let [features (->> features (let [features (-> features
(set/union (:features team)) (set/union (:features team))
(db/create-array conn "text"))] (set/difference cfeat/no-team-inheritable-features)
(into-array))]
(db/update! conn :team (db/update! conn :team
{:features features} {:features features}
{:id (:id team)} {:id (:id team)}
{::db/return-keys false}))) {::db/return-keys false})))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)}) (mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(binding [l/*context* (some-> (meta params) (binding [l/*context* (some-> (meta params)
+1 -1
View File
@@ -503,7 +503,7 @@
(let [features (-> (cfeat/get-enabled-features cf/flags) (let [features (-> (cfeat/get-enabled-features cf/flags)
(set/difference cfeat/frontend-only-features) (set/difference cfeat/frontend-only-features)
(cfeat/check-client-features! (:features params))) (set/difference cfeat/no-team-inheritable-features))
params (-> params params (-> params
(assoc :profile-id profile-id) (assoc :profile-id profile-id)
(assoc :features features)) (assoc :features features))
+1
View File
@@ -40,6 +40,7 @@
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.datafy :refer [datafy]]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.pprint :refer [print-table]] [clojure.pprint :refer [print-table]]
[clojure.stacktrace :as strace] [clojure.stacktrace :as strace]
+8 -9
View File
@@ -67,11 +67,6 @@
"design-tokens/v1" "design-tokens/v1"
"variants/v1"}) "variants/v1"})
;; A set of features that should not be propagated to team on creating
;; or modifying a file
(def no-team-inheritable-features
#{"fdata/path-data"})
;; A set of features which only affects on frontend and can be enabled ;; A set of features which only affects on frontend and can be enabled
;; and disabled freely by the user any time. This features does not ;; and disabled freely by the user any time. This features does not
;; persist on file features field but can be permanently enabled on ;; persist on file features field but can be permanently enabled on
@@ -85,8 +80,14 @@
;; Features that are mainly backend only or there are a proper ;; Features that are mainly backend only or there are a proper
;; fallback when frontend reports no support for it ;; fallback when frontend reports no support for it
(def backend-only-features (def backend-only-features
#{"fdata/objects-map" #{"fdata/pointer-map"
"fdata/pointer-map"}) "fdata/objects-map"})
;; A set of features that should not be propagated to team on creating
;; or modifying a file or creating or modifying a team
(def no-team-inheritable-features
#{"fdata/path-data"
"fdata/shape-data-type"})
;; This is a set of features that does not require an explicit ;; This is a set of features that does not require an explicit
;; migration like components/v2 or the migration is not mandatory to ;; migration like components/v2 or the migration is not mandatory to
@@ -222,8 +223,6 @@
:hint (str/ffmt "enabled feature '%' not present in file (missing migration)" :hint (str/ffmt "enabled feature '%' not present in file (missing migration)"
not-supported))) not-supported)))
(check-supported-features! file-features)
;; Components v1 is deprecated ;; Components v1 is deprecated
(when-not (contains? file-features "components/v2") (when-not (contains? file-features "components/v2")
(ex/raise :type :restriction (ex/raise :type :restriction
+36 -20
View File
@@ -6,14 +6,29 @@
(ns app.common.files.indices (ns app.common.files.indices
(:require (:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.uuid :as uuid])) [app.common.uuid :as uuid]))
(defn- generate-index
"An optimized algorithm for calculate parents index that walk from top
to down starting from a provided shape-id. Usefull when you want to
create an index for the whole objects or subpart of the tree."
[index objects shape-id parents]
(let [shape (get objects shape-id)
index (assoc index shape-id parents)
parents (cons shape-id parents)]
(reduce (fn [index shape-id]
(generate-index index objects shape-id parents))
index
(:shapes shape))))
(defn generate-child-all-parents-index (defn generate-child-all-parents-index
"Creates an index where the key is the shape id and the value is a set "Creates an index where the key is the shape id and the value is a set
with all the parents" with all the parents"
([objects] ([objects]
(generate-child-all-parents-index objects (vals objects))) (generate-index {} objects uuid/zero []))
([objects shapes] ([objects shapes]
(let [shape->entry (let [shape->entry
@@ -24,24 +39,25 @@
(defn create-clip-index (defn create-clip-index
"Retrieves the mask information for an object" "Retrieves the mask information for an object"
[objects parents-index] [objects parents-index]
(let [retrieve-clips (let [get-clip-parents
(fn [shape]
(let [shape-id (dm/get-prop shape :id)]
(cond-> []
(or (and (cfh/frame-shape? shape)
(not (:show-content shape))
(not= uuid/zero shape-id))
(cfh/bool-shape? shape))
(conj shape)
(:masked-group shape)
(conj (get objects (->> shape :shapes first))))))
xform
(comp (map (d/getf objects))
(mapcat get-clip-parents))
populate-with-clips
(fn [parents] (fn [parents]
(let [lookup-object (fn [id] (get objects id)) (into [] xform parents))]
get-clip-parents
(fn [shape]
(cond-> []
(or (and (= :frame (:type shape))
(not (:show-content shape))
(not= uuid/zero (:id shape)))
(cfh/bool-shape? shape))
(conj shape)
(:masked-group shape) (d/update-vals parents-index populate-with-clips)))
(conj (get objects (->> shape :shapes first)))))]
(into []
(comp (map lookup-object)
(mapcat get-clip-parents))
parents)))]
(-> parents-index
(update-vals retrieve-clips))))
+2 -1
View File
@@ -158,7 +158,8 @@
:enable-component-thumbnails :enable-component-thumbnails
:enable-render-wasm-dpr :enable-render-wasm-dpr
:enable-token-units :enable-token-units
:enable-token-typography-types]) :enable-token-typography-types
:enable-feature-fdata-objects-map])
(defn parse (defn parse
[& flags] [& flags]
+3 -1
View File
@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.schema.generators (ns app.common.schema.generators
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double]) (:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double not-empty])
#?(:cljs (:require-macros [app.common.schema.generators])) #?(:cljs (:require-macros [app.common.schema.generators]))
(:require (:require
[app.common.math :as mth] [app.common.math :as mth]
@@ -146,3 +146,5 @@
(def any (def any
(tg/one-of [text boolean double int keyword])) (tg/one-of [text boolean double int keyword]))
(def not-empty tg/not-empty)
@@ -0,0 +1,521 @@
;; 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.types.objects-map
"Implements a specialized map-like data structure for store an UUID =>
OBJECT mappings. The main purpose of this data structure is be able
to serialize it on fressian as byte-array and have the ability to
decode each field separatelly without the need to decode the whole
map from the byte-array.
It works transparently, so no aditional dynamic vars are needed. It
only works by reference equality and the hash-code is calculated
properly from each value."
(:require
#?(:clj [app.common.fressian :as fres])
#?(:clj [clojure.data.json :as json])
[app.common.transit :as t]
[clojure.core :as c]
[clojure.core.protocols :as cp])
#?(:clj
(:import
clojure.lang.Murmur3
clojure.lang.RT
java.util.Iterator)))
#?(:clj (set! *warn-on-reflection* true))
(declare create)
(declare ^:private do-compact)
(defprotocol IObjectsMap
(^:no-doc compact [this])
(^:no-doc get-data [this] "retrieve internal data")
(^:no-doc -hash-for-key [this key] "retrieve a hash for a key"))
#?(:cljs
(deftype ObjectsMapEntry [key omap]
c/IMapEntry
(-key [_] key)
(-val [_] (get omap key))
c/IHash
(-hash [_]
(-hash-for-key omap key))
c/IEquiv
(-equiv [this other]
(and (c/map-entry? other)
(= (key this)
(key other))
(= (val this)
(val other))))
c/ISequential
c/ISeqable
(-seq [this]
(cons key (lazy-seq (cons (c/-val this) nil))))
c/ICounted
(-count [_] 2)
c/IIndexed
(-nth [node n]
(cond (== n 0) key
(== n 1) (c/-val node)
:else (throw (js/Error. "Index out of bounds"))))
(-nth [node n not-found]
(cond (== n 0) key
(== n 1) (c/-val node)
:else not-found))
c/ILookup
(-lookup [node k]
(c/-nth node k nil))
(-lookup [node k not-found]
(c/-nth node k not-found))
c/IFn
(-invoke [node k]
(c/-nth node k))
(-invoke [node k not-found]
(c/-nth node k not-found))
c/IPrintWithWriter
(-pr-writer [this writer opts]
(c/pr-sequential-writer
writer
(fn [item w _]
(c/-write w (pr-str item)))
"[" ", " "]"
opts
this)))
:clj
(deftype ObjectsMapEntry [key omap]
clojure.lang.IMapEntry
(key [_] key)
(getKey [_] key)
(val [_]
(get omap key))
(getValue [_]
(get omap key))
clojure.lang.Indexed
(nth [node n]
(cond
(== n 0) key
(== n 1) (val node)
:else (throw (IllegalArgumentException. "Index out of bounds"))))
(nth [node n not-found]
(cond
(== n 0) key
(== n 1) (val node)
:else not-found))
clojure.lang.IPersistentCollection
(empty [_] [])
(count [_] 2)
(seq [this]
(cons key (lazy-seq (cons (val this) nil))))
(cons [this item]
(.cons ^clojure.lang.IPersistentCollection (vec this) item))
clojure.lang.IHashEq
(hasheq [_]
(-hash-for-key omap key))))
#?(:cljs
(deftype ObjectMapIterator [iterator omap]
Object
(hasNext [_]
(.hasNext ^js iterator))
(next [_]
(let [entry (.next iterator)]
(ObjectsMapEntry. (key entry) omap)))
(remove [_]
(js/Error. "Unsupported operation")))
:clj
(deftype ObjectsMapIterator [^Iterator iterator omap]
Iterator
(hasNext [_]
(.hasNext iterator))
(next [_]
(let [entry (.next iterator)]
(ObjectsMapEntry. (key entry) omap)))))
#?(:cljs
(deftype ObjectsMap [metadata cache
^:mutable data
^:mutable modified
^:mutable hash]
Object
(toString [this]
(pr-str* this))
(equiv [this other]
(c/-equiv this other))
(keys [this]
(c/es6-iterator (keys this)))
(entries [this]
(c/es6-entries-iterator (seq this)))
(values [this]
(es6-iterator (vals this)))
(has [this k]
(c/contains? this k))
(get [this k not-found]
(c/-lookup this k not-found))
(forEach [this f]
(run! (fn [[k v]] (f v k)) this))
cp/Datafiable
(datafy [_]
{:data data
:cache cache
:modified modified
:hash hash})
IObjectsMap
(compact [this]
(when modified
(do-compact data cache
(fn [data']
(set! (.-modified this) false)
(set! (.-data this) data'))))
this)
(get-data [this]
(compact this)
data)
(-hash-for-key [this key]
(if (c/-contains-key? cache key)
(c/-hash (c/-lookup cache key))
(c/-hash (c/-lookup this key))))
c/IWithMeta
(-with-meta [this new-meta]
(if (identical? new-meta meta)
this
(ObjectsMap. new-meta
cache
data
modified
hash)))
c/IMeta
(-meta [_] metadata)
c/ICloneable
(-clone [this]
(compact this)
(ObjectsMap. metadata {} data false nil))
c/IIterable
(-iterator [this]
(c/seq-iter this))
c/ICollection
(-conj [this entry]
(cond
(map-entry? entry)
(c/-assoc this (c/-key entry) (c/-val entry))
(vector? entry)
(c/-assoc this (c/-nth entry 0) (c/-nth entry 1))
:else
(loop [ret this es (seq entry)]
(if (nil? es)
ret
(let [e (first es)]
(if (vector? e)
(recur (c/-assoc ret (c/-nth e 0) (c/-nth e 1))
(next es))
(throw (js/Error. "conj on a map takes map entries or seqables of map entries"))))))))
c/IEmptyableCollection
(-empty [_]
(create))
c/IEquiv
(-equiv [this other]
(equiv-map this other))
c/IHash
(-hash [this]
(when-not hash
(set! hash (hash-unordered-coll this)))
hash)
c/ISeqable
(-seq [this]
(->> (keys data)
(map (fn [id] (new ObjectsMapEntry id this)))
(seq)))
c/ICounted
(-count [_]
(c/-count data))
c/ILookup
(-lookup [this k]
(or (c/-lookup cache k)
(if (c/-contains-key? data k)
(let [v (c/-lookup data k)
v (t/decode-str v)]
(set! (.-cache this) (c/-assoc cache k v))
v)
(do
(set! (.-cache this) (assoc cache key nil))
nil))))
(-lookup [this k not-found]
(if (c/-contains-key? data k)
(c/-lookup this k)
not-found))
c/IAssociative
(-assoc [_ k v]
(ObjectsMap. metadata
(c/-assoc cache k v)
(c/-assoc data k nil)
true
nil))
(-contains-key? [_ k]
(c/-contains-key? data k))
c/IFind
(-find [this k]
(when (c/-contains-key? data k)
(new ObjectsMapEntry k this)))
c/IMap
(-dissoc [_ k]
(ObjectsMap. metadata
(c/-dissoc cache k)
(c/-dissoc data k)
true
nil))
c/IKVReduce
(-kv-reduce [this f init]
(c/-kv-reduce data
(fn [init k _]
(f init k (c/-lookup this k)))
init))
c/IFn
(-invoke [this k]
(c/-lookup this k))
(-invoke [this k not-found]
(c/-lookup this k not-found))
c/IPrintWithWriter
(-pr-writer [this writer opts]
(c/pr-sequential-writer
writer
(fn [item w _]
(c/-write w (pr-str (c/-key item)))
(c/-write w \space)
(c/-write w (pr-str (c/-val item))))
"#penpot/objects-map {" ", " "}"
opts
(seq this))))
:clj
(deftype ObjectsMap [metadata cache
^:unsynchronized-mutable data
^:unsynchronized-mutable modified
^:unsynchronized-mutable hash]
Object
(hashCode [this]
(.hasheq ^clojure.lang.IHashEq this))
cp/Datafiable
(datafy [_]
{:data data
:cache cache
:modified modified
:hash hash})
IObjectsMap
(compact [this]
(locking this
(when modified
(do-compact data cache
(fn [data']
(set! (.-modified this) false)
(set! (.-data this) data')))))
this)
(get-data [this]
(compact this)
data)
(-hash-for-key [this key]
(if (contains? cache key)
(c/hash (get cache key))
(c/hash (get this key))))
json/JSONWriter
(-write [this writter options]
(json/-write (into {} this) writter options))
clojure.lang.IHashEq
(hasheq [this]
(when-not hash
(set! hash (Murmur3/hashUnordered this)))
hash)
clojure.lang.Seqable
(seq [this]
(RT/chunkIteratorSeq (.iterator ^Iterable this)))
java.lang.Iterable
(iterator [this]
(ObjectsMapIterator. (.iterator ^Iterable data) this))
clojure.lang.IPersistentCollection
(equiv [this other]
(and (instance? ObjectsMap other)
(= (count this) (count other))
(reduce-kv (fn [_ id _]
(let [this-val (get this id)
other-val (get other id)
result (= this-val other-val)]
(or result
(reduced false))))
true
data)))
clojure.lang.IPersistentMap
(cons [this o]
(if (map-entry? o)
(assoc this (key o) (val o))
(if (vector? o)
(assoc this (nth o 0) (nth o 1))
(throw (UnsupportedOperationException. "invalid arguments to cons")))))
(empty [_]
(create))
(containsKey [_ key]
(.containsKey ^clojure.lang.IPersistentMap data key))
(entryAt [this key]
(ObjectsMapEntry. this key))
(valAt [this key]
(or (get cache key)
(locking this
(if (contains? data key)
(let [value (get data key)
value (t/decode-str value)]
(set! (.-cache this) (assoc cache key value))
value)
(do
(set! (.-cache this) (assoc cache key nil))
nil)))))
(valAt [this key not-found]
(if (.containsKey ^clojure.lang.IPersistentMap data key)
(.valAt this key)
not-found))
(assoc [_ key val]
(ObjectsMap. metadata
(assoc cache key val)
(assoc data key nil)
true
nil))
(assocEx [_ _ _]
(throw (UnsupportedOperationException. "method not implemented")))
(without [_ key]
(ObjectsMap. metadata
(dissoc cache key)
(dissoc data key)
true
nil))
clojure.lang.Counted
(count [_]
(count data))))
#?(:cljs (es6-iterable ObjectsMap))
(defn- do-compact
[data cache update-fn]
(let [new-data
(persistent!
(reduce-kv (fn [data id obj]
(if (nil? obj)
(assoc! data id (t/encode-str (get cache id)))
data))
(transient data)
data))]
(update-fn new-data)
nil))
(defn from-data
[data]
(ObjectsMap. {} {}
data
false
nil))
(defn objects-map?
[o]
(instance? ObjectsMap o))
(defn create
([] (from-data {}))
([other]
(cond
(objects-map? other)
(-> other get-data from-data)
:else
(throw #?(:clj (UnsupportedOperationException. "invalid arguments")
:cljs (js/Error. "invalid arguments"))))))
(defn wrap
[objects]
(if (instance? ObjectsMap objects)
objects
(->> objects
(into (create))
(compact))))
#?(:clj
(fres/add-handlers!
{:name "penpot/objects-map/v2"
:class ObjectsMap
:wfn (fn [n w o]
(fres/write-tag! w n)
(fres/write-object! w (get-data o)))
:rfn (fn [r]
(-> r fres/read-object! from-data))}))
(t/add-handlers!
{:id "penpot/objects-map/v2"
:class ObjectsMap
:wfn get-data
:rfn from-data})
+4 -2
View File
@@ -41,6 +41,7 @@
[common-tests.types.components-test] [common-tests.types.components-test]
[common-tests.types.fill-test] [common-tests.types.fill-test]
[common-tests.types.modifiers-test] [common-tests.types.modifiers-test]
[common-tests.types.objects-map-test]
[common-tests.types.path-data-test] [common-tests.types.path-data-test]
[common-tests.types.shape-decode-encode-test] [common-tests.types.shape-decode-encode-test]
[common-tests.types.shape-interactions-test] [common-tests.types.shape-interactions-test]
@@ -90,9 +91,10 @@
'common-tests.time-test 'common-tests.time-test
'common-tests.types.absorb-assets-test 'common-tests.types.absorb-assets-test
'common-tests.types.components-test 'common-tests.types.components-test
'common-tests.types.modifiers-test
'common-tests.types.path-data-test
'common-tests.types.fill-test 'common-tests.types.fill-test
'common-tests.types.modifiers-test
'common-tests.types.objects-map-test
'common-tests.types.path-data-test
'common-tests.types.shape-decode-encode-test 'common-tests.types.shape-decode-encode-test
'common-tests.types.shape-interactions-test 'common-tests.types.shape-interactions-test
'common-tests.types.tokens-lib-test 'common-tests.types.tokens-lib-test
@@ -0,0 +1,133 @@
;; 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.types.objects-map-test
(:require
#?(:clj [app.common.fressian :as fres])
[app.common.json :as json]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.schema.test :as smt]
[app.common.transit :as transit]
[app.common.types.objects-map :as omap]
[app.common.types.path :as path]
[app.common.types.plugins :refer [schema:plugin-data]]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]
[clojure.test :as t]))
(t/deftest basic-operations
(t/testing "assoc"
(let [id (uuid/custom 0 1)
id' (uuid/custom 0 2)
obj (-> (omap/create) (assoc id {:foo 1}))]
(t/is (not= id id'))
(t/is (not (contains? obj id')))
(t/is (contains? obj id))))
(t/testing "assoc-with-non-uuid-keys"
(let [obj (-> (omap/create)
(assoc :a {:foo 1})
(assoc :b {:bar 1}))]
(t/is (not (contains? obj :c)))
(t/is (contains? obj :a))
(t/is (contains? obj :b))))
(t/testing "dissoc"
(let [id (uuid/custom 0 1)
obj (-> (omap/create) (assoc id {:foo 1}))]
(t/is (contains? obj id))
(let [obj (dissoc obj id)]
(t/is (not (contains? obj id))))))
(t/testing "seq"
(let [id (uuid/custom 0 1)
obj (-> (omap/create) (assoc id 1))]
(t/is (contains? obj id))
(let [[entry] (seq obj)]
(t/is (map-entry? entry))
(t/is (= (key entry) id))
(t/is (= (val entry) 1)))))
(t/testing "cons & count"
(let [obj (into (omap/create) [[uuid/zero 1]])]
(t/is (contains? obj uuid/zero))
(t/is (= 1 (count obj)))
(t/is (omap/objects-map? obj))))
(t/testing "wrap"
(let [obj1 (omap/wrap {})
tmp (omap/create)
obj2 (omap/wrap tmp)]
(t/is (omap/objects-map? obj1))
(t/is (omap/objects-map? obj2))
(t/is (identical? tmp obj2))
(t/is (= 0 (count obj1)))
(t/is (= 0 (count obj2))))))
(t/deftest internal-state
(t/testing "modified & compact"
(let [obj (-> (omap/create)
(assoc :a 1)
(assoc :b 2))]
(t/is (= 2 (count obj)))
(t/is (-> obj datafy :modified))
(let [obj (omap/compact obj)]
(t/is (not (-> obj datafy :modified))))))
(t/testing "create from other"
(let [obj1 (-> (omap/create)
(assoc :a {:foo 1})
(assoc :b {:bar 2}))
obj2 (omap/create obj1)]
(t/is (not (identical? obj1 obj2)))
(t/is (= obj1 obj2))
(t/is (= (hash obj1) (hash obj2)))
(t/is (= (get obj1 :a) (get obj2 :a)))
(t/is (= (get obj1 :b) (get obj2 :b))))))
(t/deftest creation-and-duplication
(smt/check!
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
(sg/not-empty))]
(let [obj1 (omap/wrap data)
obj2 (omap/create obj1)]
(and (= (hash obj1) (hash obj2))
(= obj1 obj2))))
{:num 100}))
#?(:clj
(t/deftest fressian-encode-decode
(smt/check!
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
(sg/not-empty)
(sg/fmap omap/wrap)
(sg/fmap (fn [o] {:objects o})))]
(let [res (-> data fres/encode fres/decode)]
(and (contains? res :objects)
(omap/objects-map? (:objects res))
(= res data))))
{:num 100})))
(t/deftest transit-encode-decode
(smt/check!
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
(sg/not-empty)
(sg/fmap omap/wrap)
(sg/fmap (fn [o] {:objects o})))]
(let [res (-> data transit/encode-str transit/decode-str)]
;; (app.common.pprint/pprint data)
;; (app.common.pprint/pprint res)
(and (every? (fn [[k v]]
(= v (get-in data [:objects k])))
(:objects res))
(omap/objects-map? (:objects data))
(omap/objects-map? (:objects res)))))
{:num 100}))
+1
View File
@@ -310,6 +310,7 @@ RUN set -ex; \
fonts-wqy-zenhei \ fonts-wqy-zenhei \
fonts-tlwg-loma-otf \ fonts-tlwg-loma-otf \
fonts-freefont-ttf \ fonts-freefont-ttf \
poppler-utils \
\ \
libasound2t64 \ libasound2t64 \
libatk-bridge2.0-0t64 \ libatk-bridge2.0-0t64 \
-1
View File
@@ -118,7 +118,6 @@ http {
location /api { location /api {
proxy_pass http://127.0.0.1:6060/api; proxy_pass http://127.0.0.1:6060/api;
proxy_buffering off;
proxy_http_version 1.1; proxy_http_version 1.1;
} }
+1 -1
View File
@@ -16,7 +16,7 @@
title=title or metadata.title, title=title or metadata.title,
desc=desc or metadata.desc or description or metadata.description, desc=desc or metadata.desc or description or metadata.description,
url="https://help.penpot.app" + page.url, url="https://help.penpot.app" + page.url,
img="https://help.penpot.app/img/th-help-center.jpg", img="https://help.penpot.app/img/thumb-help-center.jpg",
img_alt=alt, img_alt=alt,
twitterHandle=twitter or metadata.twitter, twitterHandle=twitter or metadata.twitter,
name=name name=name
+1 -1
View File
@@ -7,7 +7,7 @@ eleventyNavigation:
--- ---
<div class="main-illus"> <div class="main-illus">
<img src="/img/home-contributing.png" alt="User guide" border="0"> <img src="/img/home-contribution.webp" alt="User guide" border="0">
</div> </div>
<h1 id="contributing-guide">Contributing guide.</h1> <h1 id="contributing-guide">Contributing guide.</h1>
+13 -13
View File
@@ -832,22 +832,22 @@ a[href].post-tag:visited {
padding-left: 14.6rem; padding-left: 14.6rem;
} }
.illus-userguide { .illus-userguide {
background-image: url(/img/home-userguide.png); background-image: url(/img/home-user-guide.webp);
} }
.illus-faq { .illus-faq {
background-image: url(/img/home-faq.png); background-image: url(/img/home-faq.webp);
} }
.illus-techguide { .illus-techguide {
background-image: url(/img/home-techguide.png); background-image: url(/img/home-technical-guide.webp);
} }
.illus-plugins { .illus-plugins {
background-image: url(/img/home-plugins.png); background-image: url(/img/home-plugins.webp);
} }
.illus-contributing { .illus-contributing {
background-image: url(/img/home-contributing.png); background-image: url(/img/home-contribution.webp);
} }
.illus-contact { .illus-contact {
background-image: url(/img/home-contact.png); background-image: url(/img/home-contact.webp);
} }
.illus-libraries { .illus-libraries {
padding-left: 13rem; padding-left: 13rem;
@@ -924,7 +924,7 @@ a[href].post-tag:visited {
background-size: auto 75%; background-size: auto 75%;
background-position: center top; background-position: center top;
background-repeat: no-repeat; background-repeat: no-repeat;
background-image: url(/img/home-contact.png); background-image: url(/img/home-contact.webp);
} }
.contact-block p, .contact-block p,
.contact-block h2, .contact-block h2,
@@ -992,22 +992,22 @@ a[href].post-tag:visited {
} }
.plugins .illus-getting-started { .plugins .illus-getting-started {
background-image: url(/img/plugins/getting_started.png); background-image: url(/img/plugins/plugins-getting-started.webp);
} }
.plugins .illus-create-plugin { .plugins .illus-create-plugin {
background-image: url(/img/plugins/create_plugin.png); background-image: url(/img/plugins/plugins-create.webp);
} }
.plugins .illus-deployment { .plugins .illus-deployment {
background-image: url(/img/plugins/deployment.png); background-image: url(/img/plugins/plugins-deploy.webp);
} }
.plugins .illus-api { .plugins .illus-api {
background-image: url(/img/plugins/api.png); background-image: url(/img/plugins/plugins-api.webp);
} }
.plugins .illus-examples { .plugins .illus-examples {
background-image: url(/img/plugins/examples.png); background-image: url(/img/plugins/plugins-examples-templates.webp);
} }
.plugins .illus-faq { .plugins .illus-faq {
background-image: url(/img/plugins/faqs.png); background-image: url(/img/plugins/plugins-faqs.webp);
} }
table, tr, th, td { table, tr, th, td {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+22 -12
View File
@@ -12,22 +12,32 @@ If you need more context you can look at the <a
href="https://community.penpot.app/t/self-hosting-penpot-i/2336" target="_blank">post href="https://community.penpot.app/t/self-hosting-penpot-i/2336" target="_blank">post
about self-hosting</a> in Penpot community. about self-hosting</a> in Penpot community.
**There is absolutely no difference between <a <strong>The experience stays the same, whether you use
href="https://design.penpot.app">our SaaS offer</a> for Penpot and your Penpot <a href="https://design.penpot.app" target="_blank">in the cloud</a>
self-hosted Penpot platform!** or self-hosted.</strong>
There are three main options for creating a Penpot instance:
1. Using the platform of our partner <a href="https://elest.io/open-source/penpot" target="_blank">Elestio</a>.
2. Using <a href="https://docker.com" target="_blank">Docker</a> tool.
3. Using <a href="https://kubernetes.io/" target="_blank">Kubernetes</a>.
<p class="advice"> <p class="advice">
The recommended way is to use Elestio, since it's simpler, fully automatic and still greatly flexible. Docker images are published shortly after the SaaS update:
Use Docker if you already know the tool, if need full control of the process or have extra requirements <a href="https://community.penpot.app/t/why-do-self-hosted-versions-lag-behind-new-releases/9897" target="_blank">Why do self hosted versions lag behind new releases?</a>
and do not want to depend on any external provider, or need to do any special customization.
</p> </p>
These are the main options to configure your Penpot instance:
1. Deploy with [docker compose][2]
2. Use Kubernetes in its different flavors:
- Deploy the [official Helm Chart][3]
- Deploy in [Openshift][4]
- Deploy in [Rancher][5]
3. Other official options:
- [Elestio][6]
- [Truenas][7]
Or you can try [other options][1], offered by Penpot community. Or you can try [other options][1], offered by Penpot community.
[1]: /technical-guide/getting-started/unofficial-options/ [1]: /technical-guide/getting-started/unofficial-options/
[2]: /technical-guide/getting-started/docker/
[3]: /technical-guide/getting-started/kubernetes/
[4]: /technical-guide/getting-started/kubernetes/#using-openshift%3F
[5]: /technical-guide/getting-started/kubernetes/#using-rancher%3F
[6]: /technical-guide/getting-started/elestio/
[7]: https://apps.truenas.com/catalog/penpot/
@@ -5,12 +5,7 @@ desc: Find guides for Docker, Kubernetes, and more in Penpot's Technical Guide f
# Unofficial self-host options # Unofficial self-host options
There are some other options, **NOT SUPPORTED BY PENPOT**: Other setups maintained by the community (not Penpot-supported):
* Install with <a href="https://community.penpot.app/t/how-to-develop-penpot-with-podman-penpotman/2113" target="_blank">Podman</a> instead of Docker. * Install with <a href="https://blog.linuxgrrl.com/2022/01/19/running-penpot-locally-docker-free-with-podman/" target="_blank">Podman</a> instead of Docker.
* Try the under development <a href="https://github.com/author-more/penpot-desktop/releases/latest" target="_blank">Penpot Desktop app</a>. * Try the <a href="https://community.penpot.app/t/penpot-desktop-road-to-1-0/7296" target="_blank">Penpot Desktop app</a>.
* Try a simple Kubernetes Deployment option <a href="https://github.com/degola/penpot-kubernetes" target="_blank">penpot-kubernetes</a>.
* Penpot is available <a href="https://apps.yunohost.org/app/penpot">in the catalog</a> of apps installable on YunoHost instances.
* Or try a fully manual installation if you have a really specific use case.. For help, you can look at the [Architecture][1] section and the <a href="https://github.com/penpot/penpot/tree/develop/docker/images" target="_blank">Docker configuration files</a>.
[1]: /technical-guide/developer/architecture
+1 -1
View File
@@ -7,7 +7,7 @@ eleventyNavigation:
--- ---
<div class="main-illus"> <div class="main-illus">
<img src="/img/home-techguide.png" alt="Technical Guide" border="0"> <img src="/img/home-technical-guide.webp" alt="Technical Guide" border="0">
</div> </div>
# Technical Guide # Technical Guide
+2 -2
View File
@@ -195,13 +195,13 @@ desc: Streamline your design workflow with Penpot's Components guide! Learn to c
<iframe <iframe
width="672px" width="672px"
height="378px" height="378px"
src="https://www.youtube.com/embed/3iWc-dwjA30?start=6" src="https://peertube.kaleidos.net/videos/embed/v9Yh79hom5otcBEnqondBY"
title="Penpot Variants Demo" title="Penpot Variants Demo"
frameborder="0" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen> allowfullscreen>
</iframe> </iframe>
<figcaption>Penpot Variants sneak peek</figcaption> <figcaption>Penpot Variants release</figcaption>
</figure> </figure>
<h3 id="component-understanding-variants-properties-and-values">Understanding variants: properties and values</h3> <h3 id="component-understanding-variants-properties-and-values">Understanding variants: properties and values</h3>
+1 -1
View File
@@ -7,7 +7,7 @@ eleventyNavigation:
--- ---
<div class="main-illus"> <div class="main-illus">
<img src="/img/home-userguide.png" alt="User guide" border="0"> <img src="/img/home-user-guide.webp" alt="User guide" border="0">
</div> </div>
<h1 id="user-guide">Penpot User Guide</h1> <h1 id="user-guide">Penpot User Guide</h1>
+12 -4
View File
@@ -8,15 +8,23 @@ desc: "Work with Penpot's objects: boards, shapes, text, paths, and graphics. Le
available in Penpot, and how to get the most of them.</p> available in Penpot, and how to get the most of them.</p>
<h2 id="Boards">Boards</h2> <h2 id="Boards">Boards</h2>
<p>A Board is a layer typically used as a container for a design. Boards are useful if you want to design for a specific screen or print size. Boards can contain other boards. First level boards <p>Boards are layers that serve as your high-level containers for content organization and layout. Boards are useful if you want to design for a specific screen or print size. Boards can contain other boards. First level boards
are shown by default at the <a href="/user-guide/view-mode">View mode</a>, acting as screens of a design or pages of a document. Also, objects inside boards can be clipped. Boards are a powerful element at Penpot, opening up a ton of possibilities when creating and organizing your designs.</p> are shown by default at the <a href="/user-guide/view-mode">View mode</a>, acting as screens of a design or pages of a document. Also, objects inside boards can be clipped. Boards are a powerful element at Penpot, opening up a ton of possibilities when creating and organizing your designs.</p>
<h3>Create boards</h3> <h3>Create boards</h3>
<p>You can create a board using the board tool at the toolbar or the shortcut <kbd>B</kbd>. </p> <p>To create a board, use the board tool at the toolbar or the shortcut <kbd>B</kbd>.</p>
<p>Set a custom size or choose one of the provided presets with the most common resolution for devices and standard print sizes. </p> <figure>
<img src="/img/objects/board-tool.webp" alt="Board tool">
</figure>
<p>Then, with the board tool selected, you have two options:</p>
<ul>
<li><strong>Select a board size upfront</strong>. You can choose one of the provided presets with the most common resolution for devices and standard print sizes</li>
<li><strong>Click-and-drag</strong> to draw a frame of approximate size, then immediately edit its width/height values to be precise.</li>
</ul>
<figure> <figure>
<video title="Create board" muted="" playsinline="" controls="" width="100%" poster="/img/objects/board-create.webp" height="auto"> <video title="Create board" muted="" playsinline="" controls="" width="100%" poster="/img/objects/board-create.webp" height="auto">
<source src="/img/objects/board-create.mp4" type="video/mp4"> <source src="/img/objects/board-creation.mp4" type="video/mp4">
</video> </video>
</figure> </figure>
+27 -23
View File
@@ -28,33 +28,37 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
<strong>10)</strong> Prototype mode <strong>10)</strong> Prototype mode
<strong>11)</strong> Inspect mode <strong>11)</strong> Inspect mode
<strong>12)</strong> View mode <strong>12)</strong> View mode
<strong>13)</strong> History panel <strong>13)</strong> Share / Invite
<strong>14)</strong> Comments <strong>14)</strong> History
<strong>15)</strong> Zoom <strong>15)</strong> Comments
<strong>16)</strong> File status <strong>16)</strong> Zoom
<strong>17)</strong> Users <strong>17)</strong> Users
<strong>18)</strong> Assets panel <strong>18)</strong> Assets
<strong>19)</strong> Design tokens
<strong>20)</strong> File status
</p> </p>
<ol> <ol>
<li><strong>Viewport:</strong> An infinite canvas to design without limits.</li> <li><strong>Viewport:</strong> An infinite canvas where you can design without limits.</li>
<li><strong>Toolbar: </strong> The toolbar is the place where you will find the tools to quickly and easily create the different types of objects: board, rectangle, ellipse, text, graphic, path and free drawing. <a href="/user-guide/objects/">More about objects.</a></li> <li><strong>Toolbar:</strong> This is where youll find all the tools to quickly and easily create different types of objects: board, rectangle, ellipse, text, graphic, path, and free drawing. <a href="/user-guide/objects/">Learn more about objects.</a></li>
<li><strong>Main menu:</strong> At the file main menu you are able to tweak your workspace configuration. Manage visibility for grids, rulers, panels. Enable or disable snapping, dynamic alignment. Add or remove the file as Shared Library. You can also find here help resources.</li> <li><strong>Main menu:</strong> From the main menu, you can customize your workspace. Manage the visibility of grids, rulers, and panels. Enable or disable snapping and dynamic alignment. Add or remove the file as a Shared Library. Youll also find help resources here.</li>
<li><strong>Pages:</strong> A file can have as many pages as you need. Each page has its own viewport (the almost infinite area where you actually create your designs) with its own layers. You can create, delete or reorder your pages at your needs.</li> <li><strong>Pages:</strong> A file can contain as many pages as you need. Each page has its own viewport (the almost infinite area where you design) and its own layers. You can create, delete, or reorder pages as needed.</li>
<li><strong>Layers:</strong> Layers are the different objects that you can place at the design viewport. <a href="/user-guide/layer-basics/">More about layers.</a></li> <li><strong>Layers:</strong> Layers are the different objects you can place in the design viewport. <a href="/user-guide/layer-basics/">Learn more about layers.</a></li>
<li><strong>Rulers:</strong> Coordinates to help you design. You can also drag guides from the rulers.</li> <li><strong>Rulers:</strong> Rulers provide coordinates to help you design. You can also drag guides from them.</li>
<li><strong>Color palette:</strong> The color palette allows you to always have a color library at plain sight. Use the menu to easily switch between libraries. <a href="/user-guide/styling/#color-palette">More about the color palette</a>.</li> <li><strong>Color palette:</strong> The color palette gives you quick access to a visible library of colors. Use the menu to easily switch between libraries. <a href="/user-guide/styling/#color-palette">Learn more about the color palette.</a></li>
<li><strong>Typography palette:</strong> The Typography palette allows you to always have the typography styles at plain sight. Use the menu to easily switch between libraries.</li> <li><strong>Typography palette:</strong> The typography palette keeps text styles always at hand. Use the menu to switch between libraries.</li>
<li><strong>Design properties:</strong> At the Design properties sidebar you can find and edit the properties of a selected layer. The list of properties shown varies depending on the type of element. There are some common properties always set (like size and position) and others not set but available to be added (stroke, shadow, blur...). <a href="/user-guide/styling/">More about styling.</a></li> <li><strong>Design properties:</strong> The Design Properties sidebar lets you view and edit the attributes of a selected layer. The list changes depending on the element type: some properties are always present (size, position), while others are optional (stroke, shadow, blur). <a href="/user-guide/styling/">Learn more about styling.</a></li>
<li><strong>Prototype mode:</strong> Penpot allows you to prototype interactions connecting boards. Once selected the prototype mode at the right sidebar, select an element to drag a connection to another board. Youll be able to see the interactive prototype in action in the view mode (access by clicking the play button at the top right). <a href="/user-guide/prototyping/">More about prototyping.</a></li> <li><strong>Prototype mode:</strong> Penpot lets you prototype interactions by connecting boards. Activate prototype mode from the right sidebar, then select an element and drag a connection to another board. You can preview the interactive prototype in View Mode (access it by clicking the play button in the top-right corner). <a href="/user-guide/prototyping/">Learn more about prototyping.</a></li>
<li><strong>Inspect mode:</strong> Penpot allows you to get measures, properties and production-ready code from your designs. Also, inspect mode provides a safer view-only mode and other improvements. <a href="/user-guide/inspect/">More about inspecting designs.</a></li> <li><strong>Inspect mode:</strong> Use Inspect Mode to get measurements, properties, and production-ready code from your designs. It also provides a safer, view-only mode with additional improvements. <a href="/user-guide/inspect/">Learn more about inspecting designs.</a></li>
<li><strong>View mode:</strong> The view mode button launches a presentation with the boards. See more about what you can do at the <a href="/user-guide/view-mode/#viewmode-features">view mode.</a> </li> <li><strong>View mode:</strong> The View Mode button launches a presentation of your boards. Explore more features in the <a href="/user-guide/view-mode/#viewmode-features">View Mode guide.</a></li>
<li><strong>History panel:</strong> The history panel keeps track of the latest changes on an opened file. You can undo/redo to walk the changes and expand each one of them to get detailed information. <a href="/user-guide/workspace-basics/#history">More about history panel.</a></li> <li><strong>Share / Invite:</strong> Invite team members to give them access to this file and all team files.</li>
<li><strong>Comments:</strong> Activate comments mode to see all the comments at the file and add new ones.</li> <li><strong>History:</strong> The History panel records recent changes in an open file. Use undo/redo to step through changes, and expand each entry for detailed information. <a href="/user-guide/workspace-basics/#history">Learn more about the history panel.</a></li>
<li><strong>Zoom:</strong> Zooming options.</li> <li><strong>Comments:</strong> Switch to Comments Mode to view all comments on the file and add new ones.</li>
<li><strong>File status:</strong> Information about the file saving state. Know if last changes are saved or if there is a problem.</li> <li><strong>Zoom:</strong> Options to zoom in, zoom out, or fit the canvas.</li>
<li><strong>Users:</strong> Here you can see how many users have the file open at any moment.</li> <li><strong>Users:</strong> See how many users currently have the file open.</li>
<li><strong>Assets panel:</strong> Each file has a default library (File Library) where you can store elements and styles that are likely to be reused within a project. That includes components, colors and typographies. To add an asset to a library just click the “+” button at the header of each asset group.</li> <li><strong>Assets:</strong> Each file includes a default File Library where you can store reusable elements and styles, such as components, colors, and text styles. From this panel, you'll be also able to include and use shared libraries.</li>
<li><strong>Design tokens:</strong> Design tokens are the building blocks of UI elements, expressed in a format usable across design, tools, and code. They include colors, typography, spacing, shadows, and more.</li>
<li><strong>File status:</strong> Displays the files saving state, showing whether your latest changes are saved or if theres a problem.</li>
</ol> </ol>
<h2 id="interface-viewmode">View mode</h2> <h2 id="interface-viewmode">View mode</h2>
+1 -1
View File
@@ -16,7 +16,7 @@
"inflation": "^2.1.0", "inflation": "^2.1.0",
"ioredis": "^5.6.1", "ioredis": "^5.6.1",
"luxon": "^3.6.1", "luxon": "^3.6.1",
"playwright": "^1.53.0", "playwright": "^1.55.1",
"raw-body": "^3.0.0", "raw-body": "^3.0.0",
"svgo": "penpot/svgo#v3.1", "svgo": "penpot/svgo#v3.1",
"xml-js": "^1.6.11", "xml-js": "^1.6.11",
+10 -10
View File
@@ -557,7 +557,7 @@ __metadata:
inflation: "npm:^2.1.0" inflation: "npm:^2.1.0"
ioredis: "npm:^5.6.1" ioredis: "npm:^5.6.1"
luxon: "npm:^3.6.1" luxon: "npm:^3.6.1"
playwright: "npm:^1.53.0" playwright: "npm:^1.55.1"
raw-body: "npm:^3.0.0" raw-body: "npm:^3.0.0"
source-map-support: "npm:^0.5.21" source-map-support: "npm:^0.5.21"
svgo: "penpot/svgo#v3.1" svgo: "penpot/svgo#v3.1"
@@ -1101,27 +1101,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"playwright-core@npm:1.53.0": "playwright-core@npm:1.55.1":
version: 1.53.0 version: 1.55.1
resolution: "playwright-core@npm:1.53.0" resolution: "playwright-core@npm:1.55.1"
bin: bin:
playwright-core: cli.js playwright-core: cli.js
checksum: 10c0/fda0cf76115b15b1ca5cbc69e14185904e5c85e9e7cddb0a48121e69d681c638ac497e8a103985976cae260aa02e9c03ea27d6cd0b5f3d3ca914d4c7fd96f930 checksum: 10c0/39837a8c1232ec27486eac8c3fcacc0b090acc64310f7f9004b06715370fc426f944e3610fe8c29f17cd3d68280ed72c75f660c02aa5b5cf0eb34bab0031308f
languageName: node languageName: node
linkType: hard linkType: hard
"playwright@npm:^1.53.0": "playwright@npm:^1.55.1":
version: 1.53.0 version: 1.55.1
resolution: "playwright@npm:1.53.0" resolution: "playwright@npm:1.55.1"
dependencies: dependencies:
fsevents: "npm:2.3.2" fsevents: "npm:2.3.2"
playwright-core: "npm:1.53.0" playwright-core: "npm:1.55.1"
dependenciesMeta: dependenciesMeta:
fsevents: fsevents:
optional: true optional: true
bin: bin:
playwright: cli.js playwright: cli.js
checksum: 10c0/8d995114808b92f2005bd12ff5e494cdc3fa2d484f4d85a3e54be1fb99e88ae3e34b24792d83bb987462c73e553a0fa37a2a70264afbf67894b51c1498cf5a11 checksum: 10c0/b84a97b0d764403df512f5bbb10c7343974e151a28202cc06f90883a13e8a45f4491a0597f0ae5fb03a026746cbc0d200f0f32195bfaa381aee5ca5770626771
languageName: node languageName: node
linkType: hard linkType: hard
+1
View File
@@ -8,6 +8,7 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.types.objects-map]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.data.auth :as da] [app.main.data.auth :as da]
+1 -1
View File
@@ -52,7 +52,7 @@
(->> (rx/from changes) (->> (rx/from changes)
(rx/merge-map (fn [[page-id changes]] (rx/merge-map (fn [[page-id changes]]
(log/debug :hint "update-indexes" :page-id page-id :changes (count changes)) (log/debug :hint "update-indexes" :page-id page-id :changes (count changes))
(mw/ask! {:cmd :index/update-page-index (mw/ask! {:cmd :index/update
:page-id page-id :page-id page-id
:changes changes}))) :changes changes})))
(rx/catch (fn [cause] (rx/catch (fn [cause]
+11 -17
View File
@@ -21,7 +21,6 @@
[app.common.types.component :as ctc] [app.common.types.component :as ctc]
[app.common.types.fills :as types.fills] [app.common.types.fills :as types.fills]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.data.changes :as dch] [app.main.data.changes :as dch]
@@ -43,7 +42,6 @@
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe] [app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.fix-broken-shapes :as fbs]
[app.main.data.workspace.fix-deleted-fonts :as fdf] [app.main.data.workspace.fix-deleted-fonts :as fdf]
[app.main.data.workspace.groups :as dwg] [app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.guides :as dwgu] [app.main.data.workspace.guides :as dwgu]
@@ -70,7 +68,6 @@
[app.main.features.pointer-map :as fpmap] [app.main.features.pointer-map :as fpmap]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.worker :as mw]
[app.render-wasm :as wasm] [app.render-wasm :as wasm]
[app.render-wasm.api :as api] [app.render-wasm.api :as api]
[app.util.dom :as dom] [app.util.dom :as dom]
@@ -158,18 +155,9 @@
(->> (fpmap/resolve-file file) (->> (fpmap/resolve-file file)
(rx/map :data) (rx/map :data)
(rx/map process-fills) (rx/map process-fills)
(rx/mapcat (rx/map
(fn [{:keys [pages-index] :as data}] (fn [data]
(->> (rx/from (seq pages-index)) (assoc file :data (d/removem (comp t/pointer? val) data))))))
(rx/mapcat
(fn [[id page]]
(let [page (update page :objects ctst/start-page-index)]
(->> (mw/ask! {:cmd :index/initialize-page-index :page page})
(rx/map (fn [_] [id page]))))))
(rx/reduce conj {})
(rx/map (fn [pages-index]
(let [data (assoc data :pages-index pages-index)]
(assoc file :data (d/removem (comp t/pointer? val) data))))))))))
(defn- check-libraries-synchronozation (defn- check-libraries-synchronozation
[file-id libraries] [file-id libraries]
@@ -243,8 +231,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (dp/check-open-plugin) (rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts) (fdf/fix-deleted-fonts-for-local-library file-id)))))
(fbs/fix-broken-shapes)))))
(defn- bundle-fetched (defn- bundle-fetched
[{:keys [file file-id thumbnails] :as bundle}] [{:keys [file file-id thumbnails] :as bundle}]
@@ -280,6 +267,8 @@
(ptk/reify ::fetch-bundle (ptk/reify ::fetch-bundle
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ stream] (watch [_ _ stream]
(log/debug :hint "fetch bundle" :file-id (dm/str file-id))
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)] (let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features}) (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
(get-file-object-thumbnails file-id)) (get-file-object-thumbnails file-id))
@@ -288,6 +277,7 @@
(fn [[file thumbnails]] (fn [[file thumbnails]]
(->> (resolve-file file) (->> (resolve-file file)
(rx/map (fn [file] (rx/map (fn [file]
(log/trace :hint "file resolved" :file-id file-id)
{:file file {:file file
:file-id file-id :file-id file-id
:features features :features features
@@ -357,6 +347,10 @@
(rx/map deref) (rx/map deref)
(rx/mapcat (rx/mapcat
(fn [{:keys [file]}] (fn [{:keys [file]}]
(log/debug :hint "bundle fetched"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(rx/of (dpj/initialize-project (:project-id file)) (rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id) (dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout) (dwsl/initialize-shape-layout)
@@ -83,7 +83,7 @@
(rx/of (dwsh/add-shape shape {:no-select? (= tool :curve)})) (rx/of (dwsh/add-shape shape {:no-select? (= tool :curve)}))
(if (cfh/frame-shape? shape) (if (cfh/frame-shape? shape)
(rx/concat (rx/concat
(->> (mw/ask! {:cmd :selection/query (->> (mw/ask! {:cmd :index/query-selection
:page-id page-id :page-id page-id
:rect (:selrect shape) :rect (:selrect shape)
:include-frames? true :include-frames? true
@@ -1,56 +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.main.data.workspace.fix-broken-shapes
(:require
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn- generate-broken-link-changes
[attr {:keys [objects id] :as container}]
(let [base {:type :fix-obj :fix :broken-children attr id}
contains? (partial contains? objects)
xform (comp
;; FIXME: Ensure all obj have id field (this is needed
;; because some bug adds an ephimeral shape with id ZERO,
;; with a single attr `:shapes` having a vector of ids
;; pointing to not existing shapes). That happens on
;; components. THIS IS A WORKAOURD
(map (fn [[id obj]]
(if (some? (:id obj))
obj
(assoc obj :id id))))
;; Remove all valid shapes
(remove (fn [obj]
(every? contains? (:shapes obj))))
(map (fn [obj]
(assoc base :id (:id obj)))))]
(sequence xform objects)))
(defn fix-broken-shapes
[]
(ptk/reify ::fix-broken-shapes
ptk/WatchEvent
(watch [it state _]
(let [fdata (dsh/lookup-file-data state)
changes (concat
(mapcat (partial generate-broken-link-changes :page-id)
(vals (:pages-index fdata)))
(mapcat (partial generate-broken-link-changes :component-id)
(vals (:components fdata))))]
(if (seq changes)
(rx/of (dch/commit-changes
{:origin it
:redo-changes (vec changes)
:undo-changes []
:save-undo? false}))
(rx/empty))))))
@@ -14,8 +14,9 @@
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
;; This event will update the file so the texts with non existing custom fonts try to be fixed. ;; This event will update the file so the texts with non existing
;; This can happen when: ;; custom fonts try to be fixed. This can happen when:
;;
;; - Exporting/importing files to different teams or penpot instances ;; - Exporting/importing files to different teams or penpot instances
;; - Moving files from one team to another in the same instance ;; - Moving files from one team to another in the same instance
;; - Custom fonts are explicitly deleted in the team area ;; - Custom fonts are explicitly deleted in the team area
@@ -23,112 +24,99 @@
(defn- calculate-alternative-font-id (defn- calculate-alternative-font-id
[value] [value]
(let [fonts (deref fonts/fontsdb)] (let [fonts (deref fonts/fontsdb)]
(->> (vals fonts) (reduce-kv (fn [_ _ font]
(filter #(= (:family %) value)) (if (= (:family font) value)
(first) (reduced (:id font))
:id))) nil))
nil
fonts)))
(defn- has-invalid-font-family? (defn- has-invalid-font-family?
[node] [node]
(let [fonts (deref fonts/fontsdb) (let [fonts (deref fonts/fontsdb)
font-family (:font-family node) font-family (:font-family node)]
alternative-font-id (calculate-alternative-font-id font-family)]
(and (some? font-family) (and (some? font-family)
(nil? (get fonts (:font-id node))) (nil? (get fonts (:font-id node))))))
(some? alternative-font-id))))
(defn- should-fix-deleted-font-shape? (defn- shape-has-invalid-font-family??
[shape] [shape]
(let [text-nodes (txt/node-seq txt/is-text-node? (:content shape))] (and (cfh/text-shape? shape)
(and (cfh/text-shape? shape) (some has-invalid-font-family?
(some has-invalid-font-family? text-nodes)))) (txt/node-seq txt/is-text-node? (:content shape)))))
(defn- should-fix-deleted-font-component?
[component]
(let [xf (comp (map val)
(filter should-fix-deleted-font-shape?))]
(first (sequence xf (:objects component)))))
(defn- fix-deleted-font (defn- fix-deleted-font
[node] [node]
(let [alternative-font-id (calculate-alternative-font-id (:font-family node))] (if-let [alternative-font-id (calculate-alternative-font-id (:font-family node))]
(cond-> node (assoc node :font-id alternative-font-id)
(some? alternative-font-id) (assoc :font-id alternative-font-id)))) node))
(defn- fix-deleted-font-shape (defn- fix-shape-content
[shape] [shape]
(let [transform (partial txt/transform-nodes has-invalid-font-family? fix-deleted-font)] (txt/transform-nodes has-invalid-font-family? fix-deleted-font
(update shape :content transform))) (:content shape)))
(defn- fix-deleted-font-component (defn- fix-typography
[component]
(update component
:objects
(fn [objects]
(update-vals objects fix-deleted-font-shape))))
(defn fix-deleted-font-typography
[typography] [typography]
(let [alternative-font-id (calculate-alternative-font-id (:font-family typography))] (if-let [alternative-font-id (calculate-alternative-font-id (:font-family typography))]
(cond-> typography (assoc typography :font-id alternative-font-id)
(some? alternative-font-id) (assoc :font-id alternative-font-id)))) typography))
(defn- generate-deleted-font-shape-changes (defn- generate-page-changes
[{:keys [objects id]}] [{:keys [objects id]}]
(sequence (reduce-kv (fn [changes shape-id shape]
(comp (map val) (if (shape-has-invalid-font-family?? shape)
(filter should-fix-deleted-font-shape?) (conj changes {:type :mod-obj
(map (fn [shape] :id shape-id
{:type :mod-obj :page-id id
:id (:id shape) :operations [{:type :set
:page-id id :attr :content
:operations [{:type :set :val (fix-shape-content shape)}
:attr :content {:type :set
:val (:content (fix-deleted-font-shape shape))} :attr :position-data
{:type :set :val nil}]})
:attr :position-data changes))
:val nil}]}))) []
objects)) objects))
(defn- generate-deleted-font-components-changes (defn- generate-library-changes
[fdata] [fdata]
(sequence (reduce-kv (fn [changes _ typography]
(comp (map val) (if (has-invalid-font-family? typography)
(filter should-fix-deleted-font-component?) (conj changes {:type :mod-typography
(map (fn [component] :typography (fix-typography typography)})
{:type :mod-component changes))
:id (:id component) []
:objects (-> (fix-deleted-font-component component) :objects)}))) (:typographies fdata)))
(:components fdata)))
(defn- generate-deleted-font-typography-changes (defn fix-deleted-fonts-for-local-library
[fdata] "Looks the file local library for deleted fonts and emit changes if
(sequence invalid but fixable typographyes found."
(comp (map val) [file-id]
(filter has-invalid-font-family?) (ptk/reify ::fix-deleted-fonts-for-local-library
(map (fn [typography]
{:type :mod-typography
:typography (fix-deleted-font-typography typography)})))
(:typographies fdata)))
(defn fix-deleted-fonts
[]
(ptk/reify ::fix-deleted-fonts
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [fdata (dsh/lookup-file-data state) (let [fdata (dsh/lookup-file-data state file-id)]
pages (:pages-index fdata) (when-let [changes (-> (generate-library-changes fdata)
(not-empty))]
shape-changes (mapcat generate-deleted-font-shape-changes (vals pages))
components-changes (generate-deleted-font-components-changes fdata)
typography-changes (generate-deleted-font-typography-changes fdata)
changes (concat shape-changes
components-changes
typography-changes)]
(if (seq changes)
(rx/of (dwc/commit-changes (rx/of (dwc/commit-changes
{:origin it {:origin it
:redo-changes (vec changes) :redo-changes changes
:undo-changes [] :undo-changes []
:save-undo? false})) :save-undo? false})))))))
(rx/empty))))))
;; FIXME: would be nice to not execute this code twice per page in the
;; same working session, maybe some local memoization can improve that
(defn fix-deleted-fonts-for-page
[file-id page-id]
(ptk/reify ::fix-deleted-fonts-for-page
ptk/WatchEvent
(watch [it state _]
(let [page (dsh/lookup-page state file-id page-id)]
(when-let [changes (-> (generate-page-changes page)
(not-empty))]
(rx/of (dwc/commit-changes
{:origin it
:redo-changes changes
:undo-changes []
:save-undo? false})))))))
+24 -10
View File
@@ -15,6 +15,7 @@
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.data.changes :as dch] [app.main.data.changes :as dch]
@@ -23,12 +24,14 @@
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.persistence :as-alias dps] [app.main.data.persistence :as-alias dps]
[app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.fix-deleted-fonts :as fdf]
[app.main.data.workspace.layout :as layout] [app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.thumbnails :as dwth] [app.main.data.workspace.thumbnails :as dwth]
[app.main.errors] [app.main.errors]
[app.main.features :as features] [app.main.features :as features]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.worker :as mw]
[app.render-wasm.shape :as wasm.shape] [app.render-wasm.shape :as wasm.shape]
[app.util.http :as http] [app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@@ -56,16 +59,21 @@
(some? metadata) (cf/resolve-file-media metadata) (some? metadata) (cf/resolve-file-media metadata)
(some? fill-image) (cf/resolve-file-media fill-image)))))) (some? fill-image) (cf/resolve-file-media fill-image))))))
(defn- get-page-cache
[state file-id page-id]
(dm/get-in state [:workspace-cache [file-id page-id]]))
(defn- initialize-page* (defn- initialize-page*
"Second phase of page initialization, once we know the page is "Second phase of page initialization, once we know the page is
available in the state" available in the state"
[file-id page-id page] [file-id page-id]
(ptk/reify ::initialize-page* (ptk/reify ::initialize-page*
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
;; selection; when user abandon the current page, the selection is lost (let [state (dsh/update-page state file-id page-id #(update % :objects ctst/start-page-index))
(let [local (dm/get-in state [:workspace-cache [file-id page-id]] default-workspace-local)] page (dsh/lookup-page state file-id page-id)
local (or (get-page-cache state file-id page-id) default-workspace-local)]
(-> state (-> state
(assoc :current-page-id page-id) (assoc :current-page-id page-id)
(assoc :workspace-local (assoc local :selected (d/ordered-set))) (assoc :workspace-local (assoc local :selected (d/ordered-set)))
@@ -75,11 +83,16 @@
(update :workspace-layout layout/load-layout-flags) (update :workspace-layout layout/load-layout-flags)
(update :workspace-global layout/load-layout-state)))) (update :workspace-global layout/load-layout-state))))
ptk/EffectEvent ptk/WatchEvent
(effect [_ _ _] (watch [_ state _]
(let [uris (into #{} xf:collect-file-media (:objects page))] (let [page (dsh/lookup-page state file-id page-id)
(->> (rx/from uris) uris (into #{} xf:collect-file-media (:objects page))]
(rx/subs! #(http/fetch-data-uri % false))))))) (rx/merge
(->> (rx/from uris)
(rx/map #(http/fetch-data-uri % false))
(rx/ignore))
(->> (mw/ask! {:cmd :index/initialize :page page})
(rx/ignore)))))))
(defn initialize-page (defn initialize-page
[file-id page-id] [file-id page-id]
@@ -89,9 +102,10 @@
(ptk/reify ::initialize-page (ptk/reify ::initialize-page
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(if-let [page (dsh/lookup-page state file-id page-id)] (if (dsh/lookup-page state file-id page-id)
(rx/concat (rx/concat
(rx/of (initialize-page* file-id page-id page) (rx/of (initialize-page* file-id page-id)
(fdf/fix-deleted-fonts-for-page file-id page-id)
(dwth/watch-state-changes file-id page-id) (dwth/watch-state-changes file-id page-id)
(dwl/watch-component-changes)) (dwl/watch-component-changes))
(let [profile (:profile state) (let [profile (:profile state)
@@ -344,7 +344,7 @@
(if (some? selrect) (if (some? selrect)
(->> (ask-worker (->> (ask-worker
{:cmd :selection/query {:cmd :index/query-selection
:page-id page-id :page-id page-id
:rect selrect :rect selrect
:include-frames? true :include-frames? true
+16 -2
View File
@@ -8,6 +8,8 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as log]
[app.common.time :as ct]
[app.common.transit :as t] [app.common.transit :as t]
[app.common.uri :as u] [app.common.uri :as u]
[app.config :as cf] [app.config :as cf]
@@ -17,6 +19,8 @@
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(log/set-level! :info)
(defn handle-response (defn handle-response
[{:keys [status body headers uri] :as response}] [{:keys [status body headers uri] :as response}]
(cond (cond
@@ -126,13 +130,21 @@
(select-keys params query-params) (select-keys params query-params)
nil)) nil))
:response-type :response-type
(if stream? nil response-type)}] (if stream? nil response-type)}
tpoint
(ct/tpoint-ms)]
(log/trc :hint "make request" :id id)
(->> (http/fetch request) (->> (http/fetch request)
(rx/map http/response->map) (rx/map http/response->map)
(rx/mapcat (fn [{:keys [headers body] :as response}] (rx/mapcat (fn [{:keys [headers body] :as response}]
(log/trc :hint "response received" :id id :elapsed (tpoint))
(let [ctype (get headers "content-type") (let [ctype (get headers "content-type")
response-stream? (str/starts-with? ctype "text/event-stream")] response-stream? (str/starts-with? ctype "text/event-stream")
tpoint (ct/tpoint-ms)]
(when (and response-stream? (not stream?)) (when (and response-stream? (not stream?))
(ex/raise :type :internal (ex/raise :type :internal
@@ -148,6 +160,8 @@
(->> response (->> response
(http/process-response-type response-type) (http/process-response-type response-type)
(rx/map decode-fn) (rx/map decode-fn)
(rx/tap (fn [_]
(log/trc :hint "response decoded" :id id :elapsed (tpoint))))
(rx/mapcat handle-response))))))))) (rx/mapcat handle-response)))))))))
(defmulti cmd! (fn [id _] id)) (defmulti cmd! (fn [id _] id))
+3 -3
View File
@@ -84,7 +84,7 @@
(let [value (get point coord) (let [value (get point coord)
vbox @refs/vbox vbox @refs/vbox
ranges [[(- value (/ 0.5 zoom)) (+ value (/ 0.5 zoom))]]] ranges [[(- value (/ 0.5 zoom)) (+ value (/ 0.5 zoom))]]]
(->> (mw/ask! {:cmd :snaps/range-query (->> (mw/ask! {:cmd :index/query-snap
:page-id page-id :page-id page-id
:frame-id frame-id :frame-id frame-id
:axis coord :axis coord
@@ -101,7 +101,7 @@
(mapv #(vector (- % snap-accuracy) (mapv #(vector (- % snap-accuracy)
(+ % snap-accuracy)))) (+ % snap-accuracy))))
vbox @refs/vbox] vbox @refs/vbox]
(->> (mw/ask! {:cmd :snaps/range-query (->> (mw/ask! {:cmd :index/query-snap
:page-id page-id :page-id page-id
:frame-id frame-id :frame-id frame-id
:axis coord :axis coord
@@ -217,7 +217,7 @@
(defn select-shapes-area (defn select-shapes-area
[page-id frame-id selected objects area] [page-id frame-id selected objects area]
(->> (mw/ask! {:cmd :selection/query (->> (mw/ask! {:cmd :index/query-selection
:page-id page-id :page-id page-id
:frame-id frame-id :frame-id frame-id
:include-frames? true :include-frames? true
@@ -194,7 +194,7 @@
(if (mf/ref-val hover-disabled-ref) (if (mf/ref-val hover-disabled-ref)
(rx/of nil) (rx/of nil)
(->> (mw/ask-buffered! (->> (mw/ask-buffered!
{:cmd :selection/query {:cmd :index/query-selection
:page-id page-id :page-id page-id
:rect rect :rect rect
:include-frames? true :include-frames? true
+2 -1
View File
@@ -338,8 +338,9 @@
:else :else
(let [page (dsh/lookup-page @st/state) (let [page (dsh/lookup-page @st/state)
shape (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width}) shape (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(txt/change-text text) (update :content txt/change-text text)
(assoc :position-data nil)) (assoc :position-data nil))
changes changes
(-> (cb/empty-changes) (-> (cb/empty-changes)
(cb/with-page page) (cb/with-page page)
+1 -2
View File
@@ -9,14 +9,13 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.objects-map]
[app.util.object :as obj] [app.util.object :as obj]
[app.worker.export] [app.worker.export]
[app.worker.impl :as impl] [app.worker.impl :as impl]
[app.worker.import] [app.worker.import]
[app.worker.index] [app.worker.index]
[app.worker.messages :as wm] [app.worker.messages :as wm]
[app.worker.selection]
[app.worker.snaps]
[app.worker.thumbnails] [app.worker.thumbnails]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[promesa.core :as p])) [promesa.core :as p]))
+57 -14
View File
@@ -9,26 +9,69 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.changes :as ch] [app.common.files.changes :as ch]
[app.common.geom.rect :as grc]
[app.common.logging :as log]
[app.common.time :as ct]
[app.worker.impl :as impl] [app.worker.impl :as impl]
[app.worker.selection :as selection]
[app.worker.snap :as snap]
[okulary.core :as l])) [okulary.core :as l]))
(log/set-level! :info)
(defonce state (l/atom {:pages-index {}})) (defonce state (l/atom {:pages-index {}}))
(defmethod impl/handler :index/initialize-page-index (defmethod impl/handler :index/initialize
[{:keys [page] :as message}] [{:keys [page] :as message}]
(swap! state update :pages-index assoc (:id page) page) (let [tpoint (ct/tpoint-ms)]
(impl/handler (assoc message :cmd :selection/initialize-page-index)) (try
(impl/handler (assoc message :cmd :snaps/initialize-page-index))) (swap! state update :pages-index assoc (:id page) page)
(swap! state update ::selection selection/add-page page)
(swap! state update ::snap snap/add-page page)
(defmethod impl/handler :index/update-page-index (finally
(let [elapsed (tpoint)]
(log/dbg :hint "page indexed" :id (:id page) :elapsed elapsed ::log/sync? true))))
nil))
(defmethod impl/handler :index/update
[{:keys [page-id changes] :as message}] [{:keys [page-id changes] :as message}]
(let [tpoint (ct/tpoint-ms)]
(try
(let [old-page (dm/get-in @state [:pages-index page-id])
new-page (-> state
(swap! ch/process-changes changes false)
(dm/get-in [:pages-index page-id]))]
(let [old-page (dm/get-in @state [:pages-index page-id]) (swap! state update ::snap snap/update-page old-page new-page)
new-page (-> state (swap! state update ::selection selection/update-page old-page new-page))
(swap! ch/process-changes changes false) (finally
(dm/get-in [:pages-index page-id])) (let [elapsed (tpoint)]
message (assoc message (log/dbg :hint "page index updated" :id page-id :elapsed elapsed ::log/sync? true))))
:old-page old-page nil))
:new-page new-page)]
(impl/handler (assoc message :cmd :selection/update-page-index)) ;; FIXME: schema
(impl/handler (assoc message :cmd :snaps/update-page-index))))
(defmethod impl/handler :index/query-snap
[{:keys [page-id frame-id axis ranges bounds] :as message}]
(if-let [index (get @state ::snap)]
(let [match-bounds?
(fn [[_ data]]
(some #(or (= :guide (:type %))
(= :layout (:type %))
(grc/contains-point? bounds (:pt %))) data))
xform
(comp (mapcat #(snap/query index page-id frame-id axis %))
(distinct)
(filter match-bounds?))]
(into [] xform ranges))
[]))
;; FIXME: schema
(defmethod impl/handler :index/query-selection
[message]
(if-let [index (get @state ::selection)]
(selection/query index message)
[]))
+108 -84
View File
@@ -17,46 +17,62 @@
[app.common.types.modifiers :as ctm] [app.common.types.modifiers :as ctm]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.util.quadtree :as qdt] [app.util.quadtree :as qdt]
[app.worker.impl :as impl] [clojure.set :as set]))
[clojure.set :as set]
[okulary.core :as l]))
;; FIXME: performance shape & rect static props ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:const padding-percent 0.10) (def ^:const padding-percent 0.10)
(defonce state (l/atom {})) (defn- index-shape
"A reducing function that ads a shape to the index"
[objects parents-index clip-index index shape]
(let [bounds
(cond
(and ^boolean (cfh/text-shape? shape)
^boolean (some? (:position-data shape))
^boolean (d/not-empty? (:position-data shape)))
(gst/shape->bounds shape)
(defn make-index-shape :else
[objects parents-index clip-parents-index] (grc/points->rect (:points shape)))
(fn [index shape]
(let [{:keys [x y width height]}
(cond
(and ^boolean (cfh/text-shape? shape)
^boolean (some? (:position-data shape))
^boolean (d/not-empty? (:position-data shape)))
(gst/shape->bounds shape)
:else bound
(grc/points->rect (:points shape))) #js {:x (dm/get-prop bounds :x)
:y (dm/get-prop bounds :y)
:width (dm/get-prop bounds :width)
:height (dm/get-prop bounds :height)}
shape-bound #js {:x x :y y :width width :height height} shape-id
(dm/get-prop shape :id)
parents (get parents-index (:id shape)) frame-id
clip-parents (get clip-parents-index (:id shape)) (dm/get-prop shape :frame-id)
frame (when (and (not= :frame (:type shape)) shape-type
(not= (:frame-id shape) uuid/zero)) (dm/get-prop shape :type)
(get objects (:frame-id shape)))]
(qdt/insert index
(:id shape)
shape-bound
(assoc shape
:frame frame
:clip-parents clip-parents
:parents parents)))))
(defn objects-bounds parents
(get parents-index shape-id)
clip-parents
(get clip-index shape-id)
frame
(when (and (not= :frame shape-type)
(not= frame-id uuid/zero))
(get objects frame-id))]
(qdt/insert index
shape-id
bound
(assoc shape
:frame frame
:clip-parents clip-parents
:parents parents))))
(defn- objects-bounds
"Calculates the bounds of the quadtree given a objects map." "Calculates the bounds of the quadtree given a objects map."
[objects] [objects]
(-> objects (-> objects
@@ -64,7 +80,7 @@
vals vals
gsh/shapes->rect)) gsh/shapes->rect))
(defn add-padding-bounds (defn- add-padding-bounds
"Adds a padding to the bounds defined as a percent in the constant `padding-percent`. "Adds a padding to the bounds defined as a percent in the constant `padding-percent`.
For a value of 0.1 will add a 20% width increase (2 x padding)" For a value of 0.1 will add a 20% width increase (2 x padding)"
[bounds] [bounds]
@@ -81,41 +97,48 @@
(defn- create-index (defn- create-index
[objects] [objects]
(let [shapes (-> objects (dissoc uuid/zero) vals) (let [parents-index (cfi/generate-child-all-parents-index objects)
parents-index (cfi/generate-child-all-parents-index objects) clip-index (cfi/create-clip-index objects parents-index)
clip-parents-index (cfi/create-clip-index objects parents-index) root-shapes (cfh/get-immediate-children objects uuid/zero)
bounds (-> root-shapes gsh/shapes->rect add-padding-bounds)
root-shapes (cfh/get-immediate-children objects uuid/zero)
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))
index (reduce index-shape initial-quadtree shapes)]
index (reduce-kv #(index-shape objects parents-index clip-index %1 %3)
(qdt/create (clj->js bounds))
(dissoc objects uuid/zero))]
{:index index :bounds bounds})) {:index index :bounds bounds}))
;; FIXME: optimize
(defn- update-index (defn- update-index
[{index :index :as data} old-objects new-objects] [{index :index :as data} old-objects new-objects]
(let [changes? (fn [id] (let [object-changed?
(not= (get old-objects id) (fn [id]
(get new-objects id))) (not= (get old-objects id)
(get new-objects id)))
changed-ids (into #{} changed-ids
(comp (filter #(not= % uuid/zero)) (into #{}
(filter changes?) (comp (filter #(not= % uuid/zero))
(mapcat #(into [%] (cfh/get-children-ids new-objects %)))) (filter object-changed?)
(set/union (set (keys old-objects)) (mapcat #(into [%] (cfh/get-children-ids new-objects %))))
(set (keys new-objects))))
shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) (set/union (set (keys old-objects))
parents-index (cfi/generate-child-all-parents-index new-objects shapes) (set (keys new-objects))))
clip-parents-index (cfi/create-clip-index new-objects parents-index)
new-index (qdt/remove-all index changed-ids) shapes
(->> changed-ids
(map #(get new-objects %))
(filterv (comp not nil?)))
index-shape (make-index-shape new-objects parents-index clip-parents-index) parents-index
index (reduce index-shape new-index shapes)] (cfi/generate-child-all-parents-index new-objects shapes)
clip-index
(cfi/create-clip-index new-objects parents-index)
index
(reduce #(index-shape new-objects parents-index clip-index %1 %2)
(qdt/remove-all index changed-ids)
shapes)]
(assoc data :index index))) (assoc data :index index)))
@@ -231,35 +254,36 @@
(map :id)) (map :id))
result))) result)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod impl/handler :selection/initialize-page-index (defn add-page
[{:keys [page] :as message}] "Add a page index to the state"
(letfn [(add-page [state {:keys [id objects] :as page}] [state {:keys [id objects] :as page}]
(assoc state id (create-index objects)))] (assoc state id (create-index objects)))
(swap! state add-page page)
nil))
(defmethod impl/handler :selection/update-page-index (defn update-page
[{:keys [page-id old-page new-page] :as message}] "Update page index on the state"
(swap! state update page-id [state old-page new-page]
(fn [index] (let [page-id (get old-page :id)]
(let [old-objects (:objects old-page) (update state page-id
new-objects (:objects new-page) (fn [index]
old-bounds (:bounds index) (let [old-objects (:objects old-page)
new-bounds (objects-bounds new-objects)] new-objects (:objects new-page)
old-bounds (:bounds index)
new-bounds (objects-bounds new-objects)]
;; If the new bounds are contained within the old bounds ;; If the new bounds are contained within the old bounds
;; we can update the index. Otherwise we need to ;; we can update the index. Otherwise we need to
;; re-create it. ;; re-create it.
(if (and (some? index) (if (and (some? index)
(grc/contains-rect? old-bounds new-bounds)) (grc/contains-rect? old-bounds new-bounds))
(update-index index old-objects new-objects) (update-index index old-objects new-objects)
(create-index new-objects))))) (create-index new-objects)))))))
nil)
(defmethod impl/handler :selection/query (defn query
[{:keys [page-id rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?] [index {:keys [page-id rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?]
:or {full-frame? false include-frames? false clip-children? true using-selrect? false} :or {full-frame? false include-frames? false clip-children? true using-selrect? false}}]
:as message}] (when-let [index (get index page-id)]
(when-let [index (get @state page-id)]
(query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?))) (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?)))
@@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.util.snap-data (ns app.worker.snap
"Data structure that holds and retrieves the data to make the snaps. "Data structure that holds and retrieves the data to make the snaps.
Internally is implemented with a balanced binary tree that queries by range. Internally is implemented with a balanced binary tree that queries by range.
https://en.wikipedia.org/wiki/Range_tree" https://en.wikipedia.org/wiki/Range_tree"
-40
View File
@@ -1,40 +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.worker.snaps
(:require
[app.common.geom.rect :as grc]
[app.util.snap-data :as sd]
[app.worker.impl :as impl]
[okulary.core :as l]))
(defonce state (l/atom {}))
;; Public API
(defmethod impl/handler :snaps/initialize-page-index
[{:keys [page] :as message}]
(swap! state sd/add-page page)
nil)
(defmethod impl/handler :snaps/update-page-index
[{:keys [old-page new-page] :as message}]
(swap! state sd/update-page old-page new-page)
nil)
(defmethod impl/handler :snaps/range-query
[{:keys [page-id frame-id axis ranges bounds] :as message}]
(let [match-bounds?
(fn [[_ data]]
(some #(or (= :guide (:type %))
(= :layout (:type %))
(grc/contains-point? bounds (:pt %))) data))]
(->> (into []
(comp (mapcat #(sd/query @state page-id frame-id axis %))
(distinct))
ranges)
(filter match-bounds?))))
+8 -8
View File
@@ -17,7 +17,7 @@
[frontend-tests.tokens.token-form-test] [frontend-tests.tokens.token-form-test]
[frontend-tests.util-range-tree-test] [frontend-tests.util-range-tree-test]
[frontend-tests.util-simple-math-test] [frontend-tests.util-simple-math-test]
[frontend-tests.util-snap-data-test])) [frontend-tests.worker-snap-test]))
(enable-console-print!) (enable-console-print!)
@@ -29,6 +29,8 @@
(defn init (defn init
[] []
(t/run-tests (t/run-tests
'frontend-tests.basic-shapes-test
'frontend-tests.data.workspace-colors-test
'frontend-tests.helpers-shapes-test 'frontend-tests.helpers-shapes-test
'frontend-tests.logic.comp-remove-swap-slots-test 'frontend-tests.logic.comp-remove-swap-slots-test
'frontend-tests.logic.components-and-tokens 'frontend-tests.logic.components-and-tokens
@@ -36,13 +38,11 @@
'frontend-tests.logic.frame-guides-test 'frontend-tests.logic.frame-guides-test
'frontend-tests.logic.groups-test 'frontend-tests.logic.groups-test
'frontend-tests.plugins.context-shapes-test 'frontend-tests.plugins.context-shapes-test
'frontend-tests.util-range-tree-test 'frontend-tests.tokens.import-export-test
'frontend-tests.util-snap-data-test
'frontend-tests.util-simple-math-test
'frontend-tests.basic-shapes-test
'frontend-tests.data.workspace-colors-test
'frontend-tests.tokens.logic.token-actions-test 'frontend-tests.tokens.logic.token-actions-test
'frontend-tests.tokens.logic.token-data-test 'frontend-tests.tokens.logic.token-data-test
'frontend-tests.tokens.import-export-test
'frontend-tests.tokens.style-dictionary-test 'frontend-tests.tokens.style-dictionary-test
'frontend-tests.tokens.token-form-test)) 'frontend-tests.tokens.token-form-test
'frontend-tests.util-range-tree-test
'frontend-tests.util-simple-math-test
'frontend-tests.worker-snap-test))
@@ -4,12 +4,12 @@
;; ;;
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns frontend-tests.util-snap-data-test (ns frontend-tests.worker-snap-test
(:require (:require
[app.common.files.builder :as fb] [app.common.files.builder :as fb]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.util.snap-data :as sd] [app.worker.snap :as snap]
[cljs.pprint :refer [pprint]] [cljs.pprint :refer [pprint]]
[cljs.test :as t :include-macros true])) [cljs.test :as t :include-macros true]))
@@ -21,9 +21,9 @@
(fn [] (fn []
(uuid/custom 123456789 (swap! counter inc))))) (uuid/custom 123456789 (swap! counter inc)))))
(t/deftest test-create-index (t/deftest create-index
(t/testing "Create empty data" (t/testing "Create empty data"
(let [data (sd/make-snap-data)] (let [data (snap/make-snap-data)]
(t/is (some? data)))) (t/is (some? data))))
(t/testing "Add empty page (only root-frame)" (t/testing "Add empty page (only root-frame)"
@@ -32,8 +32,8 @@
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/get-current-page)) (fb/get-current-page))
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page))] (snap/add-page page))]
(t/is (some? data)))) (t/is (some? data))))
(t/testing "Create simple shape on root" (t/testing "Create simple shape on root"
@@ -48,10 +48,10 @@
:height 100})) :height 100}))
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
result-x (sd/query data (:id page) uuid/zero :x [0 100])] result-x (snap/query data (:id page) uuid/zero :x [0 100])]
(t/is (some? data)) (t/is (some? data))
@@ -82,11 +82,11 @@
page (fb/get-current-page state) page (fb/get-current-page state)
;; frame-id (::fb/last-id file) ;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (snap/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])] result-frame-x (snap/query data (:id page) frame-id :x [0 100])]
(t/is (some? data)) (t/is (some? data))
(t/is (= (count result-zero-x) 3)) (t/is (= (count result-zero-x) 3))
@@ -116,11 +116,11 @@
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (snap/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])] result-frame-x (snap/query data (:id page) frame-id :x [0 100])]
(t/is (some? data)) (t/is (some? data))
(t/is (= (count result-zero-x) 3)) (t/is (= (count result-zero-x) 3))
@@ -137,13 +137,13 @@
frame-id (::fb/last-id state) frame-id (::fb/last-id state)
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (snap/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y (snap/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-x (snap/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])] result-frame-y (snap/query data (:id page) frame-id :y [0 100])]
(t/is (some? data)) (t/is (some? data))
;; We can snap in the root ;; We can snap in the root
@@ -168,13 +168,13 @@
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (snap/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y (snap/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-x (snap/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])] result-frame-y (snap/query data (:id page) frame-id :y [0 100])]
(t/is (some? data)) (t/is (some? data))
;; We can snap in the root ;; We can snap in the root
@@ -185,7 +185,7 @@
(t/is (= (count result-frame-x) 1)) (t/is (= (count result-frame-x) 1))
(t/is (= (count result-frame-y) 0))))) (t/is (= (count result-frame-y) 0)))))
(t/deftest test-update-index (t/deftest update-index
(t/testing "Create frame on root and then remove it." (t/testing "Create frame on root and then remove it."
(let [state (-> (fb/create-state) (let [state (-> (fb/create-state)
(fb/add-file {:name "Test"}) (fb/add-file {:name "Test"})
@@ -200,17 +200,17 @@
shape-id (::fb/last-id state) shape-id (::fb/last-id state)
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
state (-> state state (-> state
(fb/delete-shape shape-id)) (fb/delete-shape shape-id))
new-page (fb/get-current-page state) new-page (fb/get-current-page state)
data (sd/update-page data page new-page) data (snap/update-page data page new-page)
result-x (sd/query data (:id page) uuid/zero :x [0 100]) result-x (snap/query data (:id page) uuid/zero :x [0 100])
result-y (sd/query data (:id page) uuid/zero :y [0 100])] result-y (snap/query data (:id page) uuid/zero :y [0 100])]
(t/is (some? data)) (t/is (some? data))
(t/is (= (count result-x) 0)) (t/is (= (count result-x) 0))
@@ -231,16 +231,16 @@
page (fb/get-current-page state) page (fb/get-current-page state)
;; frame-id (::fb/last-id state) ;; frame-id (::fb/last-id state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
state (fb/delete-shape state shape-id) state (fb/delete-shape state shape-id)
new-page (fb/get-current-page state) new-page (fb/get-current-page state)
data (sd/update-page data page new-page) data (snap/update-page data page new-page)
result-x (sd/query data (:id page) uuid/zero :x [0 100]) result-x (snap/query data (:id page) uuid/zero :x [0 100])
result-y (sd/query data (:id page) uuid/zero :y [0 100])] result-y (snap/query data (:id page) uuid/zero :y [0 100])]
(t/is (some? data)) (t/is (some? data))
(t/is (= (count result-x) 0)) (t/is (= (count result-x) 0))
@@ -263,16 +263,16 @@
state (fb/close-board state) state (fb/close-board state)
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
state (fb/delete-shape state shape-id) state (fb/delete-shape state shape-id)
new-page (fb/get-current-page state) new-page (fb/get-current-page state)
data (sd/update-page data page new-page) data (snap/update-page data page new-page)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (snap/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])] result-frame-x (snap/query data (:id page) frame-id :x [0 100])]
(t/is (some? data)) (t/is (some? data))
(t/is (= (count result-zero-x) 3)) (t/is (= (count result-zero-x) 3))
@@ -291,18 +291,18 @@
frame-id (::fb/last-id state) frame-id (::fb/last-id state)
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
new-page (-> (fb/delete-guide state guide-id) new-page (-> (fb/delete-guide state guide-id)
(fb/get-current-page)) (fb/get-current-page))
data (sd/update-page data page new-page) data (snap/update-page data page new-page)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (snap/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y (snap/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-x (snap/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])] result-frame-y (snap/query data (:id page) frame-id :y [0 100])]
(t/is (some? data)) (t/is (some? data))
;; We can snap in the root ;; We can snap in the root
@@ -325,17 +325,17 @@
guide-id (::fb/last-id file) guide-id (::fb/last-id file)
page (fb/get-current-page file) page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (snap/make-snap-data) (snap/add-page page))
new-page (-> (fb/delete-guide file guide-id) new-page (-> (fb/delete-guide file guide-id)
(fb/get-current-page)) (fb/get-current-page))
data (sd/update-page data page new-page) data (snap/update-page data page new-page)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (snap/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y (snap/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-x (snap/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])] result-frame-y (snap/query data (:id page) frame-id :y [0 100])]
(t/is (some? data)) (t/is (some? data))
;; We can snap in the root ;; We can snap in the root
(t/is (= (count result-zero-x) 0)) (t/is (= (count result-zero-x) 0))
@@ -358,8 +358,8 @@
frame-id (::fb/last-id state) frame-id (::fb/last-id state)
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
state (fb/update-shape state frame-id state (fb/update-shape state frame-id
(fn [shape] (fn [shape]
@@ -370,12 +370,12 @@
new-page (fb/get-current-page state) new-page (fb/get-current-page state)
data (sd/update-page data page new-page) data (snap/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x-1 (snap/query data (:id page) uuid/zero :x [0 100])
result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100]) result-frame-x-1 (snap/query data (:id page) frame-id :x [0 100])
result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300]) result-zero-x-2 (snap/query data (:id page) uuid/zero :x [200 300])
result-frame-x-2 (sd/query data (:id page) frame-id :x [200 300])] result-frame-x-2 (snap/query data (:id page) frame-id :x [200 300])]
(t/is (some? data)) (t/is (some? data))
(t/is (= (count result-zero-x-1) 0)) (t/is (= (count result-zero-x-1) 0))
@@ -396,8 +396,8 @@
shape-id (::fb/last-id state) shape-id (::fb/last-id state)
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (snap/make-snap-data)
(sd/add-page page)) (snap/add-page page))
state (fb/update-shape state shape-id state (fb/update-shape state shape-id
(fn [shape] (fn [shape]
@@ -408,10 +408,10 @@
new-page (fb/get-current-page state) new-page (fb/get-current-page state)
;; FIXME: update ;; FIXME: update
data (sd/update-page data page new-page) data (snap/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x-1 (snap/query data (:id page) uuid/zero :x [0 100])
result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300])] result-zero-x-2 (snap/query data (:id page) uuid/zero :x [200 300])]
(t/is (some? data)) (t/is (some? data))
(t/is (= (count result-zero-x-1) 0)) (t/is (= (count result-zero-x-1) 0))
@@ -432,22 +432,22 @@
frame-id (::fb/last-id state) frame-id (::fb/last-id state)
page (fb/get-current-page state) page (fb/get-current-page state)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (snap/make-snap-data) (snap/add-page page))
new-page (-> (fb/update-guide state (assoc guide :position 150)) new-page (-> (fb/update-guide state (assoc guide :position 150))
(fb/get-current-page)) (fb/get-current-page))
data (sd/update-page data page new-page) data (snap/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x-1 (snap/query data (:id page) uuid/zero :x [0 100])
result-zero-y-1 (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y-1 (snap/query data (:id page) uuid/zero :y [0 100])
result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100]) result-frame-x-1 (snap/query data (:id page) frame-id :x [0 100])
result-frame-y-1 (sd/query data (:id page) frame-id :y [0 100]) result-frame-y-1 (snap/query data (:id page) frame-id :y [0 100])
result-zero-x-2 (sd/query data (:id page) uuid/zero :x [0 200]) result-zero-x-2 (snap/query data (:id page) uuid/zero :x [0 200])
result-zero-y-2 (sd/query data (:id page) uuid/zero :y [0 200]) result-zero-y-2 (snap/query data (:id page) uuid/zero :y [0 200])
result-frame-x-2 (sd/query data (:id page) frame-id :x [0 200]) result-frame-x-2 (snap/query data (:id page) frame-id :x [0 200])
result-frame-y-2 (sd/query data (:id page) frame-id :y [0 200])] result-frame-y-2 (snap/query data (:id page) frame-id :y [0 200])]
(t/is (some? data)) (t/is (some? data))
+10
View File
@@ -1,5 +1,15 @@
# CHANGELOG # CHANGELOG
## 1.0.9
- Fix dependencies declaration on package.json
## 1.0.8
- Update penpot runtime
## 1.0.7 ## 1.0.7
- Add the ability to provide refereron creating build context - Add the ability to provide refereron creating build context
+3 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@penpot/library", "name": "@penpot/library",
"version": "1.0.7", "version": "1.0.9",
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538", "packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538",
@@ -35,13 +35,11 @@
"watch:test": "node --test --watch", "watch:test": "node --test --watch",
"watch": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library" "watch": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library"
}, },
"dependencies": {
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"date-fns": "^4.1.0"
},
"devDependencies": { "devDependencies": {
"@types/node": "^22.12.0", "@types/node": "^22.12.0",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"date-fns": "^4.1.0",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.21"
} }