mirror of
https://github.com/penpot/penpot.git
synced 2026-07-02 04:15:26 +00:00
✨ Add dedicated Line and Arrow drawing tools (#9146)
* ✨ Add dedicated Line and Arrow drawing tools Introduce a Line/Arrow toolbar option and a click-drag drawing interaction that matches Figma's workflow: select the tool, press and drag to define the line in one gesture, with Shift snapping to 15° increments. Arrowhead style can be toggled on either endpoint via the existing stroke-cap controls. Signed-off-by: jack-stormentswe <crazycoder131@gmail.com> * 💄 Fix formatting error Signed-off-by: jack-stormentswe <crazycoder131@gmail.com> * 🐛 Translate line and arrow tooltips in top toolbar Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> * 🐛 Add missing namespace Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> * 📚 Update copyright notice Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> * Add translations (EN) for toolbar elements Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> * Add translations (ES) for toolbar elements Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> * ♻️ Improve stroke-cap-end update for arrow handling Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> * 🐛 Fix shortcuts select tool but do not replace it in the toolbar Refactor tool selection logic in top_toolbar.cljs Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> * ♻️ Remove unnecessary blank line Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> --------- Signed-off-by: jack-stormentswe <crazycoder131@gmail.com> Signed-off-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com> Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net> Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
This commit is contained in:
parent
a6c7bd28e8
commit
aedb7f9195
@ -14,6 +14,7 @@
|
||||
[app.main.data.workspace.drawing.box :as box]
|
||||
[app.main.data.workspace.drawing.common :as common]
|
||||
[app.main.data.workspace.drawing.curve :as curve]
|
||||
[app.main.data.workspace.drawing.line :as line]
|
||||
[app.main.data.workspace.layout :as dwlo]
|
||||
[app.main.data.workspace.path :as path]
|
||||
[beicon.v2.core :as rx]
|
||||
@ -101,8 +102,10 @@
|
||||
(watch [_ _ _]
|
||||
(rx/of
|
||||
(case type
|
||||
:path (path/handle-drawing)
|
||||
:path (path/handle-drawing)
|
||||
:curve (curve/handle-drawing)
|
||||
:line (line/handle-drawing :line)
|
||||
:arrow (line/handle-drawing :arrow)
|
||||
(box/handle-drawing type))))))
|
||||
|
||||
(defn change-orientation
|
||||
|
||||
153
frontend/src/app/main/data/workspace/drawing/line.cljs
Normal file
153
frontend/src/app/main/data/workspace/drawing/line.cljs
Normal file
@ -0,0 +1,153 @@
|
||||
;; 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) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.main.data.workspace.drawing.line
|
||||
"Drawing handler for the Line (L) and Arrow (Shift+L) tools.
|
||||
|
||||
Unlike the Path (P) tool, which builds a multi-node path via
|
||||
successive clicks, Line/Arrow uses a single press-and-drag
|
||||
gesture (matching Figma/Sketch/XD). The result is always a
|
||||
two-node path from the drag start to the drag end. The Arrow
|
||||
variant additionally pre-configures an arrow stroke cap at
|
||||
the end so designers do not have to set it manually."
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.flex-layout :as gslf]
|
||||
[app.common.geom.shapes.grid-layout :as gslg]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.drawing.common :as common]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.mouse :as mse]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def ^:private snap-step-deg 15)
|
||||
|
||||
(defn- snap-to-angle
|
||||
"Snaps `point` relative to `origin` to the nearest multiple of
|
||||
`snap-step-deg` degrees on a circle of the same radius. Used when
|
||||
Shift is held while drawing a line."
|
||||
[origin point]
|
||||
(let [dx (- (:x point) (:x origin))
|
||||
dy (- (:y point) (:y origin))
|
||||
r (mth/sqrt (+ (* dx dx) (* dy dy)))
|
||||
angle (mth/atan2 dy dx)
|
||||
step (/ (* mth/PI snap-step-deg) 180)
|
||||
snapped (* step (mth/round (/ angle step)))]
|
||||
(gpt/point (+ (:x origin) (* r (mth/cos snapped)))
|
||||
(+ (:y origin) (* r (mth/sin snapped))))))
|
||||
|
||||
(defn- update-endpoint
|
||||
"Rebuilds the two-node line shape with `start` as the first node
|
||||
and `end-point` as the second. Called on every drag frame."
|
||||
[start end-point]
|
||||
(fn [state]
|
||||
(update-in state [:workspace-drawing :object]
|
||||
(fn [object]
|
||||
(let [points [start end-point]
|
||||
content (path/points->content points)
|
||||
selrect (path/calc-selrect content)
|
||||
points' (grc/rect->points selrect)]
|
||||
(-> object
|
||||
(assoc :content content)
|
||||
(assoc :selrect selrect)
|
||||
(assoc :points points')))))))
|
||||
|
||||
(defn- setup-frame
|
||||
[]
|
||||
(ptk/reify ::setup-frame
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
content (dm/get-in state [:workspace-drawing :object :content])
|
||||
position (path/get-handler-point content 0 nil)
|
||||
frame-id (->> (ctst/top-nested-frame objects position)
|
||||
(ctn/get-first-valid-parent objects)
|
||||
:id)
|
||||
flex-layout? (ctl/flex-layout? objects frame-id)
|
||||
grid-layout? (ctl/grid-layout? objects frame-id)
|
||||
drop-index (when flex-layout? (gslf/get-drop-index frame-id objects position))
|
||||
drop-cell (when grid-layout? (gslg/get-drop-cell frame-id objects position))]
|
||||
(update-in state [:workspace-drawing :object]
|
||||
(fn [object]
|
||||
(-> object
|
||||
(assoc :frame-id frame-id)
|
||||
(assoc :parent-id frame-id)
|
||||
(cond-> (some? drop-index)
|
||||
(with-meta {:index drop-index}))
|
||||
(cond-> (some? drop-cell)
|
||||
(with-meta {:cell drop-cell})))))))))
|
||||
|
||||
(defn- finalize
|
||||
"After mouse-up, mark the shape initialized only if it spans more
|
||||
than a single point. A zero-length drag is a noop (e.g. the user
|
||||
clicked without dragging)."
|
||||
[]
|
||||
(ptk/reify ::finalize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-drawing :object]
|
||||
(fn [{:keys [content selrect] :as shape}]
|
||||
(cond-> shape
|
||||
(or (empty? content)
|
||||
(nil? selrect)
|
||||
(<= (count content) 1))
|
||||
(assoc :initialized? false)))))))
|
||||
|
||||
(defn- arrow-strokes
|
||||
"Default stroke for the Arrow tool: a visible 1px solid line with
|
||||
a triangular arrow cap on the end node. The Line tool uses the
|
||||
same defaults minus the cap."
|
||||
[arrow?]
|
||||
(cond-> [{:stroke-color "#000000"
|
||||
:stroke-opacity 1
|
||||
:stroke-style :solid
|
||||
:stroke-alignment :center
|
||||
:stroke-width 1}]
|
||||
arrow? (update 0 assoc :stroke-cap-end :triangle-arrow)))
|
||||
|
||||
(defn handle-drawing
|
||||
"Runs the click-and-drag interaction for :line or :arrow. Produces
|
||||
a two-node path shape with stroke (and an arrow cap when arrow?)."
|
||||
[tool]
|
||||
(let [arrow? (= tool :arrow)]
|
||||
(ptk/reify ::handle-drawing
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [stopper (mse/drag-stopper stream)
|
||||
start @ms/mouse-position
|
||||
shape (cts/setup-shape {:type :path
|
||||
:initialized? true
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:strokes (arrow-strokes arrow?)
|
||||
:fills []
|
||||
:content (path/points->content [start start])})]
|
||||
(rx/concat
|
||||
(rx/of #(update % :workspace-drawing assoc :object shape))
|
||||
|
||||
(->> ms/mouse-position
|
||||
(rx/with-latest-from ms/mouse-position-shift)
|
||||
(rx/map (fn [[point shift?]]
|
||||
(if shift?
|
||||
(snap-to-angle start point)
|
||||
point)))
|
||||
(rx/map (partial update-endpoint start))
|
||||
(rx/take-until stopper))
|
||||
|
||||
(rx/of
|
||||
(setup-frame)
|
||||
(finalize)
|
||||
(common/handle-finish-drawing))))))))
|
||||
@ -313,6 +313,16 @@
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dwd/select-for-drawing :path))}
|
||||
|
||||
:draw-line {:tooltip "L"
|
||||
:command "l"
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dwd/select-for-drawing :line))}
|
||||
|
||||
:draw-arrow {:tooltip (ds/shift "L")
|
||||
:command "shift+l"
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dwd/select-for-drawing :arrow))}
|
||||
|
||||
:draw-curve {:tooltip (ds/shift "C")
|
||||
:command "shift+c"
|
||||
:subsections [:tools]
|
||||
|
||||
@ -47,7 +47,9 @@
|
||||
(def grouped-tools
|
||||
{:shapes {:default-tool :rect
|
||||
:tools {:rect {:icon i/rectangle}
|
||||
:circle {:icon i/ellipse}}}
|
||||
:circle {:icon i/ellipse}
|
||||
:line {:icon i/easing-linear}
|
||||
:arrow {:icon i/stroke-arrow}}}
|
||||
:free-draw {:default-tool :path
|
||||
:tools {:path {:icon i/path}
|
||||
:curve {:icon i/curve}}}})
|
||||
@ -59,6 +61,8 @@
|
||||
:frame (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame))
|
||||
:rect (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect))
|
||||
:circle (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse))
|
||||
:line (tr "workspace.toolbar.line" (sc/get-tooltip :draw-line))
|
||||
:arrow (tr "workspace.toolbar.arrow" (sc/get-tooltip :draw-arrow))
|
||||
:text (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text))
|
||||
:path (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path))
|
||||
:image (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image))
|
||||
@ -109,11 +113,7 @@
|
||||
on-select-tool
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [tool (-> (dom/get-current-target event)
|
||||
(dom/get-data "tool")
|
||||
(keyword))]
|
||||
(reset! default-tool* tool)
|
||||
(on-select-tool event))))
|
||||
(on-select-tool event)))
|
||||
|
||||
on-display-menu
|
||||
(mf/use-fn
|
||||
@ -144,6 +144,9 @@
|
||||
(cancel-timer! open-timer*)
|
||||
(cancel-timer! close-timer*)))
|
||||
|
||||
(mf/with-effect [drawtool group]
|
||||
(reset! default-tool* (active-group-tool group drawtool)))
|
||||
|
||||
[:li {:class (stl/css :toolbar-group)
|
||||
:on-pointer-enter on-display-menu
|
||||
:on-pointer-leave on-hide-menu}
|
||||
|
||||
@ -9725,6 +9725,9 @@ msgstr "The value is not valid"
|
||||
msgid "workspace.tokens.warning-name-change"
|
||||
msgstr "Renaming this token will break any reference to its old name"
|
||||
|
||||
msgid "workspace.toolbar.arrow"
|
||||
msgstr "Arrow (%s)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
||||
msgid "workspace.toolbar.assets"
|
||||
msgstr "Assets"
|
||||
@ -9762,6 +9765,9 @@ msgstr "Create board. Click and drag to define its size. (%s)"
|
||||
msgid "workspace.toolbar.image"
|
||||
msgstr "Image (%s)"
|
||||
|
||||
msgid "workspace.toolbar.line"
|
||||
msgstr "Line (%s)"
|
||||
|
||||
#: src/app/main/ui/workspace/top_toolbar.cljs:65, src/app/main/ui/workspace/top_toolbar.cljs:66, src/app/main/ui/workspace/top_toolbar.cljs:77
|
||||
msgid "workspace.toolbar.mcp"
|
||||
msgstr "MCP"
|
||||
|
||||
@ -9399,6 +9399,9 @@ msgstr ""
|
||||
"Cambiar el nombre de este token romperá cualquier referencia a su nombre "
|
||||
"anterior."
|
||||
|
||||
msgid "workspace.toolbar.arrow"
|
||||
msgstr "Flecha (%s)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar.cljs:159, src/app/main/ui/workspace/sidebar.cljs:166
|
||||
msgid "workspace.toolbar.assets"
|
||||
msgstr "Recursos"
|
||||
@ -9436,6 +9439,9 @@ msgstr "Crear tablero. Click y arrastrar para definir el tamaño. (%s)"
|
||||
msgid "workspace.toolbar.image"
|
||||
msgstr "Imagen (%s)"
|
||||
|
||||
msgid "workspace.toolbar.line"
|
||||
msgstr "Línea (%s)"
|
||||
|
||||
#: src/app/main/ui/workspace/top_toolbar.cljs:65, src/app/main/ui/workspace/top_toolbar.cljs:66, src/app/main/ui/workspace/top_toolbar.cljs:77
|
||||
msgid "workspace.toolbar.mcp"
|
||||
msgstr "MCP"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user