mirror of
https://github.com/penpot/penpot.git
synced 2026-09-18 18:06:14 +00:00
🐛 Fix bend curve sharp edges (#11715)
This commit is contained in:
parent
8c7230749c
commit
78b5d13b7a
@ -238,6 +238,30 @@
|
|||||||
(/ (- half d0) (- d1 d0)))]
|
(/ (- half d0) (- d1 d0)))]
|
||||||
(/ (+ i frac) n)))))
|
(/ (+ i frac) n)))))
|
||||||
|
|
||||||
|
(def ^:private straight-handler-ratio
|
||||||
|
"Chord fraction that places the handlers of a straight segment."
|
||||||
|
(/ 1.0 3.0))
|
||||||
|
|
||||||
|
(defn bend-reference-curve
|
||||||
|
"Returns the cubic a bend of `curve` is measured against.
|
||||||
|
|
||||||
|
A straight cubic gets its handlers spread along the chord thirds: it draws
|
||||||
|
the same segment with tangents that open up progressively while it bends."
|
||||||
|
[[start end h1 h2 :as curve]]
|
||||||
|
(if (and (gpt/close? start h1)
|
||||||
|
(gpt/close? end h2))
|
||||||
|
(let [offset (gpt/scale (gpt/to-vec start end) straight-handler-ratio)]
|
||||||
|
[start end (gpt/add start offset) (gpt/subtract end offset)])
|
||||||
|
curve))
|
||||||
|
|
||||||
|
(defn curve-handler-deltas
|
||||||
|
"Returns the handler offsets that turn `curve` into `other`."
|
||||||
|
[[_ _ h1 h2] [_ _ o1 o2]]
|
||||||
|
(let [d1 (gpt/to-vec h1 o1)
|
||||||
|
d2 (gpt/to-vec h2 o2)]
|
||||||
|
{:c1x (:x d1) :c1y (:y d1)
|
||||||
|
:c2x (:x d2) :c2y (:y d2)}))
|
||||||
|
|
||||||
(defn bend-curve-deltas
|
(defn bend-curve-deltas
|
||||||
"Returns the smallest handler deltas that move the point at `t` to `target`."
|
"Returns the smallest handler deltas that move the point at `t` to `target`."
|
||||||
[curve t target]
|
[curve t target]
|
||||||
|
|||||||
@ -1296,6 +1296,95 @@
|
|||||||
(t/is (mth/close? (:x target) (:x p) 0.001))
|
(t/is (mth/close? (:x target) (:x p) 0.001))
|
||||||
(t/is (mth/close? (:y target) (:y p) 0.001))))))
|
(t/is (mth/close? (:y target) (:y p) 0.001))))))
|
||||||
|
|
||||||
|
(defn- bend-straight-segment
|
||||||
|
"Returns the cubic that the path editor draws when `curve` is grabbed at
|
||||||
|
`grab` and bent onto `target`."
|
||||||
|
[[start end h1 h2 :as curve] grab target]
|
||||||
|
(let [base (path.helpers/bend-reference-curve curve)
|
||||||
|
t (path.helpers/curve-closest-t base grab 0.001)
|
||||||
|
deltas (merge-with +
|
||||||
|
(path.helpers/curve-handler-deltas curve base)
|
||||||
|
(path.helpers/bend-curve-deltas base t target))]
|
||||||
|
[start end
|
||||||
|
(gpt/point (+ (:x h1) (:c1x deltas)) (+ (:y h1) (:c1y deltas)))
|
||||||
|
(gpt/point (+ (:x h2) (:c2x deltas)) (+ (:y h2) (:c2y deltas)))]))
|
||||||
|
|
||||||
|
(defn- start-tangent-angle
|
||||||
|
"Angle, in degrees, between the chord of a cubic and its tangent at the start."
|
||||||
|
[[start end h1 _]]
|
||||||
|
(gpt/angle-with-other (gpt/to-vec start h1) (gpt/to-vec start end)))
|
||||||
|
|
||||||
|
(def ^:private straight-segment
|
||||||
|
[(gpt/point 0.0 0.0) (gpt/point 100.0 0.0)
|
||||||
|
(gpt/point 0.0 0.0) (gpt/point 100.0 0.0)])
|
||||||
|
|
||||||
|
(t/deftest helpers-bend-reference-curve-spreads-straight-handlers
|
||||||
|
;; a straight cubic carries its handlers on the chord thirds
|
||||||
|
(let [[_ _ h1 h2] (path.helpers/bend-reference-curve straight-segment)]
|
||||||
|
(t/is (gpt/close? (gpt/point 33.333 0.0) h1))
|
||||||
|
(t/is (gpt/close? (gpt/point 66.667 0.0) h2))))
|
||||||
|
|
||||||
|
(t/deftest helpers-bend-reference-curve-keeps-the-segment
|
||||||
|
;; the spread handlers draw the same straight segment
|
||||||
|
(let [base (path.helpers/bend-reference-curve straight-segment)]
|
||||||
|
(doseq [t [0.0 0.25 0.5 0.75 1.0]]
|
||||||
|
(let [p (path.helpers/curve-values base t)]
|
||||||
|
(t/is (mth/close? 0.0 (:y p) 0.001))
|
||||||
|
(t/is (mth/close? (* 100.0 t) (:x p) 0.001))))))
|
||||||
|
|
||||||
|
(t/deftest helpers-bend-reference-curve-leaves-curves-alone
|
||||||
|
;; a segment that already holds its handlers keeps them
|
||||||
|
(let [curve [(gpt/point 0.0 0.0) (gpt/point 100.0 0.0)
|
||||||
|
(gpt/point 10.0 50.0) (gpt/point 90.0 50.0)]]
|
||||||
|
(t/is (= curve (path.helpers/bend-reference-curve curve)))))
|
||||||
|
|
||||||
|
(t/deftest helpers-bend-straight-segment-opens-the-tangent-gradually
|
||||||
|
;; bending a straight segment starts flat and opens up as it is dragged away
|
||||||
|
(let [grab (gpt/point 50.0 0.0)
|
||||||
|
angles (mapv #(start-tangent-angle
|
||||||
|
(bend-straight-segment straight-segment grab (gpt/point 50.0 %)))
|
||||||
|
[1.0 10.0 50.0])]
|
||||||
|
(t/is (< (nth angles 0) 5.0))
|
||||||
|
(t/is (< (nth angles 0) (nth angles 1) (nth angles 2)))
|
||||||
|
(t/is (< (nth angles 2) 90.0))))
|
||||||
|
|
||||||
|
(t/deftest helpers-bend-straight-segment-passes-through-target
|
||||||
|
;; the grabbed point lands exactly under the pointer
|
||||||
|
(doseq [grab [(gpt/point 25.0 0.0) (gpt/point 50.0 0.0) (gpt/point 70.0 0.0)]
|
||||||
|
offset [(gpt/point 0.0 12.0) (gpt/point 5.0 -30.0)]]
|
||||||
|
(let [target (gpt/add grab offset)
|
||||||
|
base (path.helpers/bend-reference-curve straight-segment)
|
||||||
|
t (path.helpers/curve-closest-t base grab 0.001)
|
||||||
|
p (path.helpers/curve-values
|
||||||
|
(bend-straight-segment straight-segment grab target) t)]
|
||||||
|
(t/is (mth/close? (:x target) (:x p) 0.001))
|
||||||
|
(t/is (mth/close? (:y target) (:y p) 0.001)))))
|
||||||
|
|
||||||
|
(t/deftest helpers-bend-straight-segment-turns-a-line-into-a-soft-curve
|
||||||
|
;; the deltas reach the stored line through the content modifiers
|
||||||
|
(let [content (path/content
|
||||||
|
[{:command :move-to :params {:x 0.0 :y 0.0}}
|
||||||
|
{:command :line-to :params {:x 100.0 :y 0.0}}])
|
||||||
|
entry (d/seek #(= 1 (:index %)) (path/segment-entries content))
|
||||||
|
curve (path.helpers/entry->bezier entry)
|
||||||
|
base (path.helpers/bend-reference-curve curve)
|
||||||
|
grab (gpt/point 50.0 0.0)
|
||||||
|
target (gpt/point 50.0 10.0)
|
||||||
|
t (path.helpers/curve-closest-t base grab 0.001)
|
||||||
|
deltas (merge-with +
|
||||||
|
(path.helpers/curve-handler-deltas curve base)
|
||||||
|
(path.helpers/bend-curve-deltas base t target))
|
||||||
|
segment (second (vec (path/apply-content-modifiers content {1 deltas})))
|
||||||
|
{:keys [c1x c1y c2x c2y]} (:params segment)
|
||||||
|
bent [(gpt/point 0.0 0.0) (gpt/point 100.0 0.0)
|
||||||
|
(gpt/point c1x c1y) (gpt/point c2x c2y)]
|
||||||
|
p (path.helpers/curve-values bent t)]
|
||||||
|
(t/is (= :curve-to (:command segment)))
|
||||||
|
;; a 10 unit pull on a 100 unit segment barely tilts the tangents
|
||||||
|
(t/is (< (start-tangent-angle bent) 25.0))
|
||||||
|
(t/is (mth/close? (:x target) (:x p) 0.01))
|
||||||
|
(t/is (mth/close? (:y target) (:y p) 0.01))))
|
||||||
|
|
||||||
(t/deftest segment-flip-content-horizontal
|
(t/deftest segment-flip-content-horizontal
|
||||||
;; mirror every node across the bbox center on the vertical axis
|
;; mirror every node across the bbox center on the vertical axis
|
||||||
(let [content (path/content
|
(let [content (path/content
|
||||||
|
|||||||
@ -564,13 +564,17 @@
|
|||||||
(merge-dragged-on-drop))))))))
|
(merge-dragged-on-drop))))))))
|
||||||
|
|
||||||
(defn bend-segment-modifier
|
(defn bend-segment-modifier
|
||||||
"Bends segment `index` so its point at `t` reaches `target`."
|
"Bends segment `index` so its point at `t` reaches `target`.
|
||||||
[index base-curve t target]
|
|
||||||
|
`base-deltas` offsets the stored handlers onto `base-curve`."
|
||||||
|
[index base-curve base-deltas t target]
|
||||||
(ptk/reify ::bend-segment-modifier
|
(ptk/reify ::bend-segment-modifier
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [id (st/get-path-id state)
|
(let [id (st/get-path-id state)
|
||||||
deltas (path.helpers/bend-curve-deltas base-curve t target)
|
deltas (merge-with +
|
||||||
|
base-deltas
|
||||||
|
(path.helpers/bend-curve-deltas base-curve t target))
|
||||||
modifiers (dm/get-in state [:workspace-local :edit-path id :content-modifiers] {})]
|
modifiers (dm/get-in state [:workspace-local :edit-path id :content-modifiers] {})]
|
||||||
(assoc-in state [:workspace-local :edit-path id :content-modifiers]
|
(assoc-in state [:workspace-local :edit-path id :content-modifiers]
|
||||||
(assoc modifiers index deltas))))))
|
(assoc modifiers index deltas))))))
|
||||||
@ -580,17 +584,20 @@
|
|||||||
(ptk/reify ::bend-selected-segment
|
(ptk/reify ::bend-selected-segment
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [stopper (mse/drag-stopper stream)
|
(let [stopper (mse/drag-stopper stream)
|
||||||
content (st/get-path state :content)
|
content (st/get-path state :content)
|
||||||
entry (segment-entry content index)
|
entry (segment-entry content index)
|
||||||
base-curve (path.helpers/entry->bezier entry)
|
curve (path.helpers/entry->bezier entry)
|
||||||
|
;; A straight segment bends from handlers spread along its chord.
|
||||||
|
base-curve (path.helpers/bend-reference-curve curve)
|
||||||
|
base-deltas (path.helpers/curve-handler-deltas curve base-curve)
|
||||||
;; Keep the grabbed curve parameter fixed during the drag.
|
;; Keep the grabbed curve parameter fixed during the drag.
|
||||||
t (path.helpers/curve-closest-t base-curve start-position 0.001)]
|
t (path.helpers/curve-closest-t base-curve start-position 0.001)]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(->> ms/mouse-position
|
(->> ms/mouse-position
|
||||||
(rx/filter gpt/point?)
|
(rx/filter gpt/point?)
|
||||||
(rx/map streams/to-pixel-snap)
|
(rx/map streams/to-pixel-snap)
|
||||||
(rx/map #(bend-segment-modifier index base-curve t %))
|
(rx/map #(bend-segment-modifier index base-curve base-deltas t %))
|
||||||
(rx/take-until stopper))
|
(rx/take-until stopper))
|
||||||
(rx/of (apply-content-modifiers)))))))
|
(rx/of (apply-content-modifiers)))))))
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user