0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-25 07:58:49 -05:00

🎉 Add support for nested layouts

This commit is contained in:
alonso.torres 2022-07-05 10:58:00 +02:00 committed by Andrey Antukh
parent 1c8aef6fa8
commit 7176bb6f1a
7 changed files with 308 additions and 205 deletions

View file

@ -15,6 +15,7 @@
[app.common.geom.shapes.corners :as gsc]
[app.common.geom.shapes.intersect :as gin]
[app.common.geom.shapes.layout :as gcl]
[app.common.geom.shapes.modifiers :as gsm]
[app.common.geom.shapes.path :as gsp]
[app.common.geom.shapes.rect :as gpr]
[app.common.geom.shapes.transforms :as gtr]
@ -201,3 +202,6 @@
;; Corners
(dm/export gsc/shape-corners-1)
(dm/export gsc/shape-corners-4)
;; Modifiers
(dm/export gsm/set-objects-modifiers)

View file

@ -197,6 +197,9 @@
;; Build final child modifiers. Apply transform again to the result, to get the
;; real modifiers that need to be applied to the child, including rotation as needed.
(cond-> {}
(some? (:displacement-after modifiers))
(assoc :displacement-after (:displacement-after modifiers))
(or (contains? modifiers-h :displacement)
(contains? modifiers-v :displacement))
(assoc :displacement (cond-> (gpt/point (get-in modifiers-h [:displacement :x] 0)

View file

@ -68,11 +68,11 @@
(defn calc-layout-data
"Digest the layout data to pass it to the constrains"
[{:keys [layout-gap] :as shape} children modif-tree transformed-rect]
[{:keys [layout-type layout-gap] :as shape} children modif-tree transformed-rect]
(let [transformed-rect (-> transformed-rect (add-padding shape))
(let [{:keys [x y width height]} (-> transformed-rect (add-padding shape))
num-children (count children)
children-gap (* layout-gap (dec num-children) )
[children-width children-height]
(->> children
(map #(-> (merge % (get modif-tree (:id %))) gtr/transform-shape))
@ -80,13 +80,39 @@
[(+ acc-width (-> shape :points gre/points->rect :width))
(+ acc-height (-> shape :points gre/points->rect :height))]) [0 0]))
{:keys [x y width height]} transformed-rect
layout-gap
(cond
(= :packed layout-type)
layout-gap
(= :space-around layout-type)
0
(and (col? shape) (= :space-between layout-type))
(/ (- width children-width) (dec num-children))
(and (row? shape) (= :space-between layout-type))
(/ (- height children-height) (dec num-children)))
margin-x (if (and (col? shape) (= :space-around layout-type))
(/ (- width children-width) (dec num-children) 2)
0)
margin-y (if (and (row? shape) (= :space-around layout-type))
(/ (- height children-height) (dec num-children) 2)
0)
children-gap (* layout-gap (dec num-children))
start-x
(cond
(or (and (col? shape) (= :space-between layout-type))
(and (col? shape) (= :space-around layout-type)))
x
(and (row? shape) (h-center? shape))
(+ x (/ width 2))
(and (row? shape) (h-end? shape))
(+ x width)
@ -97,10 +123,14 @@
(- (+ x width) (+ children-width children-gap))
:else
(:x transformed-rect))
x)
start-y
(cond
(or (and (row? shape) (= :space-between layout-type))
(and (row? shape) (= :space-around layout-type)))
y
(and (col? shape) (v-center? shape))
(+ y (/ height 2))
@ -114,15 +144,18 @@
(- (+ y height) (+ children-height children-gap))
:else
(:y transformed-rect) )]
y)]
{:start-x start-x
:start-y start-y
:layout-gap layout-gap
:margin-x margin-x
:margin-y margin-y
:reverse? (or (= :left (:layout-dir shape)) (= :bottom (:layout-dir shape)))}))
(defn next-p
"Calculates the position for the current shape given the layout-data context"
[{:keys [layout-gap] :as shape} {:keys [width height]} {:keys [start-x start-y] :as layout-data}]
[shape {:keys [width height]} {:keys [start-x start-y layout-gap margin-x margin-y] :as layout-data}]
(let [pos-x
(cond
@ -146,8 +179,11 @@
:else
start-y)
pos-x (cond-> pos-x (some? margin-x) (+ margin-x))
pos-y (cond-> pos-y (some? margin-y) (+ margin-y))
corner-p (gpt/point pos-x pos-y)
next-x
(if (col? shape)
(+ start-x width layout-gap)
@ -158,6 +194,9 @@
(+ start-y height layout-gap)
start-y)
next-x (cond-> next-x (some? margin-x) (+ margin-x))
next-y (cond-> next-y (some? margin-y) (+ margin-y))
layout-data
(assoc layout-data :start-x next-x :start-y next-y)]
[corner-p layout-data])
@ -165,15 +204,15 @@
(defn calc-layout-modifiers
"Calculates the modifiers for the layout"
[parent child current-modifier _modifiers _transformed-rect layout-data]
[parent child modifiers layout-data]
(let [current-modifier (-> current-modifier (dissoc :displacement-after))
child (-> child (assoc :modifiers current-modifier) gtr/transform-shape)
bounds (-> child :points gre/points->selrect)
(let [modifiers (-> modifiers (dissoc :displacement-after))
child (-> child (assoc :modifiers modifiers) gtr/transform-shape)
bounds (-> child :points gre/points->selrect)
[corner-p layout-data] (next-p parent bounds layout-data)
delta-p (-> corner-p (gpt/subtract (gpt/point bounds)))
modifiers (-> current-modifier (assoc :displacement-after (gmt/translate-matrix delta-p)))]
modifiers (-> modifiers (assoc :displacement-after (gmt/translate-matrix delta-p)))]
[modifiers layout-data]))

View file

@ -0,0 +1,213 @@
;; 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.common.geom.shapes.modifiers
(:require
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.constraints :as gct]
[app.common.geom.shapes.layout :as gcl]
[app.common.geom.shapes.rect :as gpr]
[app.common.geom.shapes.transforms :as gtr]
[app.common.math :as mth]
[app.common.uuid :as uuid]))
(defn set-pixel-precision
"Adjust modifiers so they adjust to the pixel grid"
[modifiers shape]
(if (some? (:resize-transform modifiers))
;; If we're working with a rotation we don't handle pixel precision because
;; the transformation won't have the precision anyway
modifiers
(let [center (gco/center-shape shape)
base-bounds (-> (:points shape) (gpr/points->rect))
raw-bounds
(-> (gtr/transform-bounds (:points shape) center modifiers)
(gpr/points->rect))
flip-x? (neg? (get-in modifiers [:resize-vector :x]))
flip-y? (or (neg? (get-in modifiers [:resize-vector :y]))
(neg? (get-in modifiers [:resize-vector-2 :y])))
path? (= :path (:type shape))
vertical-line? (and path? (<= (:width raw-bounds) 0.01))
horizontal-line? (and path? (<= (:height raw-bounds) 0.01))
target-width (if vertical-line?
(:width raw-bounds)
(max 1 (mth/round (:width raw-bounds))))
target-height (if horizontal-line?
(:height raw-bounds)
(max 1 (mth/round (:height raw-bounds))))
target-p (cond-> (gpt/round (gpt/point raw-bounds))
flip-x?
(update :x + target-width)
flip-y?
(update :y + target-height))
ratio-width (/ target-width (:width raw-bounds))
ratio-height (/ target-height (:height raw-bounds))
modifiers
(-> modifiers
(d/without-nils)
(d/update-in-when
[:resize-vector :x] #(* % ratio-width))
;; If the resize-vector-2 modifier arrives means the resize-vector
;; will only resize on the x axis
(cond-> (nil? (:resize-vector-2 modifiers))
(d/update-in-when
[:resize-vector :y] #(* % ratio-height)))
(d/update-in-when
[:resize-vector-2 :y] #(* % ratio-height)))
origin (get modifiers :resize-origin)
origin-2 (get modifiers :resize-origin-2)
resize-v (get modifiers :resize-vector)
resize-v-2 (get modifiers :resize-vector-2)
displacement (get modifiers :displacement)
target-p-inv
(-> target-p
(gpt/transform
(cond-> (gmt/matrix)
(some? displacement)
(gmt/multiply (gmt/inverse displacement))
(and (some? resize-v) (some? origin))
(gmt/scale (gpt/inverse resize-v) origin)
(and (some? resize-v-2) (some? origin-2))
(gmt/scale (gpt/inverse resize-v-2) origin-2))))
delta-v (gpt/subtract target-p-inv (gpt/point base-bounds))
modifiers
(-> modifiers
(d/update-when :displacement #(gmt/multiply (gmt/translate-matrix delta-v) %))
(cond-> (nil? (:displacement modifiers))
(assoc :displacement (gmt/translate-matrix delta-v))))]
modifiers)))
(defn set-children-modifiers
[modif-tree shape objects ignore-constraints snap-pixel?]
(letfn [(set-child [transformed-rect snap-pixel? modif-tree child]
(let [modifiers (get-in modif-tree [(:id shape) :modifiers])
child-modifiers (gct/calc-child-modifiers shape child modifiers ignore-constraints transformed-rect)
child-modifiers (cond-> child-modifiers snap-pixel? (set-pixel-precision child))
]
(cond-> modif-tree
(not (gtr/empty-modifiers? child-modifiers))
(assoc (:id child) {:modifiers child-modifiers}))))]
(let [children (map (d/getf objects) (:shapes shape))
modifiers (get-in modif-tree [(:id shape) :modifiers])
transformed-rect (gtr/transform-selrect (:selrect shape) modifiers)
resize-modif? (or (:resize-vector modifiers) (:resize-vector-2 modifiers))]
(reduce (partial set-child transformed-rect (and snap-pixel? resize-modif?)) modif-tree children))))
(defn set-layout-modifiers
[modif-tree objects id]
(letfn [(set-layout-modifiers [parent [layout-data modif-tree] child]
(let [modifiers (get-in modif-tree [(:id child) :modifiers])
[modifiers layout-data]
(gcl/calc-layout-modifiers parent child modifiers layout-data)
modif-tree
(cond-> modif-tree
(not (gtr/empty-modifiers? modifiers))
(assoc-in [(:id child) :modifiers] modifiers))]
[layout-data modif-tree]))]
(let [shape (get objects id)
children (map (d/getf objects) (:shapes shape))
modifiers (get-in modif-tree [id :modifiers])
;;_ (.log js/console "layout" (:name shape) (clj->js modifiers))
transformed-rect (gtr/transform-selrect (:selrect shape) modifiers)
layout-data (gcl/calc-layout-data shape children modif-tree transformed-rect)
children (cond-> children (:reverse? layout-data) reverse)
[_ modif-tree]
(reduce (partial set-layout-modifiers shape) [layout-data modif-tree] children)]
;;(.log js/console "modif-tree" modif-tree)
modif-tree)))
(defn get-first-layout
[id objects]
(loop [current id
result id]
(let [shape (get objects current)
parent (get objects (:parent-id shape))]
(cond
(or (not shape) (= uuid/zero current))
result
;; Frame found, but not layout we return the last layout found (or the id)
(and (= :frame (:type parent))
(not (:layout parent)))
result
;; Layout found. We continue upward but we mark this layout
(and (= :frame (:type parent))
(:layout parent))
(recur (:id parent) (:id parent))
;; If group or boolean or other type of group we continue with the last result
:else
(recur (:id parent) result)
))))
(defn resolve-layout-ids
"Given a list of ids, resolve the parent layouts that will need to update. This will go upwards
in the tree while a layout is found"
[ids objects]
(into (d/ordered-set)
(map #(get-first-layout % objects))
ids))
(defn set-objects-modifiers
[ids objects get-modifier ignore-constraints snap-pixel?]
(let [modif-tree (reduce (fn [modif-tree id]
(assoc modif-tree id {:modifiers (get-modifier (get objects id))})) {} ids)
ids (resolve-layout-ids ids objects)]
(loop [current (first ids)
pending (rest ids)
modif-tree modif-tree]
(if (some? current)
(let [shape (get objects current)
pending (concat pending (:shapes shape))
modif-tree
(-> modif-tree
(set-children-modifiers shape objects ignore-constraints snap-pixel?)
(cond-> (:layout shape)
(set-layout-modifiers objects current)))]
(recur (first pending) (rest pending) modif-tree #_touched-layouts))
modif-tree))))

View file

@ -614,7 +614,7 @@
(dissoc :modifiers))))))
(defn transform-bounds
[points center {:keys [displacement resize-transform-inverse resize-vector resize-origin resize-vector-2 resize-origin-2]}]
[points center {:keys [displacement displacement-after resize-transform-inverse resize-vector resize-origin resize-vector-2 resize-origin-2]}]
;; FIXME: Improve Performance
(let [resize-transform-inverse (or resize-transform-inverse (gmt/matrix))
@ -628,9 +628,10 @@
resize-origin-2
(when (some? resize-origin-2)
(transform-point-center resize-origin-2 center resize-transform-inverse))]
(transform-point-center resize-origin-2 center resize-transform-inverse))
]
(if (and (nil? displacement) (nil? resize-origin) (nil? resize-origin-2))
(if (and (nil? displacement) (nil? resize-origin) (nil? resize-origin-2) (nil? displacement-after))
points
(cond-> points
@ -641,7 +642,10 @@
(gco/transform-points resize-origin (gmt/scale-matrix resize-vector))
(some? resize-origin-2)
(gco/transform-points resize-origin-2 (gmt/scale-matrix resize-vector-2))))))
(gco/transform-points resize-origin-2 (gmt/scale-matrix resize-vector-2))
(some? displacement-after)
(gco/transform-points displacement-after)))))
(defn transform-selrect
[selrect modifiers]

View file

@ -218,7 +218,7 @@
(->> (get-root-shapes objects)
(mapv :id)))
(defn- get-base
(defn get-base
[objects id-a id-b]
(let [parents-a (reverse (get-parents-seq objects id-a))

View file

@ -110,7 +110,7 @@
;; geometric attributes of the shapes.
(declare clear-local-transform)
(declare set-objects-modifiers)
(declare get-ignore-tree)
(defn set-modifiers
@ -128,20 +128,17 @@
(ptk/reify ::set-modifiers
ptk/UpdateEvent
(update [_ state]
(let [modifiers (or modifiers (get-in state [:workspace-local :modifiers] {}))
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
(let [objects (wsh/lookup-page-objects state)
ids (into #{} (remove #(get-in objects [% :blocked] false)) ids)
layout (get state :workspace-layout)
snap-pixel? (and (not ignore-snap-pixel) (contains? layout :snap-pixel-grid))
setup-modifiers
(fn [state id]
(let [shape (get objects id)]
(update state :workspace-modifiers
#(set-objects-modifiers % objects shape modifiers ignore-constraints snap-pixel?))))]
snap-pixel? (and (not ignore-snap-pixel)
(contains? (:workspace-layout state) :snap-pixel-grid))
(reduce setup-modifiers state ids))))))
modif-tree
(gsh/set-objects-modifiers ids objects (constantly modifiers) ignore-constraints snap-pixel?)]
(assoc state :workspace-modifiers modif-tree))))))
;; Rotation use different algorithm to calculate children modifiers (and do not use child constraints).
(defn- set-rotation-modifiers
@ -280,162 +277,9 @@
[root transformed-root ignore-geometry?]))
(defn set-pixel-precision
"Adjust modifiers so they adjust to the pixel grid"
[modifiers shape]
(if (some? (:resize-transform modifiers))
;; If we're working with a rotation we don't handle pixel precision because
;; the transformation won't have the precision anyway
modifiers
(let [center (gsh/center-shape shape)
base-bounds (-> (:points shape) (gsh/points->rect))
raw-bounds
(-> (gsh/transform-bounds (:points shape) center modifiers)
(gsh/points->rect))
flip-x? (neg? (get-in modifiers [:resize-vector :x]))
flip-y? (or (neg? (get-in modifiers [:resize-vector :y]))
(neg? (get-in modifiers [:resize-vector-2 :y])))
path? (= :path (:type shape))
vertical-line? (and path? (<= (:width raw-bounds) 0.01))
horizontal-line? (and path? (<= (:height raw-bounds) 0.01))
target-width (if vertical-line?
(:width raw-bounds)
(max 1 (mth/round (:width raw-bounds))))
target-height (if horizontal-line?
(:height raw-bounds)
(max 1 (mth/round (:height raw-bounds))))
target-p (cond-> (gpt/round (gpt/point raw-bounds))
flip-x?
(update :x + target-width)
flip-y?
(update :y + target-height))
ratio-width (/ target-width (:width raw-bounds))
ratio-height (/ target-height (:height raw-bounds))
modifiers
(-> modifiers
(d/without-nils)
(d/update-in-when
[:resize-vector :x] #(* % ratio-width))
;; If the resize-vector-2 modifier arrives means the resize-vector
;; will only resize on the x axis
(cond-> (nil? (:resize-vector-2 modifiers))
(d/update-in-when
[:resize-vector :y] #(* % ratio-height)))
(d/update-in-when
[:resize-vector-2 :y] #(* % ratio-height)))
origin (get modifiers :resize-origin)
origin-2 (get modifiers :resize-origin-2)
resize-v (get modifiers :resize-vector)
resize-v-2 (get modifiers :resize-vector-2)
displacement (get modifiers :displacement)
target-p-inv
(-> target-p
(gpt/transform
(cond-> (gmt/matrix)
(some? displacement)
(gmt/multiply (gmt/inverse displacement))
(and (some? resize-v) (some? origin))
(gmt/scale (gpt/inverse resize-v) origin)
(and (some? resize-v-2) (some? origin-2))
(gmt/scale (gpt/inverse resize-v-2) origin-2))))
delta-v (gpt/subtract target-p-inv (gpt/point base-bounds))
modifiers
(-> modifiers
(d/update-when :displacement #(gmt/multiply (gmt/translate-matrix delta-v) %))
(cond-> (nil? (:displacement modifiers))
(assoc :displacement (gmt/translate-matrix delta-v))))]
modifiers)))
(defn- set-objects-modifiers
[modif-tree objects shape modifiers ignore-constraints snap-pixel?]
(letfn [(set-modifiers-rec
[modif-tree shape modifiers]
(let [children (map (d/getf objects) (:shapes shape))
transformed-rect (gsh/transform-selrect (:selrect shape) modifiers)
set-layout-child
(fn [snap-pixel? {:keys [modif-tree] :as layout-data} child]
(let [current-modifier (get-in modif-tree [(:id child) :modifiers])
;; child (-> (merge child old-modif) gsh/transform-shape)
[child-modifiers next-layout-data] (gsh/calc-layout-modifiers shape child current-modifier modifiers transformed-rect layout-data)
child-modifiers (cond-> child-modifiers snap-pixel? (set-pixel-precision child))
;;child-modifiers (if (some? old-modif)
;; (d/deep-merge (:modifiers old-modif) child-modifiers)
;; child-modifiers)
modif-tree
(cond-> modif-tree
(not (gsh/empty-modifiers? child-modifiers))
(set-modifiers-rec child child-modifiers))]
(assoc next-layout-data :modif-tree modif-tree)))
set-child
(fn [snap-pixel? modif-tree child]
(let [child-modifiers (gsh/calc-child-modifiers shape child modifiers ignore-constraints transformed-rect)
child-modifiers (cond-> child-modifiers snap-pixel? (set-pixel-precision child))]
(cond-> modif-tree
(not (gsh/empty-modifiers? child-modifiers))
(set-modifiers-rec child child-modifiers))))
modif-tree
(-> modif-tree
(assoc-in [(:id shape) :modifiers] modifiers))
resize-modif?
(or (:resize-vector modifiers) (:resize-vector-2 modifiers))
modif-tree
(reduce (partial set-child (and snap-pixel? resize-modif?)) modif-tree children)]
(cond
(:layout shape)
(let [layout-data (gsh/calc-layout-data shape children modif-tree transformed-rect)
children (cond-> children (:reverse? layout-data) reverse)]
(->> children
(reduce (partial set-layout-child (and snap-pixel? resize-modif?))
(merge {:modif-tree modif-tree} layout-data))
:modif-tree))
:else
modif-tree)))]
(let [modifiers (cond-> modifiers snap-pixel? (set-pixel-precision shape))
modif-tree (set-modifiers-rec modif-tree shape modifiers)
parent (get objects (:parent-id shape))
modif-tree
(cond-> modif-tree
(:layout parent)
(set-modifiers-rec parent nil))]
modif-tree)))
(defn- get-ignore-tree
"Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"
@ -590,18 +434,16 @@
(ptk/reify ::update-dimensions
ptk/UpdateEvent
(update [_ state]
(let [objects (wsh/lookup-page-objects state)
layout (get state :workspace-layout)
snap-pixel? (contains? layout :snap-pixel-grid)
(let [objects (wsh/lookup-page-objects state)
snap-pixel? (and (contains? (:workspace-layout state) :snap-pixel-grid)
(int? value))
get-modifier
(fn [shape] (gsh/resize-modifiers shape attr value))
update-modifiers
(fn [state id]
(let [shape (get objects id)
modifiers (gsh/resize-modifiers shape attr value)]
(-> state
(update :workspace-modifiers
#(set-objects-modifiers % objects shape modifiers false (and snap-pixel? (int? value)))))))]
(reduce update-modifiers state ids)))
modif-tree
(gsh/set-objects-modifiers ids objects get-modifier false snap-pixel?)]
(assoc state :workspace-modifiers modif-tree)))
ptk/WatchEvent
(watch [_ _ _]
@ -617,17 +459,15 @@
ptk/UpdateEvent
(update [_ state]
(let [objects (wsh/lookup-page-objects state)
layout (get state :workspace-layout)
snap-pixel? (contains? layout :snap-pixel-grid)
snap-pixel? (contains? (get state :workspace-layout) :snap-pixel-grid)
update-modifiers
(fn [state id]
(let [shape (get objects id)
modifiers (gsh/change-orientation-modifiers shape orientation)]
(-> state
(update :workspace-modifiers
#(set-objects-modifiers % objects shape modifiers false snap-pixel?)))))]
(reduce update-modifiers state ids)))
get-modifier
(fn [shape] (gsh/change-orientation-modifiers shape orientation))
modif-tree
(gsh/set-objects-modifiers ids objects get-modifier false snap-pixel?)]
(assoc state :workspace-modifiers modif-tree)))
ptk/WatchEvent
(watch [_ _ _]