mirror of
https://github.com/penpot/penpot.git
synced 2025-03-26 06:31:26 -05:00
✨ Allow for nested frames
This commit is contained in:
parent
0bbd898173
commit
a9303c37c4
23 changed files with 540 additions and 283 deletions
|
@ -26,8 +26,8 @@
|
|||
(dm/export focus/is-in-focus?)
|
||||
|
||||
;; Indices
|
||||
(dm/export indices/calculate-z-index)
|
||||
(dm/export indices/update-z-index)
|
||||
#_(dm/export indices/calculate-z-index)
|
||||
#_(dm/export indices/update-z-index)
|
||||
(dm/export indices/generate-child-all-parents-index)
|
||||
(dm/export indices/generate-child-parent-index)
|
||||
(dm/export indices/create-clip-index)
|
||||
|
|
|
@ -211,7 +211,7 @@
|
|||
(let [invalid-targets (calculate-invalid-targets objects shape-id)]
|
||||
(and (contains? objects shape-id)
|
||||
(not (invalid-targets parent-id))
|
||||
(cph/valid-frame-target? objects parent-id shape-id))))
|
||||
#_(cph/valid-frame-target? objects parent-id shape-id))))
|
||||
|
||||
(insert-items [prev-shapes index shapes]
|
||||
(let [prev-shapes (or prev-shapes [])]
|
||||
|
|
|
@ -82,6 +82,8 @@
|
|||
:r1 :r2 :r3 :r4
|
||||
:selrect
|
||||
:points
|
||||
:show-content
|
||||
:hide-in-viewer
|
||||
|
||||
:opacity
|
||||
:blend-mode
|
||||
|
|
|
@ -13,21 +13,16 @@
|
|||
|
||||
(defn focus-objects
|
||||
[objects focus]
|
||||
(let [[ids-with-children z-index]
|
||||
(let [ids-with-children
|
||||
(when (d/not-empty? focus)
|
||||
[(into (conj focus uuid/zero)
|
||||
(mapcat (partial cph/get-children-ids objects))
|
||||
focus)
|
||||
(cpi/calculate-z-index objects)])
|
||||
|
||||
sort-by-z-index
|
||||
(fn [coll]
|
||||
(->> coll (sort-by (fn [a b] (- (get z-index a) (get z-index b))))))]
|
||||
(into (conj focus uuid/zero)
|
||||
(mapcat (partial cph/get-children-ids objects))
|
||||
focus))]
|
||||
|
||||
(cond-> objects
|
||||
(some? ids-with-children)
|
||||
(-> (select-keys ids-with-children)
|
||||
(assoc-in [uuid/zero :shapes] (sort-by-z-index focus))))))
|
||||
(assoc-in [uuid/zero :shapes] (cph/sort-z-index objects focus))))))
|
||||
|
||||
(defn filter-not-focus
|
||||
[objects focus ids]
|
||||
|
|
|
@ -141,6 +141,38 @@
|
|||
(keep lookup)))))
|
||||
|
||||
(defn get-frames-ids
|
||||
"Retrieves all frame objects as vector. It is not implemented in
|
||||
function of `get-immediate-children` for performance reasons. This
|
||||
function is executed in the render hot path."
|
||||
[objects]
|
||||
(let [lookup (d/getf objects)
|
||||
xform (comp (remove #(= uuid/zero %))
|
||||
(keep lookup)
|
||||
(filter frame-shape?)
|
||||
(map :id))]
|
||||
(->> (keys objects)
|
||||
(into [] xform))))
|
||||
|
||||
(defn get-frames
|
||||
"Retrieves all frame objects as vector. It is not implemented in
|
||||
function of `get-immediate-children` for performance reasons. This
|
||||
function is executed in the render hot path."
|
||||
[objects]
|
||||
(let [lookup (d/getf objects)
|
||||
xform (comp (remove #(= uuid/zero %))
|
||||
(keep lookup)
|
||||
(filter frame-shape?))]
|
||||
(->> (keys objects)
|
||||
(into [] xform))))
|
||||
|
||||
(defn get-nested-frames
|
||||
[objects frame-id]
|
||||
(into #{}
|
||||
(comp (filter frame-shape?)
|
||||
(map :id))
|
||||
(get-children objects frame-id)))
|
||||
|
||||
(defn get-root-frames-ids
|
||||
"Retrieves all frame objects as vector. It is not implemented in
|
||||
function of `get-immediate-children` for performance reasons. This
|
||||
function is executed in the render hot path."
|
||||
|
@ -152,7 +184,7 @@
|
|||
(->> (:shapes (lookup uuid/zero))
|
||||
(into [] xform))))
|
||||
|
||||
(defn get-frames
|
||||
(defn get-root-frames
|
||||
"Retrieves all frame objects as vector. It is not implemented in
|
||||
function of `get-immediate-children` for performance reasons. This
|
||||
function is executed in the render hot path."
|
||||
|
@ -163,15 +195,62 @@
|
|||
(->> (:shapes (lookup uuid/zero))
|
||||
(into [] xform))))
|
||||
|
||||
(defn- get-base
|
||||
[objects id-a id-b]
|
||||
|
||||
(let [parents-a (reverse (get-parents-seq objects id-a))
|
||||
parents-b (reverse (get-parents-seq objects id-b))
|
||||
|
||||
[base base-child-a base-child-b]
|
||||
(loop [parents-a (rest parents-a)
|
||||
parents-b (rest parents-b)
|
||||
base uuid/zero]
|
||||
(if (not= (first parents-a) (first parents-b))
|
||||
[base (first parents-a) (first parents-b)]
|
||||
(recur (rest parents-a) (rest parents-b) (first parents-a))))
|
||||
|
||||
index-base-a (when base-child-a (get-position-on-parent objects base-child-a))
|
||||
index-base-b (when base-child-b (get-position-on-parent objects base-child-b))]
|
||||
|
||||
[base index-base-a index-base-b]))
|
||||
|
||||
(defn is-shape-over-shape?
|
||||
[objects base-shape-id over-shape-id]
|
||||
|
||||
(let [[base parent-a parent-b] (get-base objects base-shape-id over-shape-id)]
|
||||
(cond
|
||||
(= base base-shape-id)
|
||||
;; over-shape is a child of base-shape. Will be over if base is a root-frame
|
||||
(= uuid/zero (get-in objects [base-shape-id :parent-id]))
|
||||
|
||||
(= base over-shape-id)
|
||||
(not= uuid/zero (get-in objects [over-shape-id :parent-id]))
|
||||
|
||||
:else
|
||||
(< parent-a parent-b))))
|
||||
|
||||
(defn sort-z-index
|
||||
[objects ids]
|
||||
(letfn [(comp [id-a id-b]
|
||||
(cond
|
||||
(= id-a id-b) 0
|
||||
(is-shape-over-shape? objects id-a id-b) 1
|
||||
:else -1))]
|
||||
(sort comp ids)))
|
||||
|
||||
(defn frame-id-by-position
|
||||
[objects position]
|
||||
(let [frames (get-frames objects)]
|
||||
(or
|
||||
(->> frames
|
||||
(reverse)
|
||||
(d/seek #(and position (gsh/has-point? % position)))
|
||||
:id)
|
||||
uuid/zero)))
|
||||
(let [frames (->> (get-frames objects)
|
||||
(filter #(and position (gsh/has-point? % position))))
|
||||
|
||||
top-frame
|
||||
(reduce (fn [current-top frame]
|
||||
(if (is-shape-over-shape? objects (:id current-top) (:id frame))
|
||||
frame
|
||||
current-top))
|
||||
(first frames)
|
||||
(rest frames))]
|
||||
(or (:id top-frame) uuid/zero)))
|
||||
|
||||
(declare indexed-shapes)
|
||||
|
||||
|
|
|
@ -11,10 +11,12 @@
|
|||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]))
|
||||
|
||||
(defn calculate-frame-z-index
|
||||
#_(defn calculate-frame-z-index
|
||||
[z-index frame-id base-idx objects]
|
||||
|
||||
(let [is-frame? (fn [id] (= :frame (get-in objects [id :type])))
|
||||
(let [is-root-frame? (fn [id]
|
||||
(and (= :frame (get-in objects [id :type]))
|
||||
(= uuid/zero (get-in objects [id :parent-id]))))
|
||||
children (or (get-in objects [frame-id :shapes]) [])]
|
||||
|
||||
(if (empty? children)
|
||||
|
@ -25,8 +27,8 @@
|
|||
z-index z-index]
|
||||
|
||||
(let [children (get-in objects [current :shapes])
|
||||
is-frame? (is-frame? current)
|
||||
pending (if (not is-frame?)
|
||||
is-root-frame? (is-root-frame? current)
|
||||
pending (if (not is-root-frame?)
|
||||
(d/concat-vec pending children)
|
||||
pending)]
|
||||
|
||||
|
@ -41,12 +43,12 @@
|
|||
;; internal z-index. To calculate the "final" z-index we add the shape z-index with
|
||||
;; the z-index of its frame. This way we can update the z-index per frame without
|
||||
;; the need of recalculate all the frames
|
||||
(defn calculate-z-index
|
||||
#_(defn calculate-z-index
|
||||
"Given a collection of shapes calculates their z-index. Greater index
|
||||
means is displayed over other shapes with less index."
|
||||
[objects]
|
||||
|
||||
(let [frames (cph/get-frames objects)
|
||||
(let [frames (cph/get-root-frames objects)
|
||||
|
||||
by-frame (cph/objects-by-frame objects)
|
||||
frame-base-idx (d/update-vals by-frame count)
|
||||
|
@ -57,7 +59,7 @@
|
|||
(fn [z-index {:keys [id]}]
|
||||
(calculate-frame-z-index z-index id (get frame-base-idx id) objects)) z-index))))
|
||||
|
||||
(defn update-z-index
|
||||
#_(defn update-z-index
|
||||
"Updates the z-index given a set of ids to change and the old and new objects
|
||||
representations"
|
||||
[z-index changed-ids old-objects new-objects]
|
||||
|
|
|
@ -52,6 +52,8 @@
|
|||
(s/def ::fill-color-ref-id (s/nilable uuid?))
|
||||
|
||||
(s/def ::hide-fill-on-export boolean?)
|
||||
(s/def ::show-content boolean?)
|
||||
(s/def ::hide-in-viewer boolean?)
|
||||
|
||||
(s/def ::file-thumbnail boolean?)
|
||||
(s/def ::masked-group? boolean?)
|
||||
|
@ -254,8 +256,7 @@
|
|||
:internal.shape.text.position-data/rtl
|
||||
:internal.shape.text.position-data/text
|
||||
:internal.shape.text.position-data/text-decoration
|
||||
:internal.shape.text.position-data/text-transform]
|
||||
))
|
||||
:internal.shape.text.position-data/text-transform]))
|
||||
|
||||
(s/def :internal.shape.text.position-data/x ::us/safe-number)
|
||||
(s/def :internal.shape.text.position-data/y ::us/safe-number)
|
||||
|
@ -303,7 +304,9 @@
|
|||
(defmethod shape-spec :frame [_]
|
||||
(s/and ::shape-attrs
|
||||
(s/keys :opt-un [::file-thumbnail
|
||||
::hide-fill-on-export])))
|
||||
::hide-fill-on-export
|
||||
::show-content
|
||||
::hide-in-viewer])))
|
||||
|
||||
(s/def ::shape
|
||||
(s/and (s/multi-spec shape-spec :type)
|
||||
|
|
|
@ -25,3 +25,170 @@
|
|||
|
||||
(def has-layout-item false)
|
||||
|
||||
(def size-presets
|
||||
[{:name "APPLE"}
|
||||
{:name "iPhone 12/12 Pro"
|
||||
:width 390
|
||||
:height 844}
|
||||
{:name "iPhone 12 Mini"
|
||||
:width 360
|
||||
:height 780}
|
||||
{:name "iPhone 12 Pro Max"
|
||||
:width 428
|
||||
:height 926}
|
||||
{:name "iPhone X/XS/11 Pro"
|
||||
:width 375
|
||||
:height 812}
|
||||
{:name "iPhone XS Max/XR/11"
|
||||
:width 414
|
||||
:height 896}
|
||||
{:name "iPhone 6/7/8 Plus"
|
||||
:width 414
|
||||
:height 736}
|
||||
{:name "iPhone 6/7/8/SE2"
|
||||
:width 375
|
||||
:height 667}
|
||||
{:name "iPhone 5/SE"
|
||||
:width 320
|
||||
:height 568}
|
||||
{:name "iPad"
|
||||
:width 768
|
||||
:height 1024}
|
||||
{:name "iPad Pro 10.5in"
|
||||
:width 834
|
||||
:height 1112}
|
||||
{:name "iPad Pro 12.9in"
|
||||
:width 1024
|
||||
:height 1366}
|
||||
{:name "Watch 44mm"
|
||||
:width 368
|
||||
:height 448}
|
||||
{:name "Watch 42mm"
|
||||
:width 312
|
||||
:height 390}
|
||||
{:name "Watch 40mm"
|
||||
:width 324
|
||||
:height 394}
|
||||
{:name "Watch 38mm"
|
||||
:width 272
|
||||
:height 340}
|
||||
|
||||
{:name "ANDROID"}
|
||||
{:name "Mobile"
|
||||
:width 360
|
||||
:height 640}
|
||||
{:name "Tablet"
|
||||
:width 768
|
||||
:height 1024}
|
||||
{:name "Google Pixel 4a/5"
|
||||
:width 393
|
||||
:height 851}
|
||||
{:name "Samsung Galaxy S20+"
|
||||
:width 384
|
||||
:height 854}
|
||||
{:name "Samsung Galaxy A71/A51"
|
||||
:width 412
|
||||
:height 914}
|
||||
|
||||
{:name "MICROSOFT"}
|
||||
{:name "Surface Pro 3"
|
||||
:width 1440
|
||||
:height 960}
|
||||
{:name "Surface Pro 4/5/6/7"
|
||||
:width 1368
|
||||
:height 912}
|
||||
|
||||
{:name "ReMarkable"}
|
||||
{:name "Remarkable 2"
|
||||
:width 840
|
||||
:height 1120}
|
||||
|
||||
{:name "WEB"}
|
||||
{:name "Web 1280"
|
||||
:width 1280
|
||||
:height 800}
|
||||
{:name "Web 1366"
|
||||
:width 1366
|
||||
:height 768}
|
||||
{:name "Web 1024"
|
||||
:width 1024
|
||||
:height 768}
|
||||
{:name "Web 1920"
|
||||
:width 1920
|
||||
:height 1080}
|
||||
|
||||
{:name "PRINT (96dpi)"}
|
||||
{:name "A0"
|
||||
:width 3179
|
||||
:height 4494}
|
||||
{:name "A1"
|
||||
:width 2245
|
||||
:height 3179}
|
||||
{:name "A2"
|
||||
:width 1587
|
||||
:height 2245}
|
||||
{:name "A3"
|
||||
:width 1123
|
||||
:height 1587}
|
||||
{:name "A4"
|
||||
:width 794
|
||||
:height 1123}
|
||||
{:name "A5"
|
||||
:width 559
|
||||
:height 794}
|
||||
{:name "A6"
|
||||
:width 397
|
||||
:height 559}
|
||||
{:name "Letter"
|
||||
:width 816
|
||||
:height 1054}
|
||||
{:name "DIN Lang"
|
||||
:width 835
|
||||
:height 413}
|
||||
|
||||
{:name "SOCIAL MEDIA"}
|
||||
{:name "Instagram profile"
|
||||
:width 320
|
||||
:height 320}
|
||||
{:name "Instagram post"
|
||||
:width 1080
|
||||
:height 1080}
|
||||
{:name "Instagram story"
|
||||
:width 1080
|
||||
:height 1920}
|
||||
{:name "Facebook profile"
|
||||
:width 720
|
||||
:height 720}
|
||||
{:name "Facebook cover"
|
||||
:width 820
|
||||
:height 312}
|
||||
{:name "Facebook post"
|
||||
:width 1200
|
||||
:height 630}
|
||||
{:name "LinkedIn profile"
|
||||
:width 400
|
||||
:height 400}
|
||||
{:name "LinkedIn cover"
|
||||
:width 1584
|
||||
:height 396}
|
||||
{:name "LinkedIn post"
|
||||
:width 1200
|
||||
:height 627}
|
||||
{:name "Twitter profile"
|
||||
:width 400
|
||||
:height 400}
|
||||
{:name "Twitter header"
|
||||
:width 1500
|
||||
:height 500}
|
||||
{:name "Twitter post"
|
||||
:width 1024
|
||||
:height 512}
|
||||
{:name "YouTube profile"
|
||||
:width 800
|
||||
:height 800}
|
||||
{:name "YouTube banner"
|
||||
:width 2560
|
||||
:height 1440}
|
||||
{:name "YouTube thumb"
|
||||
:width 1280
|
||||
:height 720}])
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
(ns app.main.data.workspace
|
||||
(:require
|
||||
[app.main.data.workspace.indices :as dwidx]
|
||||
|
||||
[app.common.attrs :as attrs]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
|
@ -127,7 +129,8 @@
|
|||
team-id (dm/get-in bundle [:project :team-id])]
|
||||
(rx/merge
|
||||
(rx/of (dwn/initialize team-id file-id)
|
||||
(dwp/initialize-file-persistence file-id))
|
||||
(dwp/initialize-file-persistence file-id)
|
||||
(dwidx/start-indexing file-id))
|
||||
|
||||
(->> stream
|
||||
(rx/filter #(= ::dwc/index-initialized %))
|
||||
|
@ -194,6 +197,7 @@
|
|||
(watch [_ _ _]
|
||||
(rx/merge
|
||||
(rx/of (dwn/finalize file-id))
|
||||
(rx/of (dwidx/stop-indexing file-id))
|
||||
(->> (rx/of ::dwp/finalize)
|
||||
(rx/observe-on :async))))))
|
||||
|
||||
|
@ -1214,16 +1218,14 @@
|
|||
;; selected and its parents
|
||||
objects (cph/selected-subtree objects selected)
|
||||
|
||||
z-index (cp/calculate-z-index objects)
|
||||
z-values (->> selected
|
||||
(map #(vector %
|
||||
(+ (get z-index %)
|
||||
(get z-index (get-in objects [% :frame-id]))))))
|
||||
selected
|
||||
(->> z-values
|
||||
(sort-by second)
|
||||
(map first)
|
||||
(into (d/ordered-set)))]
|
||||
;;z-index (cp/calculate-z-index objects)
|
||||
;;z-values (->> selected
|
||||
;; (map #(vector %
|
||||
;; (+ (get z-index %)
|
||||
;; (get z-index (get-in objects [% :frame-id]))))))
|
||||
|
||||
selected (-> (cph/sort-z-index objects selected)
|
||||
(into (d/ordered-set)))]
|
||||
|
||||
(assoc data :selected selected)))
|
||||
|
||||
|
|
|
@ -261,25 +261,21 @@
|
|||
(defn get-shape-layer-position
|
||||
[objects selected attrs]
|
||||
|
||||
(if (= :frame (:type attrs))
|
||||
;; Frames are always positioned on the root frame
|
||||
[uuid/zero uuid/zero nil]
|
||||
;; Calculate the frame over which we're drawing
|
||||
(let [position @ms/mouse-position
|
||||
frame-id (:frame-id attrs (cph/frame-id-by-position objects position))
|
||||
shape (when-not (empty? selected)
|
||||
(cph/get-base-shape objects selected))]
|
||||
|
||||
;; Calculate the frame over which we're drawing
|
||||
(let [position @ms/mouse-position
|
||||
frame-id (:frame-id attrs (cph/frame-id-by-position objects position))
|
||||
shape (when-not (empty? selected)
|
||||
(cph/get-base-shape objects selected))]
|
||||
;; When no shapes has been selected or we're over a different frame
|
||||
;; we add it as the latest shape of that frame
|
||||
(if (or (not shape) (not= (:frame-id shape) frame-id))
|
||||
[frame-id frame-id nil]
|
||||
|
||||
;; When no shapes has been selected or we're over a different frame
|
||||
;; we add it as the latest shape of that frame
|
||||
(if (or (not shape) (not= (:frame-id shape) frame-id))
|
||||
[frame-id frame-id nil]
|
||||
|
||||
;; Otherwise, we add it to next to the selected shape
|
||||
(let [index (cph/get-position-on-parent objects (:id shape))
|
||||
{:keys [frame-id parent-id]} shape]
|
||||
[frame-id parent-id (inc index)])))))
|
||||
;; Otherwise, we add it to next to the selected shape
|
||||
(let [index (cph/get-position-on-parent objects (:id shape))
|
||||
{:keys [frame-id parent-id]} shape]
|
||||
[frame-id parent-id (inc index)]))))
|
||||
|
||||
(defn make-new-shape
|
||||
[attrs objects selected]
|
||||
|
@ -325,7 +321,7 @@
|
|||
selected)
|
||||
|
||||
changes (-> (pcb/empty-changes it page-id)
|
||||
(pcb/add-object shape {:index (when (= :frame (:type shape)) 0)}))]
|
||||
(pcb/add-object shape #_{:index (when (= :frame (:type shape)) 0)}))]
|
||||
|
||||
(rx/concat
|
||||
(rx/of (dch/commit-changes changes)
|
||||
|
|
|
@ -65,12 +65,7 @@
|
|||
focus (:workspace-focus-selected state)
|
||||
zoom (get-in state [:workspace-local :zoom] 1)
|
||||
|
||||
frames (cph/get-frames objects)
|
||||
fid (or (->> frames
|
||||
(filter #(gsh/has-point? % initial))
|
||||
first
|
||||
:id)
|
||||
uuid/zero)
|
||||
fid (cph/frame-id-by-position objects initial)
|
||||
|
||||
shape (-> state
|
||||
(get-in [:workspace-drawing :object])
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
[objects selected]
|
||||
(->> selected
|
||||
(map #(get objects %))
|
||||
(filter #(not= :frame (:type %)))
|
||||
(map #(assoc % ::index (cph/get-position-on-parent objects (:id %))))
|
||||
(sort-by ::index)))
|
||||
|
||||
|
|
78
frontend/src/app/main/data/workspace/indices.cljs
Normal file
78
frontend/src/app/main/data/workspace/indices.cljs
Normal file
|
@ -0,0 +1,78 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.indices
|
||||
(:require
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.indices.object-tree :as dwi-object-tree]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.data.workspace.changes :as dwc]
|
||||
[beicon.core :as rx]
|
||||
[app.common.data :as d]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(def stop-indexing? (ptk/type? ::stop-indexing))
|
||||
|
||||
(def objects-changes #{:add-obj :mod-obj :del-obj :mov-objects})
|
||||
|
||||
(defn stop-indexing
|
||||
[file-id]
|
||||
(ptk/reify ::stop-indexing
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(dissoc :index-object-tree)))))
|
||||
|
||||
(defn process-changes
|
||||
"Simplify changes so we have only the type of operation and the ids"
|
||||
[changes]
|
||||
(->> changes
|
||||
(filter #(contains? objects-changes (:type %)))
|
||||
(mapcat (fn [{:keys [type id shapes]}]
|
||||
(if (some? shapes)
|
||||
(->> shapes (map #(vector type %)))
|
||||
[[type id]])))))
|
||||
|
||||
(defn update-indexing
|
||||
[change-type shape-id old-objects new-objects]
|
||||
(ptk/reify ::update-indexing
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (wsh/lookup-page-objects state)]
|
||||
(-> state
|
||||
(update :index-object-tree dwi-object-tree/update-index shape-id change-type old-objects new-objects))))))
|
||||
|
||||
(defn start-indexing
|
||||
[file-id]
|
||||
|
||||
(ptk/reify ::start-indexing
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (wsh/lookup-page-objects state)]
|
||||
(-> state
|
||||
(assoc :index-object-tree (dwi-object-tree/init-index objects)))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stopper (->> stream (rx/filter stop-indexing?) (rx/take 1))
|
||||
objects-delta (->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) (rx/buffer 2 1))]
|
||||
(->> stream
|
||||
(rx/filter dwc/commit-changes?)
|
||||
(rx/flat-map #(->> % deref :changes process-changes))
|
||||
(rx/with-latest-from objects-delta)
|
||||
(rx/map (fn [[[type id] [objects-old objects-new]]]
|
||||
(update-indexing type id objects-old objects-new)))
|
||||
#_(rx/tap (fn [[[type id] [objects-old objects-new]]]
|
||||
(let [obj-old (get objects-old id)
|
||||
obj-new (get objects-new id)]
|
||||
(prn ">change" (or (:name obj-old) (:name obj-new)))
|
||||
(prn " > " type)
|
||||
(.log js/console " >" (clj->js obj-old))
|
||||
(.log js/console " >" (clj->js obj-new))
|
||||
|
||||
)))
|
||||
(rx/take-until stopper)
|
||||
(rx/ignore))))))
|
|
@ -0,0 +1,28 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.indices.object-tree
|
||||
(:require
|
||||
[app.common.pages.helpers :as cph]
|
||||
))
|
||||
|
||||
(defn objects-tree
|
||||
[objects]
|
||||
|
||||
|
||||
|
||||
)
|
||||
|
||||
(defn init-index
|
||||
[objects]
|
||||
|
||||
|
||||
|
||||
)
|
||||
|
||||
(defn update-index
|
||||
[index shape-id change-type old-objects new-objects]
|
||||
)
|
|
@ -45,7 +45,7 @@
|
|||
[props]
|
||||
(let [shape (obj/get props "shape")]
|
||||
(when (:thumbnail shape)
|
||||
(let [{:keys [x y width height]} shape
|
||||
(let [{:keys [x y width height show-content]} shape
|
||||
transform (gsh/transform-matrix shape)
|
||||
props (-> (attrs/extract-style-attrs shape)
|
||||
(obj/merge!
|
||||
|
@ -59,8 +59,9 @@
|
|||
render-id (mf/use-ctx muc/render-ctx)]
|
||||
|
||||
[:*
|
||||
[:g {:clip-path (frame-clip-url shape render-id)}
|
||||
[:& frame-clip-def {:shape shape :render-id render-id}]
|
||||
[:g {:clip-path (when (not show-content) (frame-clip-url shape render-id))}
|
||||
(when (not show-content)
|
||||
[:& frame-clip-def {:shape shape :render-id render-id}])
|
||||
[:& shape-fills {:shape shape}
|
||||
(if path?
|
||||
[:> :path props]
|
||||
|
@ -88,7 +89,7 @@
|
|||
[props]
|
||||
(let [childs (unchecked-get props "childs")
|
||||
shape (unchecked-get props "shape")
|
||||
{:keys [x y width height]} shape
|
||||
{:keys [x y width height show-content]} shape
|
||||
|
||||
transform (gsh/transform-matrix shape)
|
||||
|
||||
|
@ -104,7 +105,8 @@
|
|||
render-id (mf/use-ctx muc/render-ctx)]
|
||||
|
||||
[:*
|
||||
[:g {:clip-path (frame-clip-url shape render-id)}
|
||||
[:g {:clip-path (when (not show-content)
|
||||
(frame-clip-url shape render-id))}
|
||||
[:& shape-fills {:shape shape}
|
||||
(if path?
|
||||
[:> :path props]
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
(ns app.main.ui.workspace.shapes.frame
|
||||
(:require
|
||||
[app.main.store :as st]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -45,8 +47,7 @@
|
|||
[new-props old-props]
|
||||
(and
|
||||
(= (unchecked-get new-props "thumbnail?") (unchecked-get old-props "thumbnail?"))
|
||||
(= (unchecked-get new-props "shape") (unchecked-get old-props "shape"))
|
||||
(= (unchecked-get new-props "objects") (unchecked-get old-props "objects"))))
|
||||
(= (unchecked-get new-props "shape") (unchecked-get old-props "shape"))))
|
||||
|
||||
(defn frame-wrapper-factory
|
||||
[shape-wrapper]
|
||||
|
@ -59,7 +60,7 @@
|
|||
|
||||
(let [shape (unchecked-get props "shape")
|
||||
thumbnail? (unchecked-get props "thumbnail?")
|
||||
objects (unchecked-get props "objects")
|
||||
objects (wsh/lookup-page-objects @st/state)
|
||||
|
||||
render-id (mf/use-memo #(str (uuid/next)))
|
||||
fonts (mf/use-memo (mf/deps shape objects) #(ff/shape->fonts shape objects))
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.workspace.sidebar.options.menus.measures
|
||||
(:require
|
||||
[app.main.constants :refer [size-presets]]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.spec.radius :as ctr]
|
||||
|
@ -30,12 +31,14 @@
|
|||
:rx :ry
|
||||
:r1 :r2 :r3 :r4
|
||||
:selrect
|
||||
:points])
|
||||
:points
|
||||
:show-content
|
||||
:hide-in-viewer])
|
||||
|
||||
(def ^:private type->options
|
||||
{:bool #{:size :position :rotation}
|
||||
:circle #{:size :position :rotation}
|
||||
:frame #{:presets :size :position :radius}
|
||||
:frame #{:presets :size :position :radius :clip-content :show-in-viewer}
|
||||
:group #{:size :position :rotation}
|
||||
:image #{:size :position :rotation :radius}
|
||||
:path #{:size :position :rotation}
|
||||
|
@ -43,8 +46,6 @@
|
|||
:svg-raw #{:size :position :rotation}
|
||||
:text #{:size :position :rotation}})
|
||||
|
||||
(declare +size-presets+)
|
||||
|
||||
;; -- User/drawing coords
|
||||
(mf/defc measures-menu
|
||||
[{:keys [ids ids-with-children values type all-types shape] :as props}]
|
||||
|
@ -103,6 +104,9 @@
|
|||
radius-multi? (mf/use-state nil)
|
||||
radius-input-ref (mf/use-ref nil)
|
||||
|
||||
clip-content-ref (mf/use-ref nil)
|
||||
show-in-viewer-ref (mf/use-ref nil)
|
||||
|
||||
on-preset-selected
|
||||
(fn [width height]
|
||||
(st/emit! (udw/update-dimensions ids :width width)
|
||||
|
@ -146,13 +150,13 @@
|
|||
|
||||
change-radius
|
||||
(mf/use-callback
|
||||
(mf/deps ids-with-children)
|
||||
(fn [update-fn]
|
||||
(dch/update-shapes ids-with-children
|
||||
(fn [shape]
|
||||
(if (ctr/has-radius? shape)
|
||||
(update-fn shape)
|
||||
shape)))))
|
||||
(mf/deps ids-with-children)
|
||||
(fn [update-fn]
|
||||
(dch/update-shapes ids-with-children
|
||||
(fn [shape]
|
||||
(if (ctr/has-radius? shape)
|
||||
(update-fn shape)
|
||||
shape)))))
|
||||
|
||||
on-switch-to-radius-1
|
||||
(mf/use-callback
|
||||
|
@ -200,17 +204,38 @@
|
|||
on-radius-r3-change #(on-radius-4-change % :r3)
|
||||
on-radius-r4-change #(on-radius-4-change % :r4)
|
||||
|
||||
on-change-clip-content
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/checked?)]
|
||||
(dch/update-shapes ids (fn [shape]) (assoc shape :show-content (not value))))))
|
||||
|
||||
on-change-clip-content
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/checked?)]
|
||||
(st/emit! (dch/update-shapes ids (fn [shape] (assoc shape :show-content (not value))))))))
|
||||
|
||||
on-change-show-in-viewer
|
||||
(mf/use-callback
|
||||
(mf/deps ids)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/checked?)]
|
||||
(st/emit! (dch/update-shapes ids (fn [shape] (assoc shape :hide-in-viewer (not value))))))))
|
||||
|
||||
select-all #(-> % (dom/get-target) (.select))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps radius-mode @radius-multi?)
|
||||
(fn []
|
||||
(when (and (= radius-mode :radius-1)
|
||||
(= @radius-multi? false))
|
||||
;; when going back from radius-multi to normal radius-1,
|
||||
;; restore focus to the newly created numeric-input
|
||||
(let [radius-input (mf/ref-val radius-input-ref)]
|
||||
(dom/focus! radius-input)))))
|
||||
(mf/use-layout-effect
|
||||
(mf/deps radius-mode @radius-multi?)
|
||||
(fn []
|
||||
(when (and (= radius-mode :radius-1)
|
||||
(= @radius-multi? false))
|
||||
;; when going back from radius-multi to normal radius-1,
|
||||
;; restore focus to the newly created numeric-input
|
||||
(let [radius-input (mf/ref-val radius-input-ref)]
|
||||
(dom/focus! radius-input)))))
|
||||
|
||||
[:*
|
||||
[:div.element-set
|
||||
|
@ -226,7 +251,7 @@
|
|||
[:& dropdown {:show @show-presets-dropdown?
|
||||
:on-close #(reset! show-presets-dropdown? false)}
|
||||
[:ul.custom-select-dropdown
|
||||
(for [size-preset +size-presets+]
|
||||
(for [size-preset size-presets]
|
||||
(if-not (:width size-preset)
|
||||
[:li.dropdown-label {:key (:name size-preset)}
|
||||
[:span (:name size-preset)]]
|
||||
|
@ -367,172 +392,30 @@
|
|||
:min 0
|
||||
:on-click select-all
|
||||
:on-change on-radius-r4-change
|
||||
:value (:r4 values)}]]])])]]]))
|
||||
:value (:r4 values)}]]])])
|
||||
|
||||
(def +size-presets+
|
||||
[{:name "APPLE"}
|
||||
{:name "iPhone 12/12 Pro"
|
||||
:width 390
|
||||
:height 844}
|
||||
{:name "iPhone 12 Mini"
|
||||
:width 360
|
||||
:height 780}
|
||||
{:name "iPhone 12 Pro Max"
|
||||
:width 428
|
||||
:height 926}
|
||||
{:name "iPhone X/XS/11 Pro"
|
||||
:width 375
|
||||
:height 812}
|
||||
{:name "iPhone XS Max/XR/11"
|
||||
:width 414
|
||||
:height 896}
|
||||
{:name "iPhone 6/7/8 Plus"
|
||||
:width 414
|
||||
:height 736}
|
||||
{:name "iPhone 6/7/8/SE2"
|
||||
:width 375
|
||||
:height 667}
|
||||
{:name "iPhone 5/SE"
|
||||
:width 320
|
||||
:height 568}
|
||||
{:name "iPad"
|
||||
:width 768
|
||||
:height 1024}
|
||||
{:name "iPad Pro 10.5in"
|
||||
:width 834
|
||||
:height 1112}
|
||||
{:name "iPad Pro 12.9in"
|
||||
:width 1024
|
||||
:height 1366}
|
||||
{:name "Watch 44mm"
|
||||
:width 368
|
||||
:height 448}
|
||||
{:name "Watch 42mm"
|
||||
:width 312
|
||||
:height 390}
|
||||
{:name "Watch 40mm"
|
||||
:width 324
|
||||
:height 394}
|
||||
{:name "Watch 38mm"
|
||||
:width 272
|
||||
:height 340}
|
||||
(when (options :clip-content)
|
||||
[:div.input-checkbox
|
||||
[:input {:type "checkbox"
|
||||
:id "clip-content"
|
||||
:ref clip-content-ref
|
||||
:checked (not (:show-content values))
|
||||
:on-change on-change-clip-content}]
|
||||
|
||||
{:name "ANDROID"}
|
||||
{:name "Mobile"
|
||||
:width 360
|
||||
:height 640}
|
||||
{:name "Tablet"
|
||||
:width 768
|
||||
:height 1024}
|
||||
{:name "Google Pixel 4a/5"
|
||||
:width 393
|
||||
:height 851}
|
||||
{:name "Samsung Galaxy S20+"
|
||||
:width 384
|
||||
:height 854}
|
||||
{:name "Samsung Galaxy A71/A51"
|
||||
:width 412
|
||||
:height 914}
|
||||
[:label {:for "clip-content"}
|
||||
(tr "workspace.options.clip-content")]])
|
||||
|
||||
{:name "MICROSOFT"}
|
||||
{:name "Surface Pro 3"
|
||||
:width 1440
|
||||
:height 960}
|
||||
{:name "Surface Pro 4/5/6/7"
|
||||
:width 1368
|
||||
:height 912}
|
||||
(when (options :show-in-viewer)
|
||||
[:div.input-checkbox
|
||||
[:input {:type "checkbox"
|
||||
:id "show-in-viewer"
|
||||
:ref show-in-viewer-ref
|
||||
:checked (not (:hide-in-viewer values))
|
||||
:on-change on-change-show-in-viewer}]
|
||||
|
||||
{:name "ReMarkable"}
|
||||
{:name "Remarkable 2"
|
||||
:width 840
|
||||
:height 1120}
|
||||
[:label {:for "show-in-viewer"}
|
||||
(tr "workspace.options.show-in-viewer")]])
|
||||
|
||||
{:name "WEB"}
|
||||
{:name "Web 1280"
|
||||
:width 1280
|
||||
:height 800}
|
||||
{:name "Web 1366"
|
||||
:width 1366
|
||||
:height 768}
|
||||
{:name "Web 1024"
|
||||
:width 1024
|
||||
:height 768}
|
||||
{:name "Web 1920"
|
||||
:width 1920
|
||||
:height 1080}
|
||||
]]]))
|
||||
|
||||
{:name "PRINT (96dpi)"}
|
||||
{:name "A0"
|
||||
:width 3179
|
||||
:height 4494}
|
||||
{:name "A1"
|
||||
:width 2245
|
||||
:height 3179}
|
||||
{:name "A2"
|
||||
:width 1587
|
||||
:height 2245}
|
||||
{:name "A3"
|
||||
:width 1123
|
||||
:height 1587}
|
||||
{:name "A4"
|
||||
:width 794
|
||||
:height 1123}
|
||||
{:name "A5"
|
||||
:width 559
|
||||
:height 794}
|
||||
{:name "A6"
|
||||
:width 397
|
||||
:height 559}
|
||||
{:name "Letter"
|
||||
:width 816
|
||||
:height 1054}
|
||||
{:name "DIN Lang"
|
||||
:width 835
|
||||
:height 413}
|
||||
|
||||
{:name "SOCIAL MEDIA"}
|
||||
{:name "Instagram profile"
|
||||
:width 320
|
||||
:height 320}
|
||||
{:name "Instagram post"
|
||||
:width 1080
|
||||
:height 1080}
|
||||
{:name "Instagram story"
|
||||
:width 1080
|
||||
:height 1920}
|
||||
{:name "Facebook profile"
|
||||
:width 720
|
||||
:height 720}
|
||||
{:name "Facebook cover"
|
||||
:width 820
|
||||
:height 312}
|
||||
{:name "Facebook post"
|
||||
:width 1200
|
||||
:height 630}
|
||||
{:name "LinkedIn profile"
|
||||
:width 400
|
||||
:height 400}
|
||||
{:name "LinkedIn cover"
|
||||
:width 1584
|
||||
:height 396}
|
||||
{:name "LinkedIn post"
|
||||
:width 1200
|
||||
:height 627}
|
||||
{:name "Twitter profile"
|
||||
:width 400
|
||||
:height 400}
|
||||
{:name "Twitter header"
|
||||
:width 1500
|
||||
:height 500}
|
||||
{:name "Twitter post"
|
||||
:width 1024
|
||||
:height 512}
|
||||
{:name "YouTube profile"
|
||||
:width 800
|
||||
:height 800}
|
||||
{:name "YouTube banner"
|
||||
:width 2560
|
||||
:height 1440}
|
||||
{:name "YouTube thumb"
|
||||
:width 1280
|
||||
:height 720}])
|
||||
|
|
|
@ -172,6 +172,7 @@
|
|||
over-shapes-stream
|
||||
(mf/deps page-id objects)
|
||||
(fn [ids]
|
||||
#_(prn "??hover-ids" (->> ids (map #(get-in objects [% :name]))))
|
||||
(let [is-group?
|
||||
(fn [id]
|
||||
(contains? #{:group :bool} (get-in objects [id :type])))
|
||||
|
@ -221,7 +222,7 @@
|
|||
[objects hover-ids selected active-frames zoom transform vbox]
|
||||
|
||||
(let [frame? #(= :frame (get-in objects [% :type]))
|
||||
all-frames (mf/use-memo (mf/deps objects) #(cph/get-frames-ids objects))
|
||||
all-frames (mf/use-memo (mf/deps objects) #(cph/get-root-frames-ids objects))
|
||||
selected-frames (mf/use-memo (mf/deps selected) #(->> all-frames (filter selected)))
|
||||
xf-selected-frame (comp (remove frame?) (map #(get-in objects [% :frame-id])))
|
||||
selected-shapes-frames (mf/use-memo (mf/deps selected) #(into #{} xf-selected-frame selected))
|
||||
|
|
|
@ -316,7 +316,7 @@
|
|||
:color color}
|
||||
props (map->obj (merge common-props props))]
|
||||
(case type
|
||||
:rotation (when (not= :frame (:type shape)) [:> rotation-handler props])
|
||||
:rotation [:> rotation-handler props]
|
||||
:resize-point [:> resize-point-handler props]
|
||||
:resize-side [:> resize-side-handler props])))])))
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.workspace.viewport.widgets
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
|
@ -182,15 +183,16 @@
|
|||
|
||||
[:g.frame-titles
|
||||
(for [frame frames]
|
||||
[:& frame-title {:key (dm/str "frame-title-" (:id frame))
|
||||
:frame frame
|
||||
:selected? (contains? selected (:id frame))
|
||||
:zoom zoom
|
||||
:show-artboard-names? show-artboard-names?
|
||||
:modifiers modifiers
|
||||
:on-frame-enter on-frame-enter
|
||||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select}])]))
|
||||
(when (= (:frame-id frame) uuid/zero)
|
||||
[:& frame-title {:key (dm/str "frame-title-" (:id frame))
|
||||
:frame frame
|
||||
:selected? (contains? selected (:id frame))
|
||||
:zoom zoom
|
||||
:show-artboard-names? show-artboard-names?
|
||||
:modifiers modifiers
|
||||
:on-frame-enter on-frame-enter
|
||||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select}]))]))
|
||||
|
||||
(mf/defc frame-flow
|
||||
[{:keys [flow frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}]
|
||||
|
|
|
@ -86,12 +86,17 @@
|
|||
|
||||
index (reduce index-shape initial-quadtree shapes)
|
||||
|
||||
z-index (cp/calculate-z-index objects)]
|
||||
;;z-index (cp/calculate-z-index objects)
|
||||
]
|
||||
|
||||
{:index index :z-index z-index :bounds bounds}))
|
||||
{:index index
|
||||
;;:z-index z-index
|
||||
:bounds bounds}))
|
||||
|
||||
(defn- update-index
|
||||
[{index :index z-index :z-index :as data} old-objects new-objects]
|
||||
[{index :index
|
||||
;; z-index :z-index
|
||||
:as data} old-objects new-objects]
|
||||
(let [changes? (fn [id]
|
||||
(not= (get old-objects id)
|
||||
(get new-objects id)))
|
||||
|
@ -112,12 +117,16 @@
|
|||
index-shape (make-index-shape new-objects parents-index clip-parents-index)
|
||||
index (reduce index-shape new-index shapes)
|
||||
|
||||
z-index (cp/update-z-index z-index changed-ids old-objects new-objects)]
|
||||
;;z-index (cp/update-z-index z-index changed-ids old-objects new-objects)
|
||||
]
|
||||
|
||||
(assoc data :index index :z-index z-index)))
|
||||
(assoc data :index index ;;:z-index z-index
|
||||
)))
|
||||
|
||||
(defn- query-index
|
||||
[{index :index z-index :z-index} rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?]
|
||||
[{index :index
|
||||
;;z-index :z-index
|
||||
} rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?]
|
||||
(let [result (-> (qdt/search index (clj->js rect))
|
||||
(es6-iterator-seq))
|
||||
|
||||
|
@ -145,10 +154,10 @@
|
|||
(fn [clip-parents]
|
||||
(->> clip-parents (some (comp not overlaps?)) not))
|
||||
|
||||
add-z-index
|
||||
(fn [{:keys [id frame-id] :as shape}]
|
||||
(assoc shape :z (+ (get z-index id)
|
||||
(get z-index frame-id 0))))
|
||||
;;add-z-index
|
||||
;;(fn [{:keys [id frame-id] :as shape}]
|
||||
;; (assoc shape :z (+ (get z-index id)
|
||||
;; (get z-index frame-id 0))))
|
||||
|
||||
;; Shapes after filters of overlapping and criteria
|
||||
matching-shapes
|
||||
|
@ -160,14 +169,15 @@
|
|||
(filter (if clip-children?
|
||||
(comp overlaps-parent? :clip-parents)
|
||||
(constantly true)))
|
||||
(map add-z-index))
|
||||
#_(map add-z-index))
|
||||
result)
|
||||
|
||||
keyfn (if reverse? (comp - :z) :z)]
|
||||
;;keyfn (if reverse? (comp - :z) :z)
|
||||
]
|
||||
|
||||
(into (d/ordered-set)
|
||||
(->> matching-shapes
|
||||
(sort-by keyfn)
|
||||
#_(sort-by keyfn)
|
||||
(map :id)))))
|
||||
|
||||
|
||||
|
@ -208,7 +218,7 @@
|
|||
(when-let [index (get @state page-id)]
|
||||
(query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?)))
|
||||
|
||||
(defmethod impl/handler :selection/query-z-index
|
||||
#_(defmethod impl/handler :selection/query-z-index
|
||||
[{:keys [page-id objects ids]}]
|
||||
(when-let [{z-index :z-index} (get @state page-id)]
|
||||
(->> ids (map #(+ (get z-index %)
|
||||
|
|
|
@ -3890,6 +3890,12 @@ msgstr "X"
|
|||
msgid "workspace.options.y"
|
||||
msgstr "Y"
|
||||
|
||||
msgid "workspace.options.clip-content"
|
||||
msgstr "Clip content"
|
||||
|
||||
msgid"workspace.options.show-in-viewer"
|
||||
msgstr "Show in view mode"
|
||||
|
||||
msgid "workspace.path.actions.add-node"
|
||||
msgstr "Add node (%s)"
|
||||
|
||||
|
|
|
@ -4058,6 +4058,12 @@ msgstr "X"
|
|||
msgid "workspace.options.y"
|
||||
msgstr "Y"
|
||||
|
||||
msgid "workspace.options.clip-content"
|
||||
msgstr "Truncar contenido"
|
||||
|
||||
msgid"workspace.options.show-in-viewer"
|
||||
msgstr "Mostrar en modo visualización"
|
||||
|
||||
msgid "workspace.path.actions.add-node"
|
||||
msgstr "Añadir nodo (%s)"
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue