0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-10 09:08:31 -05:00

🎉 Add resize constraints to shapes

This commit is contained in:
Andrés Moya 2021-06-01 12:38:00 +02:00 committed by Alonso Torres
parent 55b0f6e950
commit 092a973f9a
9 changed files with 602 additions and 43 deletions

View file

@ -188,6 +188,7 @@
(d/export gpr/center->rect)
(d/export gtr/transform-shape)
(d/export gtr/calc-child-modifiers)
(d/export gtr/transform-matrix)
(d/export gtr/inverse-transform-matrix)
(d/export gtr/transform-point-center)

View file

@ -9,6 +9,11 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]))
(defn left-of [rect] (:x rect))
(defn right-of [rect] (+ (:x rect) (:width rect)))
(defn top-of [rect] (:y rect))
(defn bottom-of [rect] (+ (:y rect) (:height rect)))
(defn rect->points [{:keys [x y width height]}]
;; (assert (number? x))
;; (assert (number? y))

View file

@ -14,6 +14,7 @@
[app.common.geom.shapes.path :as gpa]
[app.common.geom.shapes.rect :as gpr]
[app.common.math :as mth]
[app.common.pages.spec :as spec]
[app.common.text :as txt]))
;; --- Relative Movement
@ -362,6 +363,138 @@
(dissoc :modifiers)))
shape))))
(defn calc-child-modifiers
[parent transformed-parent child parent-modifiers]
(let [parent-rect (:selrect parent)
transformed-parent-rect (:selrect transformed-parent)
child-rect (:selrect child)
origin (:resize-origin parent-modifiers)
orig-h (when origin
(cond
(mth/close? (:x origin) (gpr/left-of parent-rect)) :left
(mth/close? (:x origin) (gpr/right-of parent-rect)) :right
:else :middle))
orig-v (when origin
(cond
(mth/close? (:y origin) (gpr/top-of parent-rect)) :top
(mth/close? (:y origin) (gpr/bottom-of parent-rect)) :bottom
:else :middle))
delta-h (when orig-h
(cond (= orig-h :left)
(- (gpr/right-of transformed-parent-rect) (gpr/right-of parent-rect))
(= orig-h :right)
(- (gpr/left-of transformed-parent-rect) (gpr/left-of parent-rect))
:else 0))
delta-v (when orig-v
(cond (= orig-v :top)
(- (gpr/bottom-of transformed-parent-rect) (gpr/bottom-of parent-rect))
(= orig-v :bottom)
(- (gpr/top-of transformed-parent-rect) (gpr/top-of parent-rect))
:else 0))
constraints-h (get child :constraints-h (spec/default-constraints-h child))
constraints-v (get child :constraints-v (spec/default-constraints-v child))
modifiers-h (case constraints-h
:left
(if (= orig-h :right)
{:displacement (gpt/point delta-h 0)} ;; we convert to matrix below
{})
:right
(if (= orig-h :left)
{:displacement (gpt/point delta-h 0)}
{})
:leftright
(cond (= orig-h :left)
{:resize-origin (gpt/point (gpr/left-of child-rect) (gpr/top-of child-rect))
:resize-vector (gpt/point (/ (+ (:width child-rect) delta-h)
(:width child-rect))
1)}
(= orig-h :right)
{:resize-origin (gpt/point (gpr/right-of child-rect) (gpr/top-of child-rect))
:resize-vector (gpt/point (/ (- (:width child-rect) delta-h)
(:width child-rect))
1)}
:else {})
:center
{:displacement (gpt/point (/ delta-h 2) 0)}
:scale
(if (:resize-origin parent-modifiers)
{:resize-origin (:resize-origin parent-modifiers)
:resize-vector (gpt/point (:x (:resize-vector parent-modifiers)) 1)}
{})
{})
modifiers-v (case constraints-v
:top
(if (= orig-v :bottom)
{:displacement (gpt/point 0 delta-v)}
{})
:bottom
(if (= orig-v :top)
{:displacement (gpt/point 0 delta-v)}
{})
:topbottom
(cond (= orig-v :top)
{:resize-origin (gpt/point (gpr/left-of child-rect) (gpr/top-of child-rect))
:resize-vector (gpt/point 1
(/ (+ (:height child-rect) delta-v)
(:height child-rect)))}
(= orig-v :bottom)
{:resize-origin (gpt/point (gpr/left-of child-rect) (gpr/bottom-of child-rect))
:resize-vector (gpt/point 1
(/ (- (:height child-rect) delta-v)
(:height child-rect)))}
:else {})
:center
{:displacement (gpt/point 0 (/ delta-v 2))}
:scale
(if (:resize-origin parent-modifiers)
{:resize-origin (:resize-origin parent-modifiers)
:resize-vector (gpt/point 1 (:y (:resize-vector parent-modifiers)))}
{})
{})]
(cond-> {}
(or (:displacement modifiers-h) (:displacement modifiers-v))
(assoc :displacement (gmt/translate-matrix
(gpt/point (get (:displacement modifiers-h) :x 0)
(get (:displacement modifiers-v) :y 0))))
(or (:resize-vector modifiers-h) (:resize-vector modifiers-v))
(assoc :resize-origin (or (:resize-origin modifiers-h) ;; we assume that the origin is the same
(:resize-origin modifiers-v)) ;; in any direction
:resize-vector (gpt/point (get (:resize-vector modifiers-h) :x 1)
(get (:resize-vector modifiers-v) :y 1)))
(:displacement parent-modifiers)
(update :displacement #(if (nil? %)
(:displacement parent-modifiers)
(gmt/multiply % (:displacement parent-modifiers)))))))
(defn update-group-viewbox
"Updates the viewbox for groups imported from SVG's"
[{:keys [selrect svg-viewbox] :as group} new-selrect]

View file

@ -9,6 +9,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[clojure.spec.alpha :as s]))
;; --- Specs
@ -195,6 +196,30 @@
(s/def :internal.shape/interactions
(s/coll-of :internal.shape/interaction :kind vector?))
;; Size constraints
(s/def :internal.shape/constraints-h #{:left :right :leftright :center :scale})
(s/def :internal.shape/constraints-v #{:top :bottom :topbottom :center :scale})
(s/def :internal.shape/fixed-scroll boolean?)
; Shapes in the top frame have no constraints. Shapes directly below some
; frame are left-top constrained. Else (shapes in a group) are scaled.
(defn default-constraints-h
[shape]
(if (= (:parent-id shape) uuid/zero)
nil
(if (= (:parent-id shape) (:frame-id shape))
:left
:scale)))
(defn default-constraints-v
[shape]
(if (= (:parent-id shape) uuid/zero)
nil
(if (= (:parent-id shape) (:frame-id shape))
:top
:scale)))
;; Page Data related
(s/def :internal.shape/blocked boolean?)
(s/def :internal.shape/collapsed boolean?)
@ -297,6 +322,9 @@
:internal.shape/locked
:internal.shape/proportion
:internal.shape/proportion-lock
:internal.shape/constraints-h
:internal.shape/constraints-v
:internal.shape/fixed-scroll
:internal.shape/rx
:internal.shape/ry
:internal.shape/r1

View file

@ -1257,4 +1257,154 @@
}
}
.row-flex.align-top {
align-items: flex-start;
}
.constraints-widget {
min-width: 72px;
min-height: 72px;
position: relative;
background-color: $color-gray-60;
flex-grow: 0;
.constraints-box {
width: 28px;
height: 28px;
position: absolute;
top: 22px;
left: 22px;
border: 2px solid $color-gray-50;
}
.constraint-button {
position: absolute;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
&::after {
content: ' ';
background-color: $color-gray-20;
}
&.active,
&:hover {
&::after {
background-color: $color-primary;
}
}
&.top,
&.bottom {
width: 28px;
height: 22px;
left: calc(50% - 14px);
&::after {
width: 3px;
height: 15px;
}
}
&.top {
top: 0;
}
&.bottom {
bottom: 0;
}
&.left,
&.right {
width: 22px;
height: 28px;
top: calc(50% - 14px);
&::after {
width: 15px;
height: 3px;
}
}
&.left {
left: 0;
}
&.right {
right: 0;
}
&.centerv {
width: 28px;
height: 28px;
left: calc(50% - 14px);
top: calc(50% - 14px);
&::after {
width: 3px;
height: 15px;
}
}
&.centerh {
width: 28px;
height: 15px;
left: calc(50% - 14px);
top: calc(50% - 7px);
&::after {
width: 15px;
height: 3px;
}
}
}
}
.constraints-form {
display: flex;
flex-grow: 1;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
.input-select {
font-size: $fs11;
margin: 0 $x-small;
}
svg {
width: 15px;
height: 15px;
margin-left: $medium;
fill: $color-gray-20;
}
.left-right svg {
transform: rotate(45deg);
}
.top-down svg {
transform: rotate(-45deg);
}
.fix-when {
font-size: $fs11;
cursor: pointer;
span {
margin-left: $small;
}
&:hover,
&.active {
color: $color-primary;
svg {
fill: $color-primary;
}
}
}
}

View file

@ -151,8 +151,7 @@
:resize-origin origin
:resize-transform shape-transform
:resize-scale-text scale-text
:resize-transform-inverse shape-transform-inverse}
false))))
:resize-transform-inverse shape-transform-inverse}))))
;; Unifies the instantaneous proportion lock modifier
;; activated by Shift key and the shapes own proportion
@ -426,10 +425,34 @@
;; -- Apply modifiers
(defn- set-modifiers-recursive
[modif-tree objects shape modifiers]
(let [children (->> (get shape :shapes [])
(map #(get objects %)))
transformed-shape (when (seq children) ; <- don't calculate it if not needed
(gsh/transform-shape
(assoc shape :modifiers (select-keys modifiers
[:resize-origin
:resize-vector]))))
set-child (fn [modif-tree child]
(let [child-modifiers (gsh/calc-child-modifiers shape
transformed-shape
child
modifiers)]
(set-modifiers-recursive modif-tree
objects
child
child-modifiers)))]
(reduce set-child
(update-in modif-tree [(:id shape) :modifiers] #(merge % modifiers))
children)))
(defn set-modifiers
([ids] (set-modifiers ids nil true))
([ids modifiers] (set-modifiers ids modifiers true))
([ids modifiers recurse-frames?]
([ids] (set-modifiers ids nil))
([ids modifiers]
(us/verify (s/coll-of uuid?) ids)
(ptk/reify ::set-modifiers
ptk/UpdateEvent
@ -438,25 +461,16 @@
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
ids (->> ids (into #{} (remove #(get-in objects [% :blocked] false))))
not-frame-id?
(fn [shape-id]
(let [shape (get objects shape-id)]
(or recurse-frames? (not (= :frame (:type shape))))))
;; For each shape updates the modifiers given as arguments
update-shape
(fn [objects shape-id]
(update-in objects [shape-id :modifiers] #(merge % modifiers)))
;; ID's + Children but remove frame children if the flag is set to false
ids-with-children (concat ids (mapcat #(cp/get-children % objects)
(filter not-frame-id? ids)))]
(update state :workspace-modifiers
#(reduce update-shape % ids-with-children)))))))
ids (->> ids (into #{} (remove #(get-in objects [% :blocked] false))))]
(reduce (fn [state id]
(update state :workspace-modifiers
#(set-modifiers-recursive %
objects
(get objects id)
modifiers)))
state
ids))))))
;; Set-rotation is custom because applies different modifiers to each
;; shape adjusting their position.
@ -567,10 +581,7 @@
(fn [objects shape-id]
(let [shape (get objects shape-id)
modifier (gsh/resize-modifiers shape attr value)]
(-> objects
(assoc-in [shape-id :modifiers] modifier)
(cond-> (not (= :frame (:type shape)))
(update-children (cp/get-children shape-id objects) modifier)))))]
(set-modifiers-recursive objects objects shape modifier)))]
(d/update-in-when
state
@ -597,8 +608,7 @@
(rx/of (set-modifiers selected
{:resize-vector (gpt/point -1.0 1.0)
:resize-origin origin
:displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))}
false)
:displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))})
(apply-modifiers selected))))))
(defn flip-vertical-selected []
@ -614,8 +624,7 @@
(rx/of (set-modifiers selected
{:resize-vector (gpt/point 1.0 -1.0)
:resize-origin origin
:displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))}
false)
:displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))})
(apply-modifiers selected))))))
(defn start-local-displacement [point]

View file

@ -6,6 +6,7 @@
(ns app.main.ui.workspace.sidebar.options.menus.measures
(:require
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.main.ui.icons :as i]
[app.main.store :as st]
@ -15,12 +16,14 @@
[app.util.data :refer [classnames]]
[app.common.geom.shapes :as gsh]
[app.common.geom.point :as gpt]
[app.common.pages.spec :as spec]
[app.common.uuid :as uuid]
[app.main.data.workspace :as udw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.changes :as dch]
[app.main.ui.components.numeric-input :refer [numeric-input]]
[app.common.math :as math]
[app.util.i18n :refer [t] :as i18n]))
[app.util.i18n :refer [tr] :as i18n]))
(def measure-attrs [:proportion-lock
:width :height
@ -28,7 +31,12 @@
:rotation
:rx :ry
:r1 :r2 :r3 :r4
:selrect])
:selrect
:constraints-h
:constraints-v
:fixed-scroll
:parent-id
:frame-id])
(defn- attr->string [attr values]
(let [value (attr values)]
@ -42,7 +50,6 @@
(mf/defc measures-menu
[{:keys [options ids ids-with-children values] :as props}]
(let [options (or options #{:size :position :rotation :radius})
locale (i18n/use-locale)
ids-with-children (or ids-with-children ids)
@ -65,6 +72,13 @@
proportion-lock (:proportion-lock values)
in-frame? (not= (:parent-id values) uuid/zero)
first-level? (and in-frame?
(= (:parent-id values) (:frame-id values)))
constraints-h (get values :constraints-h (spec/default-constraints-h values))
constraints-v (get values :constraints-v (spec/default-constraints-v values))
on-size-change
(mf/use-callback
(mf/deps ids)
@ -161,15 +175,75 @@
on-radius-r2-change #(on-radius-4-change % :r2)
on-radius-r3-change #(on-radius-4-change % :r3)
on-radius-r4-change #(on-radius-4-change % :r4)
select-all #(-> % (dom/get-target) (.select))]
select-all #(-> % (dom/get-target) (.select))
on-constraint-button-clicked
(mf/use-callback
(mf/deps [ids values])
(fn [button]
(fn [event]
(let [constraints-h (get values :constraints-h :scale)
constraints-v (get values :constraints-v :scale)
[constraint new-value]
(case button
:top (case constraints-v
:top [:constraints-v :scale]
:topbottom [:constraints-v :bottom]
:bottom [:constraints-v :topbottom]
[:constraints-v :top])
:bottom (case constraints-v
:bottom [:constraints-v :scale]
:topbottom [:constraints-v :top]
:top [:constraints-v :topbottom]
[:constraints-v :bottom])
:left (case constraints-h
:left [:constraints-h :scale]
:leftright [:constraints-h :right]
:right [:constraints-h :leftright]
[:constraints-h :left])
:right (case constraints-h
:right [:constraints-h :scale]
:leftright [:constraints-h :left]
:left [:constraints-h :leftright]
[:constraints-h :right])
:centerv (case constraints-v
:center [:constraints-v :scale]
[:constraints-v :center])
:centerh (case constraints-h
:center [:constraints-h :scale]
[:constraints-h :center]))]
(st/emit! (dch/update-shapes
ids
#(assoc % constraint new-value)))))))
on-constraint-select-changed
(mf/use-callback
(mf/deps [ids values])
(fn [constraint]
(fn [event]
(let [value (-> (dom/get-target-val event) (keyword))]
(when-not (str/empty? value)
(st/emit! (dch/update-shapes
ids
#(assoc % constraint value))))))))
on-fixed-scroll-clicked
(mf/use-callback
(mf/deps [ids values])
(fn [event]
(st/emit! (dch/update-shapes
ids
#(update % :fixed-scroll not)))))]
[:*
[:div.element-set
[:div.element-set-content
;; WIDTH & HEIGHT
(when (options :size)
[:div.row-flex
[:span.element-set-subtitle (t locale "workspace.options.size")]
[:span.element-set-subtitle (tr "workspace.options.size")]
[:div.input-element.width
[:> numeric-input {:min 1
:no-validate true
@ -197,7 +271,7 @@
;; POSITION
(when (options :position)
[:div.row-flex
[:span.element-set-subtitle (t locale "workspace.options.position")]
[:span.element-set-subtitle (tr "workspace.options.position")]
[:div.input-element.Xaxis
[:> numeric-input {:no-validate true
:placeholder "--"
@ -214,7 +288,7 @@
;; ROTATION
(when (options :rotation)
[:div.row-flex
[:span.element-set-subtitle (t locale "workspace.options.rotation")]
[:span.element-set-subtitle (tr "workspace.options.rotation")]
[:div.input-element.degrees
[:> numeric-input
{:no-validate true
@ -244,14 +318,14 @@
{:class (classnames
:selected
(and radius-1? (not radius-4?)))
:alt (t locale "workspace.options.radius.all-corners")
:alt (tr "workspace.options.radius.all-corners")
:on-click on-switch-to-radius-1}
i/radius-1]
[:div.radius-icon.tooltip.tooltip-bottom
{:class (classnames
:selected
(and radius-4? (not radius-1?)))
:alt (t locale "workspace.options.radius.single-corners")
:alt (tr "workspace.options.radius.single-corners")
:on-click on-switch-to-radius-4}
i/radius-4]]
(if radius-1?
@ -291,5 +365,68 @@
:min 0
:on-click select-all
:on-change on-radius-r4-change
:value (attr->string :r4 values)}]]])
]))]]))
:value (attr->string :r4 values)}]]])]))]]
;; CONSTRAINTS
(when in-frame?
[:div.element-set
[:div.element-set-title
[:span (tr "workspace.options.constraints")]]
[:div.element-set-content
[:div.row-flex.align-top
[:div.constraints-widget
[:div.constraints-box]
[:div.constraint-button.top
{:class (classnames :active (or (= constraints-v :top)
(= constraints-v :topbottom)))
:on-click (on-constraint-button-clicked :top)}]
[:div.constraint-button.bottom
{:class (classnames :active (or (= constraints-v :bottom)
(= constraints-v :topbottom)))
:on-click (on-constraint-button-clicked :bottom)}]
[:div.constraint-button.left
{:class (classnames :active (or (= constraints-h :left)
(= constraints-h :leftright)))
:on-click (on-constraint-button-clicked :left)}]
[:div.constraint-button.right
{:class (classnames :active (or (= constraints-h :right)
(= constraints-h :leftright)))
:on-click (on-constraint-button-clicked :right)}]
[:div.constraint-button.centerv
{:class (classnames :active (= constraints-v :center))
:on-click (on-constraint-button-clicked :centerv)}]
[:div.constraint-button.centerh
{:class (classnames :active (= constraints-h :center))
:on-click (on-constraint-button-clicked :centerh)}]]
[:div.constraints-form
[:div.row-flex
[:span.left-right i/full-screen]
[:select.input-select {:on-change (on-constraint-select-changed :constraints-h)
:value (d/name constraints-h "scale")}
(when (= constraints-h :multiple)
[:option {:value ""} (tr "settings.multiple")])
[:option {:value "left"} (tr "workspace.options.constraints.left")]
[:option {:value "right"} (tr "workspace.options.constraints.right")]
[:option {:value "leftright"} (tr "workspace.options.constraints.leftright")]
[:option {:value "center"} (tr "workspace.options.constraints.center")]
[:option {:value "scale"} (tr "workspace.options.constraints.scale")]]]
[:div.row-flex
[:span.top-bottom i/full-screen]
[:select.input-select {:on-change (on-constraint-select-changed :constraints-v)
:value (d/name constraints-v "scale")}
(when (= constraints-v :multiple)
[:option {:value ""} (tr "settings.multiple")])
[:option {:value "top"} (tr "workspace.options.constraints.top")]
[:option {:value "bottom"} (tr "workspace.options.constraints.bottom")]
[:option {:value "topbottom"} (tr "workspace.options.constraints.topbottom")]
[:option {:value "center"} (tr "workspace.options.constraints.center")]
[:option {:value "scale"} (tr "workspace.options.constraints.scale")]]]
(when first-level?
[:div.row-flex
[:div.fix-when {:class (classnames :active (:fixed-scroll values))
:on-click on-fixed-scroll-clicked}
i/pin
[:span (tr "workspace.options.constraints.fix-when-scrolling")]]])]]]])]))

View file

@ -1884,6 +1884,54 @@ msgstr "Canvas background"
msgid "workspace.options.component"
msgstr "Component"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints"
msgstr "Constraints"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.left"
msgstr "Left"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.right"
msgstr "Right"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.leftright"
msgstr "Left & Right"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.center"
msgstr "Center"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.scale"
msgstr "Scale"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.top"
msgstr "Top"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.bottom"
msgstr "Bottom"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.topbottom"
msgstr "Top & Bottom"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.center"
msgstr "Center"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.scale"
msgstr "Scale"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.fix-when-scrolling"
msgstr "Fix when scrolling"
#: src/app/main/ui/workspace/sidebar/options.cljs
msgid "workspace.options.design"
msgstr "Design"

View file

@ -1876,6 +1876,54 @@ msgstr "Color de fondo"
msgid "workspace.options.component"
msgstr "Componente"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints"
msgstr "Restricciones"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.left"
msgstr "Izquierda"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.right"
msgstr "Derecha"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.leftright"
msgstr "Izq. y Der."
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.center"
msgstr "Centro"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.scale"
msgstr "Escalar"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.top"
msgstr "Arriba"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.bottom"
msgstr "Abajo"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.topbottom"
msgstr "Arriba y Abajo"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.center"
msgstr "Centro"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.scale"
msgstr "Escalar"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.constraints.fix-when-scrolling"
msgstr "Fijo al desplazar"
#: src/app/main/ui/workspace/sidebar/options.cljs
msgid "workspace.options.design"
msgstr "Diseño"