mirror of
https://github.com/penpot/penpot.git
synced 2026-09-09 12:19:58 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16d0680ecf |
No files matched your search
@@ -647,10 +647,16 @@
|
||||
data
|
||||
library-ids)))
|
||||
|
||||
(defn disable-database-timeouts!
|
||||
(def ^:const import-transaction-timeout-ms
|
||||
"Ceiling for binfile import transactions (20 minutes). Interpolated
|
||||
directly into SQL: compile-time constant, never user input."
|
||||
(* 20 60 1000))
|
||||
|
||||
(defn configure-database-timeouts!
|
||||
[cfg]
|
||||
(let [conn (db/get-connection cfg)]
|
||||
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
|
||||
(db/exec-one! conn [(str "SET LOCAL idle_in_transaction_session_timeout = "
|
||||
import-transaction-timeout-ms)])
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])))
|
||||
|
||||
(defn process-file
|
||||
|
||||
@@ -454,7 +454,7 @@
|
||||
(defn- read-import-v1
|
||||
[{:keys [::db/conn ::bfc/project-id ::bfc/profile-id ::bfc/input] :as cfg}]
|
||||
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
(bfc/configure-database-timeouts! cfg)
|
||||
|
||||
(pu/with-open [input (zstd-input-stream input)
|
||||
input (io/data-input-stream input)]
|
||||
|
||||
@@ -529,89 +529,85 @@
|
||||
(let [manifest (json/read reader :key-fn json/read-kebab-key)]
|
||||
(decode-manifest manifest)))))
|
||||
|
||||
(defn- match-media-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/media/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
(def ^:private file-object-entry-categories
|
||||
"Zip path segments that hold per-file object entries on the
|
||||
`files/<file-id>/<category>/<object-id>.json` shape."
|
||||
#{"media" "colors" "components" "typographies"})
|
||||
|
||||
(defn- match-color-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/colors/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
(defn- index-entry-name
|
||||
"Classify a single zip entry by the raw shape of its path and
|
||||
accumulate it on the index.
|
||||
|
||||
(defn- match-component-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/components/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
It replaces the per-file regex matchers with a single
|
||||
classification pass over all entries. The `.json` suffix is matched
|
||||
literally: the previous regexes left the dot unescaped, so crafted
|
||||
paths like `files/<file-id>/tokensXjson` or `objects/x-json` matched
|
||||
by accident; requiring the literal suffix ignores them (legitimate
|
||||
exports always write a literal `.json` suffix). Unknown paths are
|
||||
ignored."
|
||||
[index ^String name entry]
|
||||
(if-not (and name (str/ends-with? name ".json"))
|
||||
index
|
||||
(let [base (subs name 0 (- (count name) 5))
|
||||
segs (str/split base "/")
|
||||
seg-n (count segs)
|
||||
seg-1 (nth segs 0 nil)
|
||||
seg-2 (nth segs 1 nil)
|
||||
seg-3 (nth segs 2 nil)]
|
||||
|
||||
(defn- match-typography-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/typographies/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
(if-not (and (pos? seg-n) (every? #(pos? (count %)) segs))
|
||||
index
|
||||
(cond
|
||||
;; objects/<object-id>.json
|
||||
(and (= seg-n 2) (= seg-1 "objects"))
|
||||
(update index :objects bfc/conj-vec
|
||||
{:entry entry :id (parse-uuid seg-2)})
|
||||
|
||||
(defn- match-tokens-lib-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/tokens.json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry}))))
|
||||
;; files/<file-id>/tokens.json
|
||||
(and (= seg-n 3) (= seg-1 "files") (= seg-3 "tokens"))
|
||||
(update-in index [:tokens seg-2] bfc/conj-vec {:entry entry})
|
||||
|
||||
(defn- match-thumbnail-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/thumbnails/([^/]+)/([^/]+)/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ tag page-id frame-id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:tag tag
|
||||
:page-id (parse-uuid page-id)
|
||||
:frame-id (parse-uuid frame-id)
|
||||
:file-id file-id}))))
|
||||
;; files/<file-id>/thumbnails/<tag>/<page-id>/<frame-id>.json
|
||||
(and (= seg-n 6) (= seg-1 "files") (= seg-3 "thumbnails"))
|
||||
(update-in index [:thumbnails seg-2] bfc/conj-vec
|
||||
{:entry entry
|
||||
:tag (nth segs 3)
|
||||
:page-id (parse-uuid (nth segs 4))
|
||||
:frame-id (parse-uuid (nth segs 5))
|
||||
:file-id seg-2})
|
||||
|
||||
(defn- match-page-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/pages/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
;; files/<file-id>/pages/<page-id>.json
|
||||
(and (= seg-n 4) (= seg-1 "files") (= seg-3 "pages"))
|
||||
(update-in index [:pages seg-2] bfc/conj-vec
|
||||
{:entry entry :id (parse-uuid (nth segs 3))})
|
||||
|
||||
(defn- match-shape-entry-fn
|
||||
[file-id page-id]
|
||||
(let [pattern (str "^files/" file-id "/pages/" page-id "/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:page-id page-id
|
||||
:id (parse-uuid id)}))))
|
||||
;; files/<file-id>/pages/<page-id>/<shape-id>.json
|
||||
(and (= seg-n 5) (= seg-1 "files") (= seg-3 "pages"))
|
||||
(update-in index [:shapes seg-2 (nth segs 3)] bfc/conj-vec
|
||||
{:entry entry
|
||||
:page-id (nth segs 3)
|
||||
:id (parse-uuid (nth segs 4))})
|
||||
|
||||
(defn- match-storage-entry-fn
|
||||
[]
|
||||
(let [pattern "^objects/([^/]+).json$"
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
;; files/<file-id>/<category>/<object-id>.json
|
||||
(and (= seg-n 4)
|
||||
(= seg-1 "files")
|
||||
(contains? file-object-entry-categories seg-3))
|
||||
(update-in index [(keyword seg-3) seg-2] bfc/conj-vec
|
||||
{:entry entry :id (parse-uuid (nth segs 3))})
|
||||
|
||||
:else
|
||||
index)))))
|
||||
|
||||
(defn- index-entries
|
||||
"Classify all the provided zip entries in a single pass and group
|
||||
them by their path shape, so import consumers can lookup their
|
||||
entries per file (and per page) instead of rescanning the whole
|
||||
entry collection for every file and page."
|
||||
[entries]
|
||||
(reduce (fn [index entry]
|
||||
(index-entry-name index (zip-entry-name entry) entry))
|
||||
{}
|
||||
entries))
|
||||
|
||||
(defn- read-entry
|
||||
[^ZipFile input entry]
|
||||
@@ -644,8 +640,8 @@
|
||||
(validate-plugin-data))))
|
||||
|
||||
(defn- read-file-media
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(->> (keep (match-media-entry-fn file-id) entries)
|
||||
[{:keys [::bfc/input ::entries-index]} file-id]
|
||||
(->> (get-in entries-index [:media (str file-id)])
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-media)
|
||||
@@ -664,8 +660,8 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-colors
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(->> (keep (match-color-entry-fn file-id) entries)
|
||||
[{:keys [::bfc/input ::entries-index]} file-id]
|
||||
(->> (get-in entries-index [:colors (str file-id)])
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-color)
|
||||
@@ -678,7 +674,7 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-components
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
[{:keys [::bfc/input ::entries-index]} file-id]
|
||||
(let [clean-component-post-decode
|
||||
(fn [component]
|
||||
(d/update-when component :objects
|
||||
@@ -696,7 +692,7 @@
|
||||
objects
|
||||
objects))))]
|
||||
|
||||
(->> (keep (match-component-entry-fn file-id) entries)
|
||||
(->> (get-in entries-index [:components (str file-id)])
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(clean-component-pre-decode)
|
||||
@@ -710,8 +706,8 @@
|
||||
(not-empty))))
|
||||
|
||||
(defn- read-file-typographies
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(->> (keep (match-typography-entry-fn file-id) entries)
|
||||
[{:keys [::bfc/input ::entries-index]} file-id]
|
||||
(->> (get-in entries-index [:typographies (str file-id)])
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-typography)
|
||||
@@ -724,16 +720,16 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-tokens-lib
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(when-let [entry (d/seek (match-tokens-lib-entry-fn file-id) entries)]
|
||||
[{:keys [::bfc/input ::entries-index]} file-id]
|
||||
(when-let [{:keys [entry]} (first (get-in entries-index [:tokens (str file-id)]))]
|
||||
(events/tap :progress {:section :tokens-lib :file-id file-id})
|
||||
(->> (read-plain-entry input entry)
|
||||
(decode-tokens-lib)
|
||||
(validate-tokens-lib))))
|
||||
|
||||
(defn- read-file-shapes
|
||||
[{:keys [::bfc/input ::entries] :as cfg} file-id page-id]
|
||||
(->> (keep (match-shape-entry-fn file-id page-id) entries)
|
||||
[{:keys [::bfc/input ::entries-index] :as cfg} file-id page-id]
|
||||
(->> (get-in entries-index [:shapes (str file-id) (str page-id)])
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(bfl/clean-shape-pre-decode)
|
||||
@@ -746,8 +742,8 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-pages
|
||||
[{:keys [::bfc/input ::entries] :as cfg} file-id]
|
||||
(->> (keep (match-page-entry-fn file-id) entries)
|
||||
[{:keys [::bfc/input ::entries-index] :as cfg} file-id]
|
||||
(->> (get-in entries-index [:pages (str file-id)])
|
||||
(keep (fn [{:keys [id entry]}]
|
||||
(let [page (->> (read-entry input entry)
|
||||
(decode-page))
|
||||
@@ -762,8 +758,8 @@
|
||||
(d/ordered-map))))
|
||||
|
||||
(defn- read-file-thumbnails
|
||||
[{:keys [::bfc/input ::entries] :as cfg} file-id]
|
||||
(->> (keep (match-thumbnail-entry-fn file-id) entries)
|
||||
[{:keys [::bfc/input ::entries-index] :as cfg} file-id]
|
||||
(->> (get-in entries-index [:thumbnails (str file-id)])
|
||||
(reduce (fn [result {:keys [page-id frame-id tag entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-file-thumbnail)
|
||||
@@ -892,7 +888,7 @@
|
||||
(bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp)))))))
|
||||
|
||||
(defn- import-storage-objects
|
||||
[{:keys [::bfc/input ::entries ::bfc/timestamp] :as cfg}]
|
||||
[{:keys [::bfc/input ::entries-index ::bfc/timestamp] :as cfg}]
|
||||
(events/tap :progress {:section :storage-objects})
|
||||
|
||||
;; IMPORTANT: we strongly do not reuse the main connection that can
|
||||
@@ -903,7 +899,7 @@
|
||||
;; what the storage subsystem registers in other parallel
|
||||
;; transaction
|
||||
(let [storage (sto/resolve cfg)
|
||||
entries (keep (match-storage-entry-fn) entries)]
|
||||
entries (:objects entries-index)]
|
||||
|
||||
(doseq [{:keys [id entry]} entries]
|
||||
(let [object (-> (read-entry input entry)
|
||||
@@ -1058,7 +1054,7 @@
|
||||
|
||||
(defn- import-files*
|
||||
[{:keys [::manifest] :as cfg}]
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
(bfc/configure-database-timeouts! cfg)
|
||||
|
||||
(vswap! bfc/*state* update :index bfc/update-index (:files manifest) :id)
|
||||
|
||||
@@ -1126,7 +1122,7 @@
|
||||
:hint "unable to perform in-place update with binfile containing more than 1 file"
|
||||
:manifest manifest))
|
||||
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
(bfc/configure-database-timeouts! cfg)
|
||||
|
||||
(let [ref-file (bfc/get-minimal-file cfg file-id ::db/for-update true)
|
||||
file (first (get manifest :files))
|
||||
@@ -1165,7 +1161,7 @@
|
||||
:found (count entries))))
|
||||
|
||||
cfg (-> cfg
|
||||
(assoc ::entries entries)
|
||||
(assoc ::entries-index (index-entries entries))
|
||||
(assoc ::manifest manifest)
|
||||
(assoc ::bfc/timestamp timestamp))]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.arrow
|
||||
"Bulk Ladybug ingest through in-memory Arrow.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.debug
|
||||
"In-memory Ladybug sessions for the debug graph console."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.ingest
|
||||
"Penpot file -> Ladybug graph projection."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.ladybug
|
||||
"Ladybug access layer for graph-backed Penpot.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.meta
|
||||
"`GraphMeta`: the graph's own account of who built it and from what.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.projection.document
|
||||
"Project a Penpot file-data map into Ladybug nodes and structural edges.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.projection.transforms
|
||||
"Derived graph links: edges a reader could compute from the projected
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.report
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.schema
|
||||
"Ladybug DDL facade for the graph-backed Penpot vertical slice.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.schema.contract
|
||||
"Deliberate choices in Penpot's graph schema, recorded as data.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.schema.nodes
|
||||
"Single source of truth for graph node tables.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.schema.projection
|
||||
"Derive Ladybug node column schemas from Penpot Malli sources.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.schema.types
|
||||
"Map Malli schemas to Ladybug column types.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.schema.values
|
||||
"Shape a Penpot value into the plain data its Ladybug column type wants.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.stats
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.graph.sync
|
||||
"Incremental Ladybug graph updates from Penpot file-change events."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.rpc.commands.plugins
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.storage.pending-gc
|
||||
"A maintenance task that reclaims storage objects created in 'pending'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.tasks.demo-purge
|
||||
"Task handler for delayed demo profile deletion. Submitted at demo
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.common.thumbnails :as thc]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -55,46 +56,47 @@
|
||||
(:result out)))
|
||||
|
||||
(defn- prepare-simple-file
|
||||
[profile]
|
||||
(let [page-id-1 (uuid/custom 1 1)
|
||||
page-id-2 (uuid/custom 1 2)
|
||||
shape-id (uuid/custom 2 1)
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-page
|
||||
:name "test 1"
|
||||
:id page-id-1}
|
||||
{:type :add-page
|
||||
:name "test 2"
|
||||
:id page-id-2}])
|
||||
([profile] (prepare-simple-file profile 1))
|
||||
([profile idx]
|
||||
(let [page-id-1 (uuid/custom 1 1)
|
||||
page-id-2 (uuid/custom 1 2)
|
||||
shape-id (uuid/custom 2 1)
|
||||
file (th/create-file* idx {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-page
|
||||
:name "test 1"
|
||||
:id page-id-1}
|
||||
{:type :add-page
|
||||
:name "test 2"
|
||||
:id page-id-2}])
|
||||
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-obj
|
||||
:page-id page-id-1
|
||||
:id shape-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-id
|
||||
:name "image"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :rect})}])
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-obj
|
||||
:page-id page-id-1
|
||||
:id shape-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-id
|
||||
:name "image"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :rect})}])
|
||||
|
||||
(dissoc file :data)))
|
||||
(dissoc file :data))))
|
||||
|
||||
(def ^:private svg-raw-page-id (uuid/custom 1 1))
|
||||
(def ^:private svg-raw-root-id (uuid/custom 3 1))
|
||||
@@ -103,58 +105,59 @@
|
||||
(defn- prepare-svg-raw-file
|
||||
"A file containing an svg-raw subtree (an svg-raw parent with an
|
||||
svg-raw child), which is what importing an SVG produces."
|
||||
[profile]
|
||||
(let [page-id svg-raw-page-id
|
||||
root-id svg-raw-root-id
|
||||
child-id svg-raw-child-id
|
||||
([profile] (prepare-svg-raw-file profile 1))
|
||||
([profile idx]
|
||||
(let [page-id svg-raw-page-id
|
||||
root-id svg-raw-root-id
|
||||
child-id svg-raw-child-id
|
||||
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-page
|
||||
:name "page 1"
|
||||
:id page-id}])
|
||||
file (th/create-file* idx {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-page
|
||||
:name "page 1"
|
||||
:id page-id}])
|
||||
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-obj
|
||||
:page-id page-id
|
||||
:id root-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id root-id
|
||||
:name "svg-root"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :svg-raw
|
||||
:content {:tag :svg :attrs {} :content []}})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id child-id
|
||||
:parent-id root-id
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id child-id
|
||||
:name "svg-text"
|
||||
:frame-id uuid/zero
|
||||
:parent-id root-id
|
||||
:type :svg-raw
|
||||
:content {:tag :text :attrs {} :content []}})}])
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-obj
|
||||
:page-id page-id
|
||||
:id root-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id root-id
|
||||
:name "svg-root"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :svg-raw
|
||||
:content {:tag :svg :attrs {} :content []}})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id child-id
|
||||
:parent-id root-id
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id child-id
|
||||
:name "svg-text"
|
||||
:frame-id uuid/zero
|
||||
:parent-id root-id
|
||||
:type :svg-raw
|
||||
:content {:tag :text :attrs {} :content []}})}])
|
||||
|
||||
(dissoc file :data)))
|
||||
(dissoc file :data))))
|
||||
|
||||
(t/deftest import-binfile-v3-preserves-svg-raw-children
|
||||
(let [profile (th/create-profile* 1)
|
||||
@@ -1967,3 +1970,251 @@
|
||||
d)))]
|
||||
(t/is (= :validation (:type out)))
|
||||
(t/is (= :max-file-size-reached (:code out))))))
|
||||
|
||||
(t/deftest import-configures-finite-transaction-idle-timeout
|
||||
;; The binfile import transaction must set a finite ceiling for
|
||||
;; idle_in_transaction_session_timeout (20 min) instead of disabling
|
||||
;; it entirely: a stalled import must not retain a pool connection
|
||||
;; without any upper bound. The ceiling must be scoped with SET
|
||||
;; LOCAL: once the transaction ends, the pool session default must
|
||||
;; be restored for subsequent transactions.
|
||||
(let [pg-setting-sql ["SELECT setting FROM pg_settings WHERE name = 'idle_in_transaction_session_timeout'"]
|
||||
in-tx (db/tx-run! th/*system*
|
||||
(fn [cfg]
|
||||
(bfc/configure-database-timeouts! cfg)
|
||||
(:setting (db/exec-one! cfg pg-setting-sql))))
|
||||
;; SET LOCAL must not leak past the transaction boundary
|
||||
after (db/tx-run! th/*system*
|
||||
(fn [cfg]
|
||||
(:setting (db/exec-one! cfg pg-setting-sql))))]
|
||||
(t/is (= "1200000" in-tx))
|
||||
(t/is (= "300000" after))))
|
||||
|
||||
(def ^:private index-test-file-id "22222222-2222-2222-2222-222222222222")
|
||||
(def ^:private index-test-page-id "44444444-4444-4444-4444-444444444444")
|
||||
|
||||
(defn- index-entries-of
|
||||
[names]
|
||||
(@#'v3/index-entries (map #(java.util.zip.ZipEntry. %) names)))
|
||||
|
||||
(t/deftest index-entries-classifies-entry-names
|
||||
(let [f index-test-file-id
|
||||
page index-test-page-id
|
||||
index (index-entries-of
|
||||
["manifest.json"
|
||||
(str "files/" f ".json")
|
||||
(str "files/" f "/plugin-data.json")
|
||||
(str "files/" f "/tokens.json")
|
||||
"objects/11111111-1111-1111-1111-111111111111.json"
|
||||
(str "files/" f "/media/33333333-3333-3333-3333-333333333333.json")
|
||||
(str "files/" f "/colors/not-a-uuid.json")
|
||||
(str "files/" f "/components/comp.json")
|
||||
(str "files/" f "/typographies/t.json")
|
||||
(str "files/" f "/pages/" page ".json")
|
||||
(str "files/" f "/pages/" page "/shape.json")
|
||||
(str "files/" f "/thumbnails/medium/" page "/frame.json")])]
|
||||
|
||||
(t/is (= [(parse-uuid "11111111-1111-1111-1111-111111111111")]
|
||||
(mapv :id (:objects index))))
|
||||
|
||||
(t/is (= #{f} (set (keys (:media index)))))
|
||||
(t/is (= [(parse-uuid "33333333-3333-3333-3333-333333333333")]
|
||||
(mapv :id (get (:media index) f))))
|
||||
|
||||
(t/is (= #{f} (set (keys (:colors index)))))
|
||||
;; non-uuid ids are preserved as nil, same as the matchers do today
|
||||
(t/is (= [nil] (mapv :id (get (:colors index) f))))
|
||||
|
||||
(t/is (= #{f} (set (keys (:components index)))))
|
||||
(t/is (= #{f} (set (keys (:typographies index)))))
|
||||
|
||||
(t/is (= #{f} (set (keys (:pages index)))))
|
||||
(t/is (= [(parse-uuid page)] (mapv :id (get (:pages index) f))))
|
||||
|
||||
(t/is (= #{f} (set (keys (:shapes index)))))
|
||||
(t/is (= #{page} (set (keys (get (:shapes index) f)))))
|
||||
(t/is (= [nil] (mapv :id (get (get (:shapes index) f) page))))
|
||||
|
||||
(t/is (= #{f} (set (keys (:thumbnails index)))))
|
||||
(let [thumb (first (get (:thumbnails index) f))]
|
||||
(t/is (= "medium" (:tag thumb)))
|
||||
(t/is (= (parse-uuid page) (:page-id thumb)))
|
||||
(t/is (= (parse-uuid "frame") (:frame-id thumb)))
|
||||
(t/is (= f (:file-id thumb))))
|
||||
|
||||
(t/is (= #{f} (set (keys (:tokens index)))))))
|
||||
|
||||
(t/deftest index-entries-ignores-unknown-entry-names
|
||||
;; unknown paths, wrong depth, wrong suffix or non-uuid file segments
|
||||
;; are all ignored, same as today's anchored regexes
|
||||
(t/is (empty? (index-entries-of ["manifest.json"])))
|
||||
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id ".json")])))
|
||||
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/plugin-data.json")])))
|
||||
(t/is (empty? (index-entries-of ["objects/a/b.json"])))
|
||||
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/media/a/b.json")])))
|
||||
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/media/a.txt")])))
|
||||
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/unknown/a.json")])))
|
||||
(t/is (empty? (index-entries-of [(str "files//media/a.json")]))))
|
||||
|
||||
(t/deftest index-entries-requires-literal-json-suffix
|
||||
;; deliberate tightening vs today's regexes: the dot is unescaped in
|
||||
;; `([^/]+).json$` / `tokens.json$`, so today `tokensXjson` and
|
||||
;; `objects/x-json` DO match; the classifier requires a literal
|
||||
;; `.json` suffix and ignores them
|
||||
(t/is (empty? (index-entries-of [(str "files/" index-test-file-id "/tokensXjson")])))
|
||||
(t/is (empty? (index-entries-of ["objects/x-json"]))))
|
||||
|
||||
(t/deftest import-binfile-v3-multiple-files-preserves-per-file-content
|
||||
;; the entries index must attribute every zip entry to the file it
|
||||
;; belongs to: with several files in the same manifest, pages and
|
||||
;; shapes of one file must not leak into another. The file also
|
||||
;; carries library content (color, typography, component and tokens
|
||||
;; set) so every rewired consumer is exercised on its present path.
|
||||
(let [profile (th/create-profile* 1)
|
||||
simple (prepare-simple-file profile)
|
||||
color-id (uuid/custom 5 1)
|
||||
typo-id (uuid/custom 5 2)
|
||||
comp-id (uuid/custom 5 3)
|
||||
svg (prepare-svg-raw-file profile 2)
|
||||
output (tmp/tempfile :suffix ".zip")]
|
||||
|
||||
(update-file!
|
||||
:file-id (:id simple)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-color
|
||||
:color {:id color-id
|
||||
:name "import-color"
|
||||
:color "#FF0000"}}
|
||||
{:type :add-typography
|
||||
:typography {:id typo-id
|
||||
:name "import-typography"
|
||||
:font-id "source-sans-pro"
|
||||
:font-family "Source Sans Pro"
|
||||
:font-variant-id "regular"
|
||||
:font-size "16"
|
||||
:font-weight "400"
|
||||
:font-style "normal"
|
||||
:line-height "1.4"
|
||||
:letter-spacing "0"
|
||||
:text-transform "none"}}
|
||||
{:type :add-component
|
||||
:id comp-id
|
||||
:name "import-component"
|
||||
:path ""
|
||||
:main-instance-id (uuid/custom 2 1)
|
||||
:main-instance-page (uuid/custom 1 1)}
|
||||
{:type :set-tokens-lib
|
||||
:tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "ImportSet")))}])
|
||||
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id simple) (:id svg)})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output))
|
||||
|
||||
;; import returns the imported file ids plus the library link
|
||||
;; resolution; the test only needs the ids
|
||||
(let [result (:file-ids (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!)))
|
||||
files (map #(bfc/get-file th/*system* %) result)
|
||||
svg-imported (some #(when (contains? (get-in % [:data :pages-index
|
||||
svg-raw-page-id
|
||||
:objects])
|
||||
svg-raw-root-id)
|
||||
%)
|
||||
files)
|
||||
simple-imported (some #(when (contains? (get-in % [:data :pages-index
|
||||
(uuid/custom 1 1)
|
||||
:objects])
|
||||
(uuid/custom 2 1))
|
||||
%)
|
||||
files)]
|
||||
|
||||
(t/is (= 2 (count result)))
|
||||
(t/is (= 2 (count (distinct result))))
|
||||
(t/is (some? svg-imported))
|
||||
(t/is (some? simple-imported))
|
||||
|
||||
;; the svg-raw file keeps its subtree on its own page (plus the
|
||||
;; default page created by create-file)
|
||||
(t/is (= [svg-raw-child-id]
|
||||
(get-in svg-imported [:data :pages-index svg-raw-page-id
|
||||
:objects svg-raw-root-id :shapes])))
|
||||
(t/is (= 2 (count (get-in svg-imported [:data :pages-index]))))
|
||||
|
||||
;; the simple file keeps its default page plus its two pages and
|
||||
;; its shape
|
||||
(t/is (= 3 (count (get-in simple-imported [:data :pages-index]))))
|
||||
(t/is (contains? (get-in simple-imported
|
||||
[:data :pages-index (uuid/custom 1 1) :objects])
|
||||
(uuid/custom 2 1)))
|
||||
|
||||
;; library content is restored on its present path: the
|
||||
;; consumers' get-in keys must match the classifier buckets
|
||||
(t/is (some? (get-in simple-imported [:data :colors color-id])))
|
||||
(t/is (some? (get-in simple-imported [:data :typographies typo-id])))
|
||||
(t/is (some? (get-in simple-imported [:data :components comp-id])))
|
||||
(t/is (= ["ImportSet"]
|
||||
(vec (ctob/get-set-names
|
||||
(get-in simple-imported [:data :tokens-lib]))))))))
|
||||
|
||||
(t/deftest import-binfile-v3-restores-media-objects
|
||||
;; storage objects, per-file media entries and object thumbnails
|
||||
;; are classified through the entries index and restored end to end
|
||||
(let [profile (th/create-profile* 1)
|
||||
file (prepare-file-with-media profile)
|
||||
thumb-page-id (uuid/custom 1 1)
|
||||
thumb-frame-id (uuid/custom 6 1)
|
||||
thumb-tag "medium"
|
||||
output (tmp/tempfile :suffix ".zip")]
|
||||
|
||||
;; a thumbnail row backed by the same storage object as the media
|
||||
;; object, so the export produces a thumbnails/ zip entry
|
||||
(let [mobj (th/db-get :file-media-object {:file-id (:id file)})]
|
||||
(db/insert! th/*system* :file-tagged-object-thumbnail
|
||||
{:file-id (:id file)
|
||||
:tag thumb-tag
|
||||
:object-id (thc/fmt-object-id {:file-id (:id file)
|
||||
:page-id thumb-page-id
|
||||
:frame-id thumb-frame-id
|
||||
:tag thumb-tag})
|
||||
:media-id (:media-id mobj)}))
|
||||
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id file)})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output))
|
||||
|
||||
;; import returns the imported file ids plus the library link
|
||||
;; resolution; the test only needs the ids
|
||||
(let [result (:file-ids (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!)))
|
||||
mobjs (db/query th/*system* :file-media-object
|
||||
{:file-id (first result)})
|
||||
thumbs (db/query th/*system* :file-tagged-object-thumbnail
|
||||
{:file-id (first result)})]
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (pos? (count mobjs)))
|
||||
(t/is (every? some? (map :media-id mobjs)))
|
||||
|
||||
;; the thumbnail is restored with its object-id rebuilt around
|
||||
;; the new file id and a media-id that resolves to storage
|
||||
(t/is (= 1 (count thumbs)))
|
||||
(let [thumb (first thumbs)]
|
||||
(t/is (= thumb-tag (:tag thumb)))
|
||||
(t/is (= (str (first result) "/" thumb-page-id "/" thumb-frame-id "/" thumb-tag)
|
||||
(:object-id thumb)))
|
||||
(t/is (some? (:media-id thumb)))))))
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.demo-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.graph-binder-gate-test
|
||||
"Binder gate for the incremental-sync statement templates.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.graph-sync-parity-test
|
||||
"Cold projection and incremental sync are two implementations of one mapping,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.passwords-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.rpc-demo-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.rpc-plugins-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.common.types.path.fit
|
||||
"Curve fitting helpers."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.common.types.path.selection
|
||||
"Transforms selected path nodes and handlers."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.common.types.tokens-status
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns common-tests.files-migrations-0026-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns common-tests.types.tokens-status-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.auth
|
||||
"Resolves the caller's session cookie to a real profile id.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.handlers.export
|
||||
"Handle export jobs"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.handlers.jobs
|
||||
"REST surface for export jobs, under `/api/export/jobs`.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.jobs
|
||||
"Export job model and lifecycle.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.jobs.scheduler
|
||||
"Admission control for export jobs.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.jobs.store
|
||||
"Redis persistence for export jobs.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.jobs.utils
|
||||
"Temp file ownership for export jobs.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.router
|
||||
"Method + path dispatch.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.wasm.pool
|
||||
"Pool of headless render workers.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.wasm.render
|
||||
"Headless render pipeline: renders exports with the render-wasm Skia pipeline,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.wasm.worker
|
||||
"Render worker entry point.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns exporter-tests.export-shapes-test
|
||||
"Chunking of the browser backend."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns exporter-tests.jobs-test
|
||||
"Job state machine. Runs without redis: a store write with no connection is
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns exporter-tests.scheduler-test
|
||||
"Admission control. A headless job leases one render worker for its whole run,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns exporter-tests.wasm-pool-test
|
||||
"Worker leasing, against a stub pool: `with-worker` must give the worker back
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.main.data.workspace.path.clipboard
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.path-state
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.logic.path-actions-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-clipboard-test
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-helpers-test
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-lifecycle-test
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-test-helpers
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
|
||||
(ns frontend-tests.logic.path-tools-test
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.logic.wasm-modifiers-nil-id-test
|
||||
"Reproduces the production crash \"Cannot read properties of null
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.flex-test
|
||||
(:require
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.user-test
|
||||
(:require
|
||||
|
||||
@@ -124,21 +124,14 @@ macro_rules! with_current_shape {
|
||||
#[cfg(test)]
|
||||
pub(crate) struct TestRenderResourcesGuard {
|
||||
prev: *mut RenderResources,
|
||||
_lock: std::sync::MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
static TEST_RENDER_RESOURCES_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
#[cfg(test)]
|
||||
impl TestRenderResourcesGuard {
|
||||
pub(crate) fn install(resources: &mut RenderResources) -> Self {
|
||||
let lock = TEST_RENDER_RESOURCES_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
let prev = unsafe { RENDER_RESOURCES };
|
||||
unsafe { RENDER_RESOURCES = resources as *mut _ };
|
||||
Self { prev, _lock: lock }
|
||||
Self { prev }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+50
-316
@@ -268,6 +268,7 @@ fn difference(
|
||||
.iter()
|
||||
.filter(|s| path_a.contains(to_point(s.evaluate(TValue::Parametric(0.5)))))
|
||||
.copied()
|
||||
.map(|s| s.reverse())
|
||||
.map(|b| (BezierSource::B, b)),
|
||||
);
|
||||
|
||||
@@ -277,53 +278,16 @@ fn difference(
|
||||
fn exclusion(segments_a: Vec<Bezier>, segments_b: Vec<Bezier>) -> Vec<(BezierSource, Bezier)> {
|
||||
let mut result = Vec::new();
|
||||
result.extend(segments_a.iter().copied().map(|b| (BezierSource::A, b)));
|
||||
result.extend(segments_b.iter().copied().map(|b| (BezierSource::B, b)));
|
||||
result.extend(
|
||||
segments_b
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|s| s.reverse())
|
||||
.map(|b| (BezierSource::B, b)),
|
||||
);
|
||||
result
|
||||
}
|
||||
|
||||
// Mirrors `app.common.types.path.subpath/clockwise?`.
|
||||
fn is_clockwise(path: &Path) -> bool {
|
||||
let mut points: Vec<(f32, f32)> = Vec::new();
|
||||
|
||||
for segment in path.segments().iter() {
|
||||
match *segment {
|
||||
Segment::MoveTo(p) => {
|
||||
if !points.is_empty() {
|
||||
break;
|
||||
}
|
||||
points.push(p);
|
||||
}
|
||||
Segment::LineTo(p) => points.push(p),
|
||||
Segment::CurveTo((_, _, p)) => points.push(p),
|
||||
Segment::Close => break,
|
||||
}
|
||||
}
|
||||
|
||||
if points.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut signed_area = 0.0f64;
|
||||
for i in 0..points.len() {
|
||||
let (x1, y1) = points[i];
|
||||
let (x2, y2) = points[(i + 1) % points.len()];
|
||||
signed_area += f64::from(x1) * f64::from(y2) - f64::from(x2) * f64::from(y1);
|
||||
}
|
||||
|
||||
signed_area > 0.0
|
||||
}
|
||||
|
||||
// The kept pieces of B must point the same way round as the kept pieces of A. Not the
|
||||
// `path.bool/content-bool-pair` rule, which reverses intersection on same winding and
|
||||
// relies on `subpath/merge-paths` flipping subpaths when it joins them.
|
||||
fn should_reverse_b(bool_type: BoolType, a_is_clockwise: bool, path_b: &Path) -> bool {
|
||||
let same_winding = a_is_clockwise == is_clockwise(path_b);
|
||||
match bool_type {
|
||||
BoolType::Union | BoolType::Intersection => !same_winding,
|
||||
BoolType::Difference | BoolType::Exclusion => same_winding,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
enum BezierSource {
|
||||
A,
|
||||
@@ -341,14 +305,17 @@ fn pop_first_from_pool(pool: &mut BezierPool) -> Option<(BezierSource, Bezier)>
|
||||
pool.iter_mut().find_map(|e| e.take())
|
||||
}
|
||||
|
||||
// Same-source candidates get a tighter threshold so we stay on the original path. A
|
||||
// candidate that joins by its `end` points the wrong way, so we reverse it.
|
||||
// Find and remove the segment whose start point is closest to `end` within the
|
||||
// appropriate threshold. Same-source segments use a tight threshold
|
||||
// (INTERSECT_THRESHOLD_SAMEd) so we prefer staying on the same original path;
|
||||
// cross-source segments use a wider threshold (INTERSECT_THRESHOLD_DIFFERENT)
|
||||
// to allow switching paths at intersection points.
|
||||
fn find_next_in_pool(
|
||||
pool: &mut BezierPool,
|
||||
end: DVec2,
|
||||
source: BezierSource,
|
||||
) -> Option<(BezierSource, Bezier)> {
|
||||
let mut best: Option<(usize, bool)> = None;
|
||||
let mut best_idx: Option<usize> = None;
|
||||
let mut best_dist_sq = f64::MAX;
|
||||
|
||||
for (i, entry) in pool.iter().enumerate() {
|
||||
@@ -360,21 +327,16 @@ fn find_next_in_pool(
|
||||
} else {
|
||||
INTERSECT_THRESHOLD_DIFFERENT as f64
|
||||
};
|
||||
for (reversed, point) in [(false, bezier.start), (true, bezier.end)] {
|
||||
let dx = point.x - end.x;
|
||||
let dy = point.y - end.y;
|
||||
let dist_sq = dx * dx + dy * dy;
|
||||
if dist_sq <= threshold * threshold && dist_sq < best_dist_sq {
|
||||
best_dist_sq = dist_sq;
|
||||
best = Some((i, reversed));
|
||||
}
|
||||
let dx = bezier.start.x - end.x;
|
||||
let dy = bezier.start.y - end.y;
|
||||
let dist_sq = dx * dx + dy * dy;
|
||||
if dist_sq <= threshold * threshold && dist_sq < best_dist_sq {
|
||||
best_dist_sq = dist_sq;
|
||||
best_idx = Some(i);
|
||||
}
|
||||
}
|
||||
|
||||
let (idx, reversed) = best?;
|
||||
pool[idx]
|
||||
.take()
|
||||
.map(|(src, bezier)| (src, if reversed { bezier.reverse() } else { bezier }))
|
||||
best_idx.and_then(|i| pool[i].take())
|
||||
}
|
||||
|
||||
fn push_bezier(result: &mut Vec<Segment>, bezier: &Bezier) {
|
||||
@@ -448,45 +410,32 @@ fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec<Segment> {
|
||||
result
|
||||
}
|
||||
|
||||
fn bool_beziers(
|
||||
bool_type: BoolType,
|
||||
path_a: &Path,
|
||||
a_is_clockwise: bool,
|
||||
path_b: &Path,
|
||||
) -> (Vec<(BezierSource, Bezier)>, bool) {
|
||||
let (segs_a, mut segs_b) = split_segments(path_a, path_b);
|
||||
|
||||
if should_reverse_b(bool_type, a_is_clockwise, path_b) {
|
||||
for segment in segs_b.iter_mut() {
|
||||
*segment = segment.reverse();
|
||||
}
|
||||
pub fn bool_from_shapes(bool_type: BoolType, children_ids: &[Uuid], shapes: ShapesPoolRef) -> Path {
|
||||
if children_ids.is_empty() {
|
||||
return Path::default();
|
||||
}
|
||||
|
||||
let beziers = match bool_type {
|
||||
BoolType::Union => union(path_a, segs_a, path_b, segs_b),
|
||||
BoolType::Difference => difference(path_a, segs_a, path_b, segs_b),
|
||||
BoolType::Intersection => intersection(path_a, segs_a, path_b, segs_b),
|
||||
BoolType::Exclusion => exclusion(segs_a, segs_b),
|
||||
};
|
||||
|
||||
(beziers, path_a.is_even_odd() || path_b.is_even_odd())
|
||||
}
|
||||
|
||||
// Fold `paths` left to right; the first entry is the base operand.
|
||||
fn bool_fold(bool_type: BoolType, paths: &[Path]) -> Path {
|
||||
let Some((first, rest)) = paths.split_first() else {
|
||||
let Some(child) = shapes.get(&children_ids[children_ids.len() - 1]) else {
|
||||
return Path::default();
|
||||
};
|
||||
|
||||
let mut current_path = first.clone();
|
||||
// Every fold step chains A's fragments, which keep their direction, so the
|
||||
// accumulated path keeps this winding. Carry it instead of re-reading it from the
|
||||
// emitted segment list, whose subpath order and direction fall out of pool ordering.
|
||||
let is_clockwise_a = is_clockwise(¤t_path);
|
||||
let mut current_path = child.to_path(shapes);
|
||||
|
||||
for other_path in rest {
|
||||
let (beziers, is_even_odd) =
|
||||
bool_beziers(bool_type, ¤t_path, is_clockwise_a, other_path);
|
||||
for idx in (0..children_ids.len() - 1).rev() {
|
||||
let Some(other) = shapes.get(&children_ids[idx]) else {
|
||||
continue;
|
||||
};
|
||||
let other_path = other.to_path(shapes);
|
||||
|
||||
let (segs_a, segs_b) = split_segments(¤t_path, &other_path);
|
||||
|
||||
let is_even_odd = current_path.is_even_odd() || other_path.is_even_odd();
|
||||
let beziers = match bool_type {
|
||||
BoolType::Union => union(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Difference => difference(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Intersection => intersection(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Exclusion => exclusion(segs_a, segs_b),
|
||||
};
|
||||
|
||||
current_path = Path::new(beziers_to_segments(&beziers)).with_even_odd(is_even_odd);
|
||||
}
|
||||
@@ -494,16 +443,6 @@ fn bool_fold(bool_type: BoolType, paths: &[Path]) -> Path {
|
||||
current_path
|
||||
}
|
||||
|
||||
pub fn bool_from_shapes(bool_type: BoolType, children_ids: &[Uuid], shapes: ShapesPoolRef) -> Path {
|
||||
let paths: Vec<Path> = children_ids
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|id| shapes.get(id).map(|child| child.to_path(shapes)))
|
||||
.collect();
|
||||
|
||||
bool_fold(bool_type, &paths)
|
||||
}
|
||||
|
||||
pub fn update_bool_to_path(shape: &mut Shape, shapes: ShapesPoolRef) {
|
||||
let children_ids = shape.children_ids(true);
|
||||
|
||||
@@ -542,7 +481,6 @@ pub fn debug_render_bool_paths(
|
||||
};
|
||||
|
||||
let mut current_path = child.to_path(shapes);
|
||||
let is_clockwise_a = is_clockwise(¤t_path);
|
||||
|
||||
for idx in (0..children_ids.len() - 1).rev() {
|
||||
let Some(other) = shapes.get(&children_ids[idx]) else {
|
||||
@@ -550,12 +488,15 @@ pub fn debug_render_bool_paths(
|
||||
};
|
||||
let other_path = other.to_path(shapes);
|
||||
|
||||
let (beziers, is_even_odd) = bool_beziers(
|
||||
bool_data.bool_type,
|
||||
¤t_path,
|
||||
is_clockwise_a,
|
||||
&other_path,
|
||||
);
|
||||
let (segs_a, segs_b) = split_segments(¤t_path, &other_path);
|
||||
|
||||
let is_even_odd = current_path.is_even_odd() || other_path.is_even_odd();
|
||||
let beziers = match bool_data.bool_type {
|
||||
BoolType::Union => union(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Difference => difference(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Intersection => intersection(¤t_path, segs_a, &other_path, segs_b),
|
||||
BoolType::Exclusion => exclusion(segs_a, segs_b),
|
||||
};
|
||||
current_path = Path::new(beziers_to_segments(&beziers)).with_even_odd(is_even_odd);
|
||||
|
||||
if idx == 0 {
|
||||
@@ -631,210 +572,3 @@ pub fn debug_render_bool_paths(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn linear(from: (f64, f64), to: (f64, f64)) -> Bezier {
|
||||
Bezier::from_linear_coordinates(from.0, from.1, to.0, to.1)
|
||||
}
|
||||
|
||||
fn polygon(points: &[(f32, f32)]) -> Path {
|
||||
let mut segments = vec![Segment::MoveTo(points[0])];
|
||||
segments.extend(points[1..].iter().map(|p| Segment::LineTo(*p)));
|
||||
segments.push(Segment::Close);
|
||||
Path::new(segments)
|
||||
}
|
||||
|
||||
fn count(segments: &[Segment], f: fn(&Segment) -> bool) -> usize {
|
||||
segments.iter().filter(|s| f(s)).count()
|
||||
}
|
||||
|
||||
fn is_move_to(s: &Segment) -> bool {
|
||||
matches!(s, Segment::MoveTo(_))
|
||||
}
|
||||
|
||||
fn is_close(s: &Segment) -> bool {
|
||||
matches!(s, Segment::Close)
|
||||
}
|
||||
|
||||
fn ring_area(ring: &[(f32, f32)]) -> f64 {
|
||||
let mut area = 0.0f64;
|
||||
for i in 0..ring.len() {
|
||||
let (x1, y1) = ring[i];
|
||||
let (x2, y2) = ring[(i + 1) % ring.len()];
|
||||
area += f64::from(x1) * f64::from(y2) - f64::from(x2) * f64::from(y1);
|
||||
}
|
||||
area / 2.0
|
||||
}
|
||||
|
||||
fn signed_area(segments: &[Segment]) -> f64 {
|
||||
let mut total = 0.0f64;
|
||||
let mut ring: Vec<(f32, f32)> = Vec::new();
|
||||
|
||||
for segment in segments {
|
||||
match *segment {
|
||||
Segment::MoveTo(p) => {
|
||||
total += ring_area(&ring);
|
||||
ring.clear();
|
||||
ring.push(p);
|
||||
}
|
||||
Segment::LineTo(p) | Segment::CurveTo((_, _, p)) => ring.push(p),
|
||||
Segment::Close => {
|
||||
total += ring_area(&ring);
|
||||
ring.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total + ring_area(&ring)
|
||||
}
|
||||
|
||||
// Operands and expected results taken from the CLJS bool (`app.common.types.path.bool`)
|
||||
// run on the same shapes: A clockwise, B and C counter-clockwise, all overlapping.
|
||||
const A_CW: [(f32, f32); 4] = [
|
||||
(100.0, 100.0),
|
||||
(300.0, 100.0),
|
||||
(300.0, 300.0),
|
||||
(100.0, 300.0),
|
||||
];
|
||||
const B_CCW: [(f32, f32); 4] = [
|
||||
(200.0, 200.0),
|
||||
(200.0, 400.0),
|
||||
(400.0, 400.0),
|
||||
(400.0, 200.0),
|
||||
];
|
||||
const C_CCW: [(f32, f32); 4] = [(60.0, 240.0), (60.0, 360.0), (260.0, 360.0), (260.0, 240.0)];
|
||||
|
||||
#[test]
|
||||
fn test_is_clockwise() {
|
||||
let cw = Path::new(vec![
|
||||
Segment::MoveTo((0.0, 0.0)),
|
||||
Segment::LineTo((10.0, 0.0)),
|
||||
Segment::LineTo((10.0, 10.0)),
|
||||
Segment::LineTo((0.0, 10.0)),
|
||||
Segment::Close,
|
||||
]);
|
||||
assert!(is_clockwise(&cw));
|
||||
|
||||
let ccw = Path::new(vec![
|
||||
Segment::MoveTo((0.0, 0.0)),
|
||||
Segment::LineTo((0.0, 10.0)),
|
||||
Segment::LineTo((10.0, 10.0)),
|
||||
Segment::LineTo((10.0, 0.0)),
|
||||
Segment::Close,
|
||||
]);
|
||||
assert!(!is_clockwise(&ccw));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_reverse_b_only_depends_on_relative_winding() {
|
||||
let cw = Path::new(vec![
|
||||
Segment::MoveTo((0.0, 0.0)),
|
||||
Segment::LineTo((10.0, 0.0)),
|
||||
Segment::LineTo((10.0, 10.0)),
|
||||
Segment::LineTo((0.0, 10.0)),
|
||||
Segment::Close,
|
||||
]);
|
||||
let ccw = Path::new(vec![
|
||||
Segment::MoveTo((0.0, 0.0)),
|
||||
Segment::LineTo((0.0, 10.0)),
|
||||
Segment::LineTo((10.0, 10.0)),
|
||||
Segment::LineTo((10.0, 0.0)),
|
||||
Segment::Close,
|
||||
]);
|
||||
|
||||
assert!(should_reverse_b(BoolType::Difference, true, &cw));
|
||||
assert!(!should_reverse_b(BoolType::Difference, true, &ccw));
|
||||
assert!(!should_reverse_b(BoolType::Union, true, &cw));
|
||||
assert!(should_reverse_b(BoolType::Union, true, &ccw));
|
||||
}
|
||||
|
||||
// Fragments from #11482: two point the wrong way, so joining them start-to-start only
|
||||
// left five open subpaths.
|
||||
#[test]
|
||||
fn test_beziers_to_segments_closes_reversed_fragments() {
|
||||
let beziers = vec![
|
||||
(
|
||||
BezierSource::A,
|
||||
linear((2764.00, -240.00), (2834.74, -110.71)),
|
||||
),
|
||||
(
|
||||
BezierSource::A,
|
||||
linear((2809.29, -85.26), (2693.26, -201.29)),
|
||||
),
|
||||
(
|
||||
BezierSource::A,
|
||||
linear((2718.71, -226.74), (2764.00, -240.00)),
|
||||
),
|
||||
(
|
||||
BezierSource::B,
|
||||
linear((2718.71, -226.74), (2834.74, -110.71)),
|
||||
),
|
||||
(
|
||||
BezierSource::B,
|
||||
linear((2809.29, -85.26), (2693.26, -201.29)),
|
||||
),
|
||||
];
|
||||
|
||||
let segments = beziers_to_segments(&beziers);
|
||||
|
||||
let moves = segments
|
||||
.iter()
|
||||
.filter(|s| matches!(s, Segment::MoveTo(_)))
|
||||
.count();
|
||||
let closes = segments
|
||||
.iter()
|
||||
.filter(|s| matches!(s, Segment::Close))
|
||||
.count();
|
||||
|
||||
assert_eq!(moves, 2);
|
||||
assert_eq!(closes, 2);
|
||||
// 3 fragments in the first subpath, 2 in the second, each dropping its closing LineTo.
|
||||
assert_eq!(segments.len(), 7);
|
||||
}
|
||||
|
||||
// #11482: A clockwise, B counter-clockwise. Reference (CLJS):
|
||||
// M100,100 L300,100 L300,200 L200,200 L200,300 L100,300 Z
|
||||
#[test]
|
||||
fn test_difference_with_opposite_winding_operand() {
|
||||
let result = bool_fold(BoolType::Difference, &[polygon(&A_CW), polygon(&B_CCW)]);
|
||||
let segments = result.segments();
|
||||
|
||||
assert_eq!(count(segments, is_move_to), 1);
|
||||
assert_eq!(count(segments, is_close), 1);
|
||||
assert!((signed_area(segments) - 30000.0).abs() < 1.0);
|
||||
assert!(is_clockwise(&result));
|
||||
}
|
||||
|
||||
// Reference (CLJS):
|
||||
// M100,100 L300,100 L300,200 L400,200 L400,400 L200,400 L200,300 L100,300 Z
|
||||
#[test]
|
||||
fn test_union_with_opposite_winding_operand() {
|
||||
let result = bool_fold(BoolType::Union, &[polygon(&A_CW), polygon(&B_CCW)]);
|
||||
let segments = result.segments();
|
||||
|
||||
assert_eq!(count(segments, is_move_to), 1);
|
||||
assert_eq!(count(segments, is_close), 1);
|
||||
assert!((signed_area(segments) - 70000.0).abs() < 1.0);
|
||||
assert!(is_clockwise(&result));
|
||||
}
|
||||
|
||||
// The second fold step must compare against A's winding, not against the winding of
|
||||
// the intermediate path, whose subpath order and direction fall out of pool ordering.
|
||||
// Reference (CLJS): M100,100 L300,100 L300,200 L200,200 L200,240 L100,240 Z
|
||||
#[test]
|
||||
fn test_difference_folds_three_opposite_winding_operands() {
|
||||
let result = bool_fold(
|
||||
BoolType::Difference,
|
||||
&[polygon(&A_CW), polygon(&B_CCW), polygon(&C_CCW)],
|
||||
);
|
||||
let segments = result.segments();
|
||||
|
||||
assert_eq!(count(segments, is_move_to), 1);
|
||||
assert_eq!(count(segments, is_close), 1);
|
||||
assert!((signed_area(segments) - 24000.0).abs() < 1.0);
|
||||
assert!(is_clockwise(&result));
|
||||
}
|
||||
}
|
||||
@@ -173,11 +173,9 @@ main() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Get tracked files matching our extensions, excluding this script itself
|
||||
# (it mentions the search string in its own usage docs).
|
||||
local files script_name
|
||||
script_name=$(basename "$0")
|
||||
files=$(git ls-files | grep -E "\.(${EXTENSIONS})$" | grep -v -F "$script_name" || true)
|
||||
# Get tracked files matching our extensions
|
||||
local files
|
||||
files=$(git ls-files | grep -E "\.(${EXTENSIONS})$" || true)
|
||||
|
||||
if [[ -z "$files" ]]; then
|
||||
log_warn "No tracked files found matching extensions: ${EXTENSIONS}"
|
||||
|
||||
Reference in new issue
Block a user