🎉 Add full integration with path data type feature

This commit is contained in:
Andrey Antukh 2025-03-28 00:07:45 +01:00
parent f545d7b3ea
commit 1fc0203c38
64 changed files with 2622 additions and 2237 deletions

View file

@ -11,7 +11,7 @@
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.svg.path.command :as upc]))
[app.common.types.path :as path]))
(defn lookup-profile
([state]
@ -157,7 +157,7 @@
shape)
modifiers (dm/get-in content-modifiers [id :content-modifiers])
shape (if (some? modifiers)
(update shape :content upc/apply-content-modifiers modifiers)
(update shape :content path/apply-content-modifiers modifiers)
shape)]
(assoc result id shape))
result))

View file

@ -10,9 +10,10 @@
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cph]
[app.common.geom.shapes :as gsh]
[app.common.svg.path.shapes-to-path :as stp]
[app.common.types.component :as ctc]
[app.common.types.container :as ctn]
[app.common.types.path :as path]
[app.common.types.path.bool :as bool]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
@ -30,7 +31,7 @@
(or id (uuid/next))
shapes
(mapv #(stp/convert-to-path % objects) shapes)
(mapv #(path/convert-to-path % objects) shapes)
head
(if (= type :difference) (first shapes) (last shapes))
@ -38,7 +39,7 @@
head
(cond-> head
(and (contains? head :svg-attrs) (empty? (:fills head)))
(assoc :fills stp/default-bool-fills))
(assoc :fills bool/default-fills))
shape
{:id shape-id
@ -51,7 +52,7 @@
shape
(-> shape
(merge (select-keys head stp/style-properties))
(merge (select-keys head bool/style-properties))
(cts/setup-shape)
(gsh/update-bool-selrect shapes objects))]
@ -108,12 +109,12 @@
[type group objects]
(let [shapes (->> (:shapes group)
(map #(get objects %))
(mapv #(stp/convert-to-path % objects)))
(mapv #(path/convert-to-path % objects)))
head (if (= type :difference) (first shapes) (last shapes))
head (cond-> head
(and (contains? head :svg-attrs) (empty? (:fills head)))
(assoc :fills stp/default-bool-fills))
head-data (select-keys head stp/style-properties)]
(assoc :fills bool/default-fills))
head-data (select-keys head bool/style-properties)]
(-> group
(assoc :type :bool)
@ -136,7 +137,7 @@
(-> shape
(assoc :type :group)
(dissoc :bool-type)
(d/without-keys stp/style-group-properties)
(d/without-keys bool/style-group-properties)
(gsh/update-group-selrect
(mapv (d/getf objects)
(:shapes shape)))))

View file

@ -11,6 +11,7 @@
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.types.modifiers :as ctm]
[app.common.types.path :as path]
[app.common.types.shape :as cts]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.shapes :as dwsh]
@ -65,6 +66,10 @@
(-> (assoc :height 17 :width 4 :grow-type :auto-width)
(cts/setup-shape))
(or (cfh/path-shape? shape)
(cfh/bool-shape? shape))
(update :content path/content)
:always
(dissoc :initialized? :click-draw?))]

View file

@ -9,11 +9,10 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.flex-layout :as gslf]
[app.common.geom.shapes.grid-layout :as gslg]
[app.common.geom.shapes.path :as gsp]
[app.common.types.container :as ctn]
[app.common.types.path.segment :as path.segm]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
@ -37,8 +36,8 @@
(fn [object]
(let [segments (-> (:segments object)
(conj point))
content (gsp/segments->content segments)
selrect (gsh/content->selrect content)
content (path.segm/segments->content segments)
selrect (path.segm/content->selrect content)
points (grc/rect->points selrect)]
(-> object
(assoc :segments segments)
@ -81,8 +80,8 @@
(update-in state [:workspace-drawing :object]
(fn [{:keys [segments] :as shape}]
(let [segments (ups/simplify segments simplify-tolerance)
content (gsp/segments->content segments)
selrect (gsh/content->selrect content)
content (path.segm/segments->content segments)
selrect (path.segm/content->selrect content)
points (grc/rect->points selrect)]
(-> shape
(dissoc :segments)

View file

@ -18,6 +18,7 @@
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.modifiers :as ctm]
[app.common.types.path :as path]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.attrs :refer [editable-attrs]]
[app.common.types.shape.layout :as ctl]
@ -705,6 +706,9 @@
(gsh/transform-shape modifiers)
(cond-> (d/not-empty? pos-data)
(assoc-position-data pos-data shape))
(cond-> (or (cfh/path-shape? shape)
(cfh/bool-shape? shape))
(update :content path/content))
(cond-> text-shape?
(update-grow-type shape)))))]

View file

@ -6,12 +6,10 @@
(ns app.main.data.workspace.path.changes
(:require
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.types.path :as path]
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.path.common :refer [check-path-content!]]
[app.main.data.workspace.path.helpers :as helpers]
[app.main.data.workspace.path.state :as st]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -20,31 +18,25 @@
"Generates changes to update the new content of the shape"
[it objects page-id shape old-content new-content]
(dm/assert!
"expected valid path content"
(and (check-path-content! old-content)
(check-path-content! new-content)))
(assert (path/check-path-content old-content))
(assert (path/check-path-content new-content))
(let [shape-id (:id shape)
[old-points old-selrect]
(helpers/content->points+selrect shape old-content)
[new-points new-selrect]
(helpers/content->points+selrect shape new-content)
;; We set the old values so the update-shapes works
objects
(-> objects
(update
shape-id
assoc
:content old-content
:selrect old-selrect
:points old-points))
(update objects shape-id
(fn [shape]
(-> shape
(assoc :content old-content)
(path/update-geometry))))
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))]
changes
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
new-content
(path/content new-content)]
(cond
;; https://tree.taiga.io/project/penpot/issue/2366
@ -60,10 +52,9 @@
(-> changes
(pcb/update-shapes [shape-id]
(fn [shape]
(assoc shape
:content new-content
:selrect new-selrect
:points new-points)))
(-> shape
(assoc :content new-content)
(path/update-geometry))))
(pcb/resize-parents [shape-id])))))
(defn save-path-content
@ -88,6 +79,7 @@
id (get-in state [:workspace-local :edition])
old-content (get-in state [:workspace-local :edit-path id :old-content])
shape (st/get-path state)]
(if (and (some? old-content) (some? (:id shape)))
(let [changes (generate-path-changes it objects page-id shape old-content (:content shape))]
(rx/of (dch/commit-changes changes)))

View file

@ -6,44 +6,10 @@
(ns app.main.data.workspace.path.common
(:require
[app.common.schema :as sm]
[app.common.svg.path.subpath :as ups]
[app.common.types.path :as path]
[app.main.data.workspace.path.state :as st]
[potok.v2.core :as ptk]))
(def valid-commands
#{:move-to
:line-to
:line-to-horizontal
:line-to-vertical
:curve-to
:smooth-curve-to
:quadratic-bezier-curve-to
:smooth-quadratic-bezier-curve-to
:elliptical-arc
:close-path})
;; FIXME: should this schema be defined on common.types ?
(def ^:private
schema:path-content
[:vector {:title "PathContent"}
[:map {:title "PathContentEntry"}
[:command [::sm/one-of valid-commands]]
;; FIXME: remove the `?` from prop name
[:relative? {:optional true} :boolean]
[:params {:optional true}
[:map {:title "PathContentEntryParams"}
[:x :double]
[:y :double]
[:c1x {:optional true} :double]
[:c1y {:optional true} :double]
[:c2x {:optional true} :double]
[:c2y {:optional true} :double]]]]])
(def check-path-content!
(sm/check-fn schema:path-content))
(defn init-path []
(ptk/reify ::init-path))
@ -59,4 +25,4 @@
(let [id (st/get-path-id state)]
(-> state
(update-in [:workspace-local :edit-path id] clean-edit-state)
(update-in (st/get-path-location state :content) ups/close-subpaths))))))
(update-in (st/get-path-location state :content) path/close-subpaths))))))

View file

@ -9,9 +9,10 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.flex-layout :as gsl]
[app.common.svg.path.command :as upc]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.types.container :as ctn]
[app.common.types.path :as path]
[app.common.types.path.helpers :as path.helpers]
[app.common.types.path.segment :as path.segment]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
@ -19,7 +20,7 @@
[app.main.data.workspace.drawing.common :as dwdc]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.path.changes :as changes]
[app.main.data.workspace.path.common :as common :refer [check-path-content!]]
[app.main.data.workspace.path.common :as common]
[app.main.data.workspace.path.helpers :as helpers]
[app.main.data.workspace.path.state :as st]
[app.main.data.workspace.path.streams :as streams]
@ -39,10 +40,10 @@
fix-angle? shift?
last-point (get-in state [:workspace-local :edit-path id :last-point])
position (cond-> (gpt/point x y)
fix-angle? (helpers/position-fixed-angle last-point))
fix-angle? (path.helpers/position-fixed-angle last-point))
shape (st/get-path state)
{:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])
command (helpers/next-node shape position last-point prev-handler)]
command (path.segment/next-node shape position last-point prev-handler)]
(assoc-in state [:workspace-local :edit-path id :preview] command)))))
(defn add-node
@ -54,7 +55,7 @@
fix-angle? shift?
{:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])
position (cond-> (gpt/point x y)
fix-angle? (helpers/position-fixed-angle last-point))]
fix-angle? (path.helpers/position-fixed-angle last-point))]
(if-not (= last-point position)
(-> state
(assoc-in [:workspace-local :edit-path id :last-point] position)
@ -75,12 +76,12 @@
index (or index (count content))
prefix (or prefix :c1)
position (or position (upc/command->point (nth content (dec index))))
position (or position (path.segment/get-point (nth content (dec index))))
old-handler (upc/handler->point content index prefix)
old-handler (path.segment/handler->point content index prefix)
handler-position (cond-> (gpt/point x y)
shift? (helpers/position-fixed-angle position))
shift? (path.helpers/position-fixed-angle position))
{dx :x dy :y} (if (some? old-handler)
(gpt/add (gpt/to-vec old-handler position)
@ -102,7 +103,7 @@
modifiers (get-in state [:workspace-local :edit-path id :content-modifiers])
content (-> (st/get-path state :content)
(upc/apply-content-modifiers modifiers))
(path/apply-content-modifiers modifiers))
handler (get-in state [:workspace-local :edit-path id :drag-handler])]
(-> state
@ -110,7 +111,7 @@
(update-in [:workspace-local :edit-path id] dissoc :drag-handler)
(update-in [:workspace-local :edit-path id] dissoc :content-modifiers)
(assoc-in [:workspace-local :edit-path id :prev-handler] handler)
(update-in (st/get-path-location state) helpers/update-selrect))))
(update-in (st/get-path-location state) path/update-geometry))))
ptk/WatchEvent
(watch [_ state _]
@ -128,7 +129,7 @@
ptk/WatchEvent
(watch [_ state stream]
(let [content (st/get-path state :content)
handlers (-> (upc/content->handlers content)
handlers (-> (path.segment/content->handlers content)
(get position))
[idx prefix] (when (= (count handlers) 1)
@ -254,7 +255,12 @@
(update [_ state]
(let [objects (dsh/lookup-page-objects state)
content (get-in state [:workspace-drawing :object :content] [])
position (gpt/point (get-in content [0 :params] nil))
;; FIXME: use native operation for retrieve the first position
position (-> (nth content 0)
(get :params)
(gpt/point))
frame-id (->> (ctst/top-nested-frame objects position)
(ctn/get-first-not-copy-parent objects) ;; We don't want to change the structure of component copies
:id)
@ -274,11 +280,10 @@
(ptk/reify ::handle-new-shape-result
ptk/UpdateEvent
(update [_ state]
(let [content (get-in state [:workspace-drawing :object :content] [])]
(let [content (dm/get-in state [:workspace-drawing :object :content])]
(dm/assert!
"expected valid path content"
(check-path-content! content))
(assert (path/check-path-content content)
"expected valid path content instance")
(if (> (count content) 1)
(assoc-in state [:workspace-drawing :object :initialized?] true)
@ -286,8 +291,8 @@
ptk/WatchEvent
(watch [_ state _]
(let [content (get-in state [:workspace-drawing :object :content] [])]
(if (and (seq content) (> (count content) 1))
(when-let [content (dm/get-in state [:workspace-drawing :object :content])]
(if (> (count content) 1)
(rx/of (setup-frame)
(dwdc/handle-finish-drawing)
(dwe/start-edition-mode shape-id)
@ -300,9 +305,8 @@
(ptk/reify ::handle-new-shape
ptk/UpdateEvent
(update [_ state]
(let [shape (cts/setup-shape {:type :path})]
(-> state
(update :workspace-drawing assoc :object shape))))
(let [shape (cts/setup-shape {:type :path :content (path/content nil)})]
(update state :workspace-drawing assoc :object shape)))
ptk/WatchEvent
(watch [_ state stream]
@ -334,12 +338,12 @@
edit-mode (get-in state [:workspace-local :edit-path id :edit-mode])]
(if (= :draw edit-mode)
(rx/concat
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path))
(rx/of (dwsh/update-shapes [id] path/convert-to-path))
(rx/of (handle-drawing id))
(->> stream
(rx/filter (ptk/type? ::common/finish-path))
(rx/take 1)
(rx/merge-map #(rx/of (check-changed-content)))))
(rx/map check-changed-content)))
(rx/empty))))))
(defn check-changed-content []

View file

@ -10,10 +10,9 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.svg.path.command :as upc]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.svg.path.subpath :as ups]
[app.common.types.path :as path]
[app.common.types.path.helpers :as path.helpers]
[app.common.types.path.segment :as path.segment]
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.edition :as dwe]
@ -27,7 +26,6 @@
[app.main.data.workspace.shapes :as dwsh]
[app.main.streams :as ms]
[app.util.mouse :as mse]
[app.util.path.tools :as upt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -58,10 +56,10 @@
content-modifiers (dm/get-in state [:workspace-local :edit-path id :content-modifiers])
content (:content shape)
new-content (upc/apply-content-modifiers content content-modifiers)
new-content (path/apply-content-modifiers content content-modifiers)
old-points (->> content upg/content->points)
new-points (->> new-content upg/content->points)
old-points (->> content path.segment/content->points)
new-points (->> new-content path.segment/content->points)
point-change (->> (map hash-map old-points new-points) (reduce merge))]
(when (and (some? new-content) (some? shape))
@ -75,8 +73,8 @@
(defn modify-content-point
[content {dx :x dy :y} modifiers point]
(let [point-indices (upc/point-indices content point) ;; [indices]
handler-indices (upc/handler-indices content point) ;; [[index prefix]]
(let [point-indices (path.segment/point-indices content point) ;; [indices]
handler-indices (path.segment/handler-indices content point) ;; [[index prefix]]
modify-point
(fn [modifiers index]
@ -116,7 +114,7 @@
(let [id (st/get-path-id state)
content (st/get-path state :content)
to-point (cond-> to-point
(:shift? to-point) (helpers/position-fixed-angle from-point))
(:shift? to-point) (path.helpers/position-fixed-angle from-point))
delta (gpt/subtract to-point from-point)
@ -144,7 +142,7 @@
selected? (contains? selected-points position)]
(streams/drag-stream
(rx/of
(dwsh/update-shapes [id] upsp/convert-to-path)
(dwsh/update-shapes [id] path/convert-to-path)
(when-not selected? (selection/select-node position shift?))
(drag-selected-points @ms/mouse-position))
(rx/of (selection/select-node position shift?)))))))
@ -163,7 +161,7 @@
start-position (apply min-key #(gpt/distance start-position %) selected-points)
content (st/get-path state :content)
points (upg/content->points content)]
points (path.segment/content->points content)]
(rx/concat
;; This stream checks the consecutive mouse positions to do the dragging
@ -228,7 +226,7 @@
mov-vec (gpt/multiply (get-displacement direction) scale)]
(rx/concat
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path))
(rx/of (dwsh/update-shapes [id] path/convert-to-path))
(rx/merge
(->> move-events
(rx/take-until stopper)
@ -256,22 +254,22 @@
start-delta-y (dm/get-in modifiers [index cy] 0)
content (st/get-path state :content)
points (upg/content->points content)
points (path.segment/content->points content)
point (-> content (get (if (= prefix :c1) (dec index) index)) (upc/command->point))
handler (-> content (get index) (upc/get-handler prefix))
point (-> content (nth (if (= prefix :c1) (dec index) index)) (path.segment/get-point))
handler (-> content (nth index) (path.segment/get-handler prefix))
[op-idx op-prefix] (upc/opposite-index content index prefix)
opposite (upc/handler->point content op-idx op-prefix)]
[op-idx op-prefix] (path.segment/opposite-index content index prefix)
opposite (path.segment/handler->point content op-idx op-prefix)]
(streams/drag-stream
(rx/concat
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path))
(rx/of (dwsh/update-shapes [id] path/convert-to-path))
(->> (streams/move-handler-stream handler point handler opposite points)
(rx/map
(fn [{:keys [x y alt? shift?]}]
(let [pos (cond-> (gpt/point x y)
shift? (helpers/position-fixed-angle point))]
shift? (path.helpers/position-fixed-angle point))]
(modify-handler
id
index
@ -294,19 +292,22 @@
(ptk/reify ::start-path-edit
ptk/UpdateEvent
(update [_ state]
(let [objects (dsh/lookup-page-objects state)
(let [objects (dsh/lookup-page-objects state)
edit-path (dm/get-in state [:workspace-local :edit-path id])
content (st/get-path state :content)
state (cond-> state
(cfh/path-shape? objects id)
(st/set-content (ups/close-subpaths content)))]
content (st/get-path state :content)
state (cond-> state
(cfh/path-shape? objects id)
(st/set-content (path/close-subpaths content)))]
(cond-> state
(or (not edit-path) (= :draw (:edit-mode edit-path)))
(or (not edit-path)
(= :draw (:edit-mode edit-path)))
(assoc-in [:workspace-local :edit-path id] {:edit-mode :move
:selected #{}
:snap-toggled false})
(and (some? edit-path) (= :move (:edit-mode edit-path)))
(and (some? edit-path)
(= :move (:edit-mode edit-path)))
(assoc-in [:workspace-local :edit-path id :edit-mode] :draw))))
ptk/WatchEvent
@ -343,7 +344,9 @@
content (st/get-path state :content)]
(-> state
(assoc-in [:workspace-local :edit-path id :old-content] content)
(st/set-content (-> content (upt/split-segments #{from-p to-p} t))))))
(st/set-content (-> content
(path.segment/split-segments #{from-p to-p} t)
(path/content))))))
ptk/WatchEvent
(watch [_ _ _]
@ -355,5 +358,5 @@
ptk/WatchEvent
(watch [_ state _]
(let [id (st/get-path-id state)]
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path)
(rx/of (dwsh/update-shapes [id] path/convert-to-path)
(split-segments event))))))

View file

@ -6,12 +6,11 @@
(ns app.main.data.workspace.path.helpers
(:require
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.svg.path.command :as upc]
[app.common.types.path :as path]
[app.common.types.path.helpers :as path.helpers]
[app.common.types.path.segment :as path.segment]
[app.main.data.workspace.path.common :as common]
[app.util.mouse :as mse]
[potok.v2.core :as ptk]))
@ -28,96 +27,13 @@
(and ^boolean (mse/mouse-event? event)
^boolean (mse/mouse-double-click-event? event)))))
(defn content-center
[content]
(-> content
gsh/content->selrect
grc/rect->center))
(defn content->points+selrect
"Given the content of a shape, calculate its points and selrect"
[shape content]
(let [{:keys [flip-x flip-y]} shape
transform
(cond-> (:transform shape (gmt/matrix))
flip-x (gmt/scale (gpt/point -1 1))
flip-y (gmt/scale (gpt/point 1 -1)))
transform-inverse
(cond-> (gmt/matrix)
flip-x (gmt/scale (gpt/point -1 1))
flip-y (gmt/scale (gpt/point 1 -1))
:always (gmt/multiply (:transform-inverse shape (gmt/matrix))))
center (or (gsh/shape->center shape)
(content-center content))
base-content (gsh/transform-content
content
(gmt/transform-in center transform-inverse))
;; Calculates the new selrect with points given the old center
points (-> (gsh/content->selrect base-content)
(grc/rect->points)
(gsh/transform-points center transform))
points-center (gsh/points->center points)
;; Points is now the selrect but the center is different so we can create the selrect
;; through points
selrect (-> points
(gsh/transform-points points-center transform-inverse)
(grc/points->rect))]
[points selrect]))
(defn update-selrect
"Updates the selrect and points for a path"
[shape]
(let [[points selrect] (content->points+selrect shape (:content shape))]
(assoc shape :points points :selrect selrect)))
(defn closest-angle
[angle]
(cond
(or (> angle 337.5) (<= angle 22.5)) 0
(and (> angle 22.5) (<= angle 67.5)) 45
(and (> angle 67.5) (<= angle 112.5)) 90
(and (> angle 112.5) (<= angle 157.5)) 135
(and (> angle 157.5) (<= angle 202.5)) 180
(and (> angle 202.5) (<= angle 247.5)) 225
(and (> angle 247.5) (<= angle 292.5)) 270
(and (> angle 292.5) (<= angle 337.5)) 315))
(defn position-fixed-angle [point from-point]
(if (and from-point point)
(let [angle (mod (+ 360 (- (gpt/angle point from-point))) 360)
to-angle (closest-angle angle)
distance (gpt/distance point from-point)]
(gpt/angle->point from-point (mth/radians to-angle) distance))
point))
(defn next-node
"Calculates the next-node to be inserted."
[shape position prev-point prev-handler]
(let [position (select-keys position [:x :y])
last-command (-> shape :content last :command)
add-line? (and prev-point (not prev-handler) (not= last-command :close-path))
add-curve? (and prev-point prev-handler (not= last-command :close-path))]
(cond
add-line? {:command :line-to
:params position}
add-curve? {:command :curve-to
:params (upc/make-curve-params position prev-handler)}
:else {:command :move-to
:params position})))
(defn append-node
"Creates a new node in the path. Usually used when drawing."
[shape position prev-point prev-handler]
(let [command (next-node shape position prev-point prev-handler)]
(let [segment (path.segment/next-node (:content shape) position prev-point prev-handler)]
(-> shape
(update :content (fnil conj []) command)
(update-selrect))))
(update :content path.segment/append-segment segment)
(path/update-geometry))))
(defn angle-points [common p1 p2]
(mth/abs
@ -125,7 +41,7 @@
(gpt/to-vec common p1)
(gpt/to-vec common p2))))
(defn calculate-opposite-delta [node handler opposite match-angle? match-distance? dx dy]
(defn- calculate-opposite-delta [node handler opposite match-angle? match-distance? dx dy]
(when (and (some? handler) (some? opposite))
(let [;; To match the angle, the angle should be matching (angle between points 180deg)
angle-handlers (angle-points node handler opposite)
@ -159,14 +75,14 @@
(defn move-handler-modifiers
[content index prefix match-distance? match-angle? dx dy]
(let [[cx cy] (upc/prefix->coords prefix)
[op-idx op-prefix] (upc/opposite-index content index prefix)
(let [[cx cy] (path.helpers/prefix->coords prefix)
[op-idx op-prefix] (path.segment/opposite-index content index prefix)
node (upc/handler->node content index prefix)
handler (upc/handler->point content index prefix)
opposite (upc/handler->point content op-idx op-prefix)
node (path.segment/handler->node content index prefix)
handler (path.segment/handler->point content index prefix)
opposite (path.segment/handler->point content op-idx op-prefix)
[ocx ocy] (upc/prefix->coords op-prefix)
[ocx ocy] (path.helpers/prefix->coords op-prefix)
[odx ody] (calculate-opposite-delta node handler opposite match-angle? match-distance? dx dy)
hnv (if (some? handler)

View file

@ -8,8 +8,8 @@
(:require
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cph]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.types.container :as ctn]
[app.common.types.path :as path]
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[beicon.v2.core :as rx]
@ -35,7 +35,8 @@
changes
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/update-shapes selected #(upsp/convert-to-path % objects))
;; FIXME: use with-objects? true
(pcb/update-shapes selected #(path/convert-to-path % objects))
(pcb/remove-objects children-ids))]
(rx/of (dch/commit-changes changes)))))))

View file

@ -8,7 +8,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.files.helpers :as cph]
[app.common.svg.path.shapes-to-path :as upsp]))
[app.common.types.path.shape-to-path :as stp]))
(defn path-editing?
"Returns true if we're editing a path or creating a new one."
@ -63,8 +63,7 @@
[state & ks]
(let [path-loc (get-path-location state)
shape (-> (get-in state path-loc)
;; Empty map because we know the current shape will not have children
(upsp/convert-to-path {}))]
(stp/convert-to-path {}))]
(if (empty? ks)
shape
(get-in shape ks))))

View file

@ -8,7 +8,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.types.path.segment :as path.segm]
[app.main.constants :refer [zoom-half-pixel-precision]]
[app.main.data.workspace.path.state :as pst]
[app.main.snap :as snap]
@ -170,7 +170,7 @@
ranges-stream
(->> content-stream
(rx/map upg/content->points)
(rx/map path.segm/content->points)
(rx/map snap/create-ranges))]
(->> ms/mouse-position

View file

@ -6,15 +6,16 @@
(ns app.main.data.workspace.path.tools
(:require
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.svg.path.subpath :as ups]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.types.path :as path]
[app.common.types.path.segment :as path.segment]
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.path.changes :as changes]
[app.main.data.workspace.path.state :as st]
[app.main.data.workspace.shapes :as dwsh]
[app.util.path.tools :as upt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -26,19 +27,30 @@
(ptk/reify ::process-path-tool
ptk/WatchEvent
(watch [it state _]
(let [objects (dsh/lookup-page-objects state)
id (st/get-path-id state)
page-id (:current-page-id state)
shape (st/get-path state)
selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{})
points (or points selected-points)]
(let [page-id (get state :current-page-id)
objects (dsh/lookup-page-objects state page-id)
shape (st/get-path state)
id (st/get-path-id state)
selected-points
(dm/get-in state [:workspace-local :edit-path id :selected-points] #{})
points
(or points selected-points)]
(when (and (seq points) (some? shape))
(let [new-content (-> (tool-fn (:content shape) points)
(ups/close-subpaths))
changes (changes/generate-path-changes it objects page-id shape (:content shape) new-content)]
(let [new-content
(-> (tool-fn (:content shape) points)
(path/close-subpaths))
changes
(changes/generate-path-changes it objects page-id shape (:content shape) new-content)]
(rx/concat
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path))
(if (cfh/path-shape? shape)
(rx/empty)
(rx/of (dwsh/update-shapes [id] path/convert-to-path)))
(rx/of (dch/commit-changes changes)
(when (empty? new-content)
(dwe/clear-edition-mode)))))))))))
@ -50,7 +62,7 @@
(process-path-tool
(when point #{point})
(fn [content points]
(reduce upt/make-corner-point content points)))))
(reduce path.segment/make-corner-point content points)))))
(defn make-curve
([]
@ -59,22 +71,22 @@
(process-path-tool
(when point #{point})
(fn [content points]
(reduce upt/make-curve-point content points)))))
(reduce path.segment/make-curve-point content points)))))
(defn add-node []
(process-path-tool (fn [content points] (upt/split-segments content points 0.5))))
(process-path-tool (fn [content points] (path.segment/split-segments content points 0.5))))
(defn remove-node []
(process-path-tool upt/remove-nodes))
(process-path-tool path.segment/remove-nodes))
(defn merge-nodes []
(process-path-tool upt/merge-nodes))
(process-path-tool path.segment/merge-nodes))
(defn join-nodes []
(process-path-tool upt/join-nodes))
(process-path-tool path.segment/join-nodes))
(defn separate-nodes []
(process-path-tool upt/separate-nodes))
(process-path-tool path.segment/separate-nodes))
(defn toggle-snap []
(ptk/reify ::toggle-snap

View file

@ -47,40 +47,53 @@
(defn update-shapes
([ids update-fn] (update-shapes ids update-fn nil))
([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-touched undo-group with-objects? changed-sub-attr]
:or {reg-objects? false save-undo? true stack-undo? false ignore-touched false with-objects? false}}]
([ids update-fn
{:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id
ignore-touched undo-group with-objects? changed-sub-attr]
:or {reg-objects? false
save-undo? true
stack-undo? false
ignore-touched false
with-objects? false}}]
(assert (sm/check-coll-of-uuid ids))
(assert (fn? update-fn))
(assert (every? uuid? ids) "expect a coll of uuid for `ids`")
(assert (fn? update-fn) "the `update-fn` should be a valid function")
(ptk/reify ::update-shapes
ptk/WatchEvent
(watch [it state _]
(let [page-id (or page-id (:current-page-id state))
(let [page-id (or page-id (get state :current-page-id))
objects (dsh/lookup-page-objects state page-id)
ids (into [] (filter some?) ids)
xf-update-layout
(comp
(map (d/getf objects))
(filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?})))
(map :id))
update-layout-ids
(->> ids
(map (d/getf objects))
(filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?})))
(map :id))
(->> (into [] xf-update-layout ids)
(not-empty))
changes (-> (pcb/empty-changes it page-id)
(pcb/set-save-undo? save-undo?)
(pcb/set-stack-undo? stack-undo?)
(cls/generate-update-shapes ids
update-fn
objects
{:attrs attrs
:changed-sub-attr changed-sub-attr
:ignore-tree ignore-tree
:ignore-touched ignore-touched
:with-objects? with-objects?})
(cond-> undo-group
(pcb/set-undo-group undo-group)))
changes
(-> (pcb/empty-changes it page-id)
(pcb/set-save-undo? save-undo?)
(pcb/set-stack-undo? stack-undo?)
(cls/generate-update-shapes ids
update-fn
objects
{:attrs attrs
:changed-sub-attr changed-sub-attr
:ignore-tree ignore-tree
:ignore-touched ignore-touched
:with-objects? with-objects?})
(cond-> undo-group
(pcb/set-undo-group undo-group)))
changes
(add-undo-group changes state)]
changes (add-undo-group changes state)]
(rx/concat
(if (seq (:redo-changes changes))
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))]
@ -88,7 +101,7 @@
(rx/empty))
;; Update layouts for properties marked
(if (d/not-empty? update-layout-ids)
(if update-layout-ids
(rx/of (ptk/data-event :layout/update {:ids update-layout-ids}))
(rx/empty))))))))
@ -112,11 +125,13 @@
(pcb/with-objects objects)
(cfsh/prepare-add-shape shape objects))
changes (cond-> changes
(cfh/text-shape? shape)
(pcb/set-undo-group (:id shape)))
changes
(cond-> changes
(cfh/text-shape? shape)
(pcb/set-undo-group (:id shape)))
undo-id (js/Symbol)]
undo-id
(js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id)

View file

@ -110,4 +110,3 @@
(log/inf :hint "initialized"
:enabled (str/join "," features)
:runtime (str/join "," (:features-runtime state)))))))

View file

@ -7,7 +7,8 @@
(ns app.main.ui.shapes.bool
(:require
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.types.path :as path]
[app.main.ui.hooks :as h]
[app.main.ui.shapes.export :as use]
[app.main.ui.shapes.path :refer [path-shape]]
@ -30,7 +31,7 @@
content
(some? child-objs)
(gsh/calc-bool-content shape child-objs))))
(path/calc-bool-content shape child-objs))))
shape (mf/with-memo [shape content]
(assoc shape :content content))]

View file

@ -13,6 +13,7 @@
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.bounds :as gsb]
[app.common.geom.shapes.text :as gst]
[app.common.types.path :as path]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.ui.context :as muc]
@ -204,7 +205,7 @@
{::mf/wrap-props false}
[{:keys [shape stroke render-id index]}]
(let [open-path? (and ^boolean (cfh/path-shape? shape)
^boolean (gsh/open-path? shape))
^boolean (path/shape-with-open-path? shape))
gradient (:stroke-color-gradient stroke)
alignment (:stroke-alignment stroke :center)
width (:stroke-width stroke 0)
@ -397,7 +398,7 @@
has-stroke? (and (> stroke-width 0)
(not= stroke-style :none))
closed? (or (not ^boolean (cfh/path-shape? shape))
(not ^boolean (gsh/open-path? shape)))
(not ^boolean (path/shape-with-open-path? shape)))
inner? (= :inner stroke-position)
outer? (= :outer stroke-position)]
@ -496,7 +497,7 @@
:style style})
open-path? (and ^boolean (cfh/path-shape? shape)
^boolean (gsh/open-path? shape))]
^boolean (path/shape-with-open-path? shape))]
(when-not ^boolean (cfh/frame-shape? shape)
(when (and (some? shape-blur)
(not ^boolean (:hidden shape-blur)))

View file

@ -7,28 +7,35 @@
(ns app.main.ui.shapes.path
(:require
[app.common.logging :as log]
[app.common.types.path :as path]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
[app.util.object :as obj]
[app.util.path.format :as upf]
[rumext.v2 :as mf]))
(defn- content->string
[content]
(cond
(nil? content)
""
(path/content? content)
(.toString content)
:else
(let [content (path/content content)]
(.toString content))))
(mf/defc path-shape
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
content (:content shape)
{::mf/props :obj}
[{:keys [shape]}]
(let [content (get shape :content)
pdata (mf/with-memo [content]
(try
(upf/format-path content)
(catch :default e
(content->string content)
(catch :default cause
(log/error :hint "unexpected error on formatting path"
:shape-name (:name shape)
:shape-id (:id shape)
:cause e)
"")))
props (-> #js {}
(obj/set! "d" pdata))]
:cause cause)
"")))]
[:& shape-custom-strokes {:shape shape}
[:> :path props]]))
[:path {:d pdata}]]))

View file

@ -10,12 +10,13 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gsp]
[app.common.geom.shapes.text :as gst]
[app.common.math :as mth]
[app.common.svg.path.bool :as pb]
[app.common.svg.path.shapes-to-path :as stp]
[app.common.svg.path.subpath :as ups]
[app.common.types.path :as path]
[app.common.types.path.bool :as path.bool]
[app.common.types.path.helpers :as path.helpers]
[app.common.types.path.segment :as path.segment]
[app.common.types.path.subpath :as path.subpath]
[app.main.refs :as refs]
[app.util.color :as uc]
[app.util.debug :as dbg]
@ -101,49 +102,49 @@
radius (/ 3 zoom)
c1 (-> (get objects (first (:shapes shape)))
(stp/convert-to-path objects))
(path/convert-to-path objects))
c2 (-> (get objects (second (:shapes shape)))
(stp/convert-to-path objects))
(path/convert-to-path objects))
content-a (:content c1)
content-b (:content c2)
bool-type (:bool-type shape)
should-reverse? (and (not= :union bool-type)
(= (ups/clockwise? content-b)
(ups/clockwise? content-a)))
(= (path.subpath/clockwise? content-b)
(path.subpath/clockwise? content-a)))
content-a (-> (:content c1)
(pb/close-paths)
(pb/add-previous))
(path.bool/close-paths)
(path.bool/add-previous))
content-b (-> (:content c2)
(pb/close-paths)
(cond-> should-reverse? (ups/reverse-content))
(pb/add-previous))
(path.bool/close-paths)
(cond-> should-reverse? (path.subpath/reverse-content))
(path.bool/add-previous))
sr-a (gsp/content->selrect content-a)
sr-b (gsp/content->selrect content-b)
sr-a (path.segment/content->selrect content-a)
sr-b (path.segment/content->selrect content-b)
[content-a-split content-b-split] (pb/content-intersect-split content-a content-b sr-a sr-b)
[content-a-split content-b-split] (path.bool/content-intersect-split content-a content-b sr-a sr-b)
;;content-a-geom (gsp/content->geom-data content-a)
;;content-b-geom (gsp/content->geom-data content-b)
;;content-a-split (->> content-a-split #_(filter #(pb/contains-segment? % content-b sr-b content-b-geom)))
;;content-b-split (->> content-b-split #_(filter #(pb/contains-segment? % content-a sr-a content-a-geom)))
;;content-a-geom (path.segment/content->geom-data content-a)
;;content-b-geom (path.segment/content->geom-data content-b)
;;content-a-split (->> content-a-split #_(filter #(path.bool/contains-segment? % content-b sr-b content-b-geom)))
;;content-b-split (->> content-b-split #_(filter #(path.bool/contains-segment? % content-a sr-a content-a-geom)))
]
[:*
(for [[i cmd] (d/enumerate content-a-split)]
(let [p1 (:prev cmd)
p2 (gsp/command->point cmd)
p2 (path.helpers/command->point cmd)
hp (case (:command cmd)
:line-to (-> (gsp/command->line cmd)
(gsp/line-values 0.5))
:line-to (-> (path.helpers/command->line cmd)
(path.helpers/line-values 0.5))
:curve-to (-> (gsp/command->bezier cmd)
(gsp/curve-values 0.5))
:curve-to (-> (path.helpers/command->bezier cmd)
(path.helpers/curve-values 0.5))
nil)]
[:*
(when p1
@ -155,14 +156,14 @@
(for [[i cmd] (d/enumerate content-b-split)]
(let [p1 (:prev cmd)
p2 (gsp/command->point cmd)
p2 (path.helpers/command->point cmd)
hp (case (:command cmd)
:line-to (-> (gsp/command->line cmd)
(gsp/line-values 0.5))
:line-to (-> (path.helpers/command->line cmd)
(path.helpers/line-values 0.5))
:curve-to (-> (gsp/command->bezier cmd)
(gsp/curve-values 0.5))
:curve-to (-> (path.helpers/command->bezier cmd)
(path.helpers/curve-values 0.5))
nil)]
[:*
(when p1

View file

@ -6,8 +6,8 @@
(ns app.main.ui.workspace.shapes.path
(:require
[app.common.svg.path.command :as upc]
[app.main.data.workspace.path.helpers :as helpers]
[app.common.data.macros :as dm]
[app.common.types.path :as types.path]
[app.main.refs :as refs]
[app.main.ui.shapes.path :as path]
[app.main.ui.shapes.shape :refer [shape-container]]
@ -15,25 +15,31 @@
[app.main.ui.workspace.shapes.path.common :as pc]
[rumext.v2 :as mf]))
(defn apply-content-modifiers
(defn- apply-content-modifiers
[shape content-modifiers]
(let [shape (update shape :content upc/apply-content-modifiers content-modifiers)
[_ new-selrect] (helpers/content->points+selrect shape (:content shape))]
(assoc shape :selrect new-selrect)))
(let [shape (update shape :content types.path/apply-content-modifiers content-modifiers)]
(types.path/update-geometry shape)))
(mf/defc path-wrapper
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
content-modifiers-ref (pc/make-content-modifiers-ref (:id shape))
content-modifiers (mf/deref content-modifiers-ref)
editing-id (mf/deref refs/selected-edition)
editing? (= editing-id (:id shape))
[{:keys [shape]}]
(let [shape-id (dm/get-prop shape :id)
content-modifiers-ref
(pc/make-content-modifiers-ref shape-id)
content-modifiers
(mf/deref content-modifiers-ref)
editing-id
(mf/deref refs/selected-edition)
editing?
(= editing-id shape-id)
shape
(mf/use-memo
(mf/deps shape content-modifiers)
#(cond-> shape
(mf/with-memo [shape content-modifiers]
(cond-> shape
(some? content-modifiers)
(apply-content-modifiers content-modifiers)))]

View file

@ -9,9 +9,8 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as gsp]
[app.common.svg.path.command :as upc]
[app.common.svg.path.shapes-to-path :as ups]
[app.common.types.path :as path]
[app.common.types.path.segment :as path.segment]
[app.main.data.workspace.path :as drp]
[app.main.snap :as snap]
[app.main.store :as st]
@ -196,8 +195,8 @@
(defn matching-handler? [content node handlers]
(when (= 2 (count handlers))
(let [[[i1 p1] [i2 p2]] handlers
p1 (upc/handler->point content i1 p1)
p2 (upc/handler->point content i2 p2)
p1 (path.segment/handler->point content i1 p1)
p2 (path.segment/handler->point content i2 p2)
v1 (gpt/to-vec node p1)
v2 (gpt/to-vec node p2)
@ -227,34 +226,36 @@
:as edit-path} (mf/deref edit-path-ref)
selected-points (or selected-points #{})
shape (hooks/use-equal-memo shape)
shape (cond-> shape
(not= :path (:type shape))
(ups/convert-to-path {})
base-content
(get shape :content)
:always
hooks/use-equal-memo)
base-points
(mf/with-memo [base-content]
(path.segment/content->points base-content))
base-content (:content shape)
base-points (mf/use-memo (mf/deps base-content) #(->> base-content gsp/content->points))
content
(path/apply-content-modifiers base-content content-modifiers)
content (upc/apply-content-modifiers base-content content-modifiers)
content-points (mf/use-memo (mf/deps content) #(->> content gsp/content->points))
content-points
(mf/with-memo [content]
(path.segment/content->points content))
point->base (->> (map hash-map content-points base-points) (reduce merge))
base->point (map-invert point->base)
points (into #{} content-points)
last-p (->> content last upc/command->point)
handlers (upc/content->handlers content)
last-p (->> content last path.segment/get-point)
handlers (path.segment/content->handlers content)
start-p? (not (some? last-point))
[snap-selected snap-points]
(cond
(some? drag-handler) [#{drag-handler} points]
(some? preview) [#{(upc/command->point preview)} points]
(some? preview) [#{(path.segment/get-point preview)} points]
(some? moving-handler) [#{moving-handler} points]
:else
[(->> selected-points (map base->point) (into #{}))
@ -282,7 +283,7 @@
ms/mouse-position
(mf/deps shape zoom)
(fn [position]
(when-let [point (gsp/path-closest-point shape position)]
(when-let [point (path.segment/path-closest-point shape position)]
(reset! hover-point (when (< (gpt/distance position point) (/ 10 zoom)) point)))))
[:g.path-editor {:ref editor-ref}
@ -313,7 +314,7 @@
(for [[index position] (d/enumerate points)]
(let [show-handler?
(fn [[index prefix]]
(let [handler-position (upc/handler->point content index prefix)]
(let [handler-position (path.segment/handler->point content index prefix)]
(not= position handler-position)))
pos-handlers (get handlers position)
@ -327,7 +328,7 @@
[:g.path-node {:key (dm/str index "-" (:x position) "-" (:y position))}
[:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")}
(for [[hindex prefix] pos-handlers]
(let [handler-position (upc/handler->point content hindex prefix)
(let [handler-position (path.segment/handler->point content hindex prefix)
handler-hover? (contains? hover-handlers [hindex prefix])
moving-handler? (= handler-position moving-handler)
matching-handler? (matching-handler? content position pos-handlers)]

View file

@ -12,6 +12,7 @@
[app.common.geom.shapes :as gsh]
[app.common.text :as txt]
[app.common.types.component :as ctk]
[app.common.types.path :as path]
[app.common.types.shape.attrs :refer [editable-attrs]]
[app.common.types.shape.layout :as ctl]
[app.main.refs :as refs]
@ -294,7 +295,7 @@
file-id (unchecked-get props "file-id")
shared-libs (unchecked-get props "libraries")
show-caps (some #(and (= :path (:type %)) (gsh/open-path? %)) shapes)
show-caps (some #(and (= :path (:type %)) (path/shape-with-open-path? %)) shapes)
;; Selrect/points only used for measures and it's the one that changes the most. We separate it
;; so we can memoize it

View file

@ -7,16 +7,15 @@
(ns app.main.ui.workspace.viewport.path-actions
(:require-macros [app.main.style :as stl])
(:require
[app.common.types.path.segment :as path.segm]
[app.main.data.workspace.path :as drp]
[app.main.data.workspace.path.shortcuts :as sc]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.workspace.shapes.path.common :as pc]
[app.util.i18n :as i18n :refer [tr]]
[app.util.path.tools :as upt]
[rumext.v2 :as mf]))
(def ^:private pentool-icon
(i/icon-xref :pentool (stl/css :pentool-icon :pathbar-icon)))
@ -49,7 +48,7 @@
(defn check-enabled [content selected-points]
(let [segments (upt/get-segments content selected-points)
(let [segments (path.segm/get-segments content selected-points)
num-segments (count segments)
num-points (count selected-points)
points-selected? (seq selected-points)
@ -58,7 +57,7 @@
max-segments (-> num-points
(* (- num-points 1))
(/ 2))
is-curve? (some #(upt/is-curve? content %) selected-points)]
is-curve? (some #(path.segm/is-curve? content %) selected-points)]
{:make-corner (and points-selected? is-curve?)
:make-curve (and points-selected? (not is-curve?))

View file

@ -15,18 +15,19 @@
[app.common.record :as crc]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.svg.path :as path]
[app.common.svg.path :as svg.path]
[app.common.text :as txt]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.grid :as ctg]
[app.common.types.path :as path]
[app.common.types.path.segment :as path.segm]
[app.common.types.shape :as cts]
[app.common.types.shape.blur :as ctsb]
[app.common.types.shape.export :as ctse]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.path :as ctsp]
[app.common.types.shape.radius :as ctsr]
[app.common.types.shape.shadow :as ctss]
[app.common.uuid :as uuid]
@ -1312,18 +1313,19 @@
:get #(-> % u/proxy->shape :content upf/format-path)
:set
(fn [_ value]
(let [content (->> (path/parse value))]
(let [content (svg.path/parse value)]
(cond
(not (cfh/path-shape? data))
(u/display-not-valid :content-type type)
(not (sm/validate ::ctsp/content content))
;; FIXME: revisit path content validation
(not (sm/validate ::path/content content))
(u/display-not-valid :content value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :content "Plugin doesn't have 'content:write' permission")
:else
(let [selrect (gsh/content->selrect content)
(let [selrect (path.segm/content->selrect content)
points (grc/rect->points selrect)]
(st/emit! (dwsh/update-shapes [id] (fn [shape] (assoc shape :content content :selrect selrect :points points))))))))}))))))

View file

@ -10,8 +10,8 @@
["react-dom/server" :as rds]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.path :as path]
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.path :as path]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.fonts :as fonts]
@ -309,13 +309,14 @@
(h/call wasm/internal-module "stringToUTF8" str offset size)
(h/call wasm/internal-module "_set_shape_path_attrs" (count attrs))))
;; FIXME: revisit on heap refactor is merged to use u32 instead u8
(defn set-shape-path-content
[content]
(let [pdata (path/path-data content)
size (* (count pdata) path/SEGMENT-BYTE-SIZE)
(let [pdata (path/content content)
size (path/get-byte-size content)
offset (mem/alloc-bytes size)
heap (mem/get-heap-u8)]
(path/-write-to pdata (.-buffer heap) offset)
(path/write-to pdata (.-buffer heap) offset)
(h/call wasm/internal-module "_set_shape_path_content")))
(defn set-shape-svg-raw-content

View file

@ -5,12 +5,19 @@
;; Copyright (c) KALEIDOS INC
(ns app.util.path.format
"Legacy path data formater, replaced by
app.common.types.path.PathData type.
WARNING: Pending to be removed from codebase once completly unused"
(:require
[app.common.svg.path.command :as upc]
[app.common.svg.path.subpath :refer [pt=]]
[app.common.geom.point :as gpt]
[app.common.types.path.segment :as path.segm]
[app.util.array :as arr]))
;; TODO: move to common
(defn pt=
"Check if two points are close"
[p1 p2]
(< (gpt/distance p1 p2) 0.1))
(def path-precision 3)
@ -115,7 +122,7 @@
(try
(let [result (make-array (count content))]
(reduce (fn [last-move current]
(let [point (upc/command->point current)
(let [point (path.segm/get-point current)
current-move? (= :move-to (:command current))
last-move (if current-move? point last-move)]

View file

@ -1,461 +0,0 @@
;; 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
(ns app.util.path.tools
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.svg.path.command :as upc]
[clojure.set :as set]))
;; FIXME: move to common, there are nothing tied to frontend
(defn remove-line-curves
"Remove all curves that have both handlers in the same position that the
beginning and end points. This makes them really line-to commands"
[content]
(let [with-prev (d/enumerate (d/with-prev content))
process-command
(fn [content [index [command prev]]]
(let [cur-point (upc/command->point command)
pre-point (upc/command->point prev)
handler-c1 (upc/get-handler command :c1)
handler-c2 (upc/get-handler command :c2)]
(if (and (= :curve-to (:command command))
(= cur-point handler-c2)
(= pre-point handler-c1))
(assoc content index {:command :line-to
:params (into {} cur-point)})
content)))]
(reduce process-command content with-prev)))
(defn make-corner-point
"Changes the content to make a point a 'corner'"
[content point]
(let [handlers (-> (upc/content->handlers content)
(get point))
change-content
(fn [content [index prefix]]
(let [cx (d/prefix-keyword prefix :x)
cy (d/prefix-keyword prefix :y)]
(-> content
(assoc-in [index :params cx] (:x point))
(assoc-in [index :params cy] (:y point)))))]
(as-> content $
(reduce change-content $ handlers)
(remove-line-curves $))))
(defn line->curve
[from-p cmd]
(let [to-p (upc/command->point cmd)
v (gpt/to-vec from-p to-p)
d (gpt/distance from-p to-p)
dv1 (-> (gpt/normal-left v)
(gpt/scale (/ d 3)))
h1 (gpt/add from-p dv1)
dv2 (-> (gpt/to-vec to-p h1)
(gpt/unit)
(gpt/scale (/ d 3)))
h2 (gpt/add to-p dv2)]
(-> cmd
(assoc :command :curve-to)
(update :params (fn [params]
;; ensure plain map
(-> (into {} params)
(assoc :c1x (:x h1))
(assoc :c1y (:y h1))
(assoc :c2x (:x h2))
(assoc :c2y (:y h2))))))))
(defn is-curve?
[content point]
(let [handlers (-> (upc/content->handlers content)
(get point))
handler-points (map #(upc/handler->point content (first %) (second %)) handlers)]
(some #(not= point %) handler-points)))
(def ^:private xf:mapcat-points
(comp
(mapcat #(vector (:next-p %) (:prev-p %)))
(remove nil?)))
(defn make-curve-point
"Changes the content to make the point a 'curve'. The handlers will be positioned
in the same vector that results from the previous->next points but with fixed length."
[content point]
(let [indices (upc/point-indices content point)
vectors (map (fn [index]
(let [cmd (nth content index)
prev-i (dec index)
prev (when (not (= :move-to (:command cmd)))
(get content prev-i))
next-i (inc index)
next (get content next-i)
next (when (not (= :move-to (:command next)))
next)]
{:index index
:prev-i (when (some? prev) prev-i)
:prev-c prev
:prev-p (upc/command->point prev)
:next-i (when (some? next) next-i)
:next-c next
:next-p (upc/command->point next)
:command cmd}))
indices)
points (into #{} xf:mapcat-points vectors)]
(if (= (count points) 2)
(let [v1 (gpt/to-vec (first points) point)
v2 (gpt/to-vec (first points) (second points))
vp (gpt/project v1 v2)
vh (gpt/subtract v1 vp)
add-curve
(fn [content {:keys [index prev-p next-p next-i]}]
(let [cur-cmd (get content index)
next-cmd (get content next-i)
;; New handlers for prev-point and next-point
prev-h (when (some? prev-p) (gpt/add prev-p vh))
next-h (when (some? next-p) (gpt/add next-p vh))
;; Correct 1/3 to the point improves the curve
prev-correction (when (some? prev-h) (gpt/scale (gpt/to-vec prev-h point) (/ 1 3)))
next-correction (when (some? next-h) (gpt/scale (gpt/to-vec next-h point) (/ 1 3)))
prev-h (when (some? prev-h) (gpt/add prev-h prev-correction))
next-h (when (some? next-h) (gpt/add next-h next-correction))]
(cond-> content
(and (= :line-to (:command cur-cmd)) (some? prev-p))
(update index upc/update-curve-to prev-p prev-h)
(and (= :line-to (:command next-cmd)) (some? next-p))
(update next-i upc/update-curve-to next-h next-p)
(and (= :curve-to (:command cur-cmd)) (some? prev-p))
(update index upc/update-handler :c2 prev-h)
(and (= :curve-to (:command next-cmd)) (some? next-p))
(update next-i upc/update-handler :c1 next-h))))]
(reduce add-curve content vectors))
(let [add-curve
(fn [content {:keys [index command prev-p next-c next-i]}]
(cond-> content
(= :line-to (:command command))
(update index #(line->curve prev-p %))
(= :curve-to (:command command))
(update index #(line->curve prev-p %))
(= :line-to (:command next-c))
(update next-i #(line->curve point %))
(= :curve-to (:command next-c))
(update next-i #(line->curve point %))))]
(reduce add-curve content vectors)))))
(defn get-segments
"Given a content and a set of points return all the segments in the path
that uses the points"
[content points]
(let [point-set (set points)]
(loop [segments []
prev-point nil
start-point nil
index 0
cur-cmd (first content)
content (rest content)]
(let [command (:command cur-cmd)
close-path? (= command :close-path)
move-to? (= command :move-to)
;; Close-path makes a segment from the last point to the initial path point
cur-point (if close-path?
start-point
(upc/command->point cur-cmd))
;; If there is a move-to we don't have a segment
prev-point (if move-to?
nil
prev-point)
;; We update the start point
start-point (if move-to?
cur-point
start-point)
is-segment? (and (some? prev-point)
(contains? point-set prev-point)
(contains? point-set cur-point))
segments (cond-> segments
is-segment?
(conj {:start prev-point
:end cur-point
:cmd cur-cmd
:index index}))]
(if (some? cur-cmd)
(recur segments
cur-point
start-point
(inc index)
(first content)
(rest content))
segments)))))
(defn split-segments
"Given a content creates splits commands between points with new segments"
[content points value]
(let [split-command
(fn [{:keys [start end cmd index]}]
(case (:command cmd)
:line-to [index (upg/split-line-to start cmd value)]
:curve-to [index (upg/split-curve-to start cmd value)]
:close-path [index [(upc/make-line-to (gpt/lerp start end value)) cmd]]
nil))
cmd-changes
(->> (get-segments content points)
(into {} (comp (map split-command)
(filter (comp not nil?)))))
process-segments
(fn [[index command]]
(if (contains? cmd-changes index)
(get cmd-changes index)
[command]))]
(into [] (mapcat process-segments) (d/enumerate content))))
(defn remove-nodes
"Removes from content the points given. Will try to reconstruct the paths
to keep everything consistent"
[content points]
(if (empty? points)
content
(let [content (d/with-prev content)]
(loop [result []
last-handler nil
[cur-cmd prev-cmd] (first content)
content (rest content)]
(if (nil? cur-cmd)
;; The result with be an array of arrays were every entry is a subpath
(->> result
;; remove empty and only 1 node subpaths
(filter #(> (count %) 1))
;; flatten array-of-arrays plain array
(flatten)
(into []))
(let [move? (= :move-to (:command cur-cmd))
curve? (= :curve-to (:command cur-cmd))
;; When the old command was a move we start a subpath
result (if move? (conj result []) result)
subpath (peek result)
point (upc/command->point cur-cmd)
old-prev-point (upc/command->point prev-cmd)
new-prev-point (upc/command->point (peek subpath))
remove? (contains? points point)
;; We store the first handler for the first curve to be removed to
;; use it for the first handler of the regenerated path
cur-handler (cond
(and (not last-handler) remove? curve?)
(select-keys (:params cur-cmd) [:c1x :c1y])
(not remove?)
nil
:else
last-handler)
cur-cmd (cond-> cur-cmd
;; If we're starting a subpath and it's not a move make it a move
(and (not move?) (empty? subpath))
(assoc :command :move-to
:params (select-keys (:params cur-cmd) [:x :y]))
;; If have a curve the first handler will be relative to the previous
;; point. We change the handler to the new previous point
(and curve? (seq subpath) (not= old-prev-point new-prev-point))
(update :params merge last-handler))
head-idx (dec (count result))
result (cond-> result
(not remove?)
(update head-idx conj cur-cmd))]
(recur result
cur-handler
(first content)
(rest content))))))))
(defn join-nodes
"Creates new segments between points that weren't previously"
[content points]
(let [segments-set (into #{}
(map (juxt :start :end))
(get-segments content points))
create-line-command (fn [point other]
[(upc/make-move-to point)
(upc/make-line-to other)])
not-segment? (fn [point other] (and (not (contains? segments-set [point other]))
(not (contains? segments-set [other point]))))
new-content (->> (d/map-perm create-line-command not-segment? points)
(flatten)
(into []))]
(into content new-content)))
(defn separate-nodes
"Removes the segments between the points given"
[content points]
(let [content (d/with-prev content)]
(loop [result []
[cur-cmd prev-cmd] (first content)
content (rest content)]
(if (nil? cur-cmd)
(->> result
(filter #(> (count %) 1))
(flatten)
(into []))
(let [prev-point (upc/command->point prev-cmd)
cur-point (upc/command->point cur-cmd)
cur-cmd (cond-> cur-cmd
(and (contains? points prev-point)
(contains? points cur-point))
(assoc :command :move-to
:params (select-keys (:params cur-cmd) [:x :y])))
move? (= :move-to (:command cur-cmd))
result (if move? (conj result []) result)
head-idx (dec (count result))
result (-> result
(update head-idx conj cur-cmd))]
(recur result
(first content)
(rest content)))))))
(defn- add-to-set
"Given a list of sets adds the value to the target set"
[set-list target value]
(->> set-list
(mapv (fn [it]
(cond-> it
(= it target) (conj value))))))
(defn- join-sets
"Given a list of sets join two sets in the list into a new one"
[set-list target other]
(conj (->> set-list
(filterv #(and (not= % target)
(not= % other))))
(set/union target other)))
(defn group-segments [segments]
(loop [result []
{point-a :start point-b :end :as segment} (first segments)
segments (rest segments)]
(if (nil? segment)
result
(let [set-a (d/seek #(contains? % point-a) result)
set-b (d/seek #(contains? % point-b) result)
result (cond-> result
(and (nil? set-a) (nil? set-b))
(conj #{point-a point-b})
(and (some? set-a) (nil? set-b))
(add-to-set set-a point-b)
(and (nil? set-a) (some? set-b))
(add-to-set set-b point-a)
(and (some? set-a) (some? set-b) (not= set-a set-b))
(join-sets set-a set-b))]
(recur result
(first segments)
(rest segments))))))
(defn calculate-merge-points [group-segments points]
(let [index-merge-point (fn [group] (vector group (gpt/center-points group)))
index-group (fn [point] (vector point (d/seek #(contains? % point) group-segments)))
group->merge-point (into {} (map index-merge-point) group-segments)
point->group (into {} (map index-group) points)]
(d/mapm #(group->merge-point %2) point->group)))
;; TODO: Improve the replace for curves
(defn replace-points
"Replaces the points in a path for its merge-point"
[content point->merge-point]
(let [replace-command
(fn [cmd]
(let [point (upc/command->point cmd)]
(if (contains? point->merge-point point)
(let [merge-point (get point->merge-point point)]
(-> cmd (update :params assoc :x (:x merge-point) :y (:y merge-point))))
cmd)))]
(->> content
(mapv replace-command))))
(defn merge-nodes
"Reduces the contiguous segments in points to a single point"
[content points]
(let [segments (get-segments content points)]
(if (seq segments)
(let [point->merge-point (-> segments
(group-segments)
(calculate-merge-points points))]
(-> content
(separate-nodes points)
(replace-points point->merge-point)))
content)))

View file

@ -11,13 +11,13 @@
[app.common.exceptions :as ex]
[app.common.files.builder :as fb]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as gpa]
[app.common.json :as json]
[app.common.logging :as log]
[app.common.media :as cm]
[app.common.schema :as sm]
[app.common.text :as ct]
[app.common.time :as tm]
[app.common.types.path :as path]
[app.common.uuid :as uuid]
[app.main.repo :as rp]
[app.util.http :as http]
@ -330,7 +330,7 @@
(d/update-when :x + (:x frame))
(d/update-when :y + (:y frame))
(cond-> (= :path type)
(update :content gpa/move-content (gpt/point (:x frame) (:y frame)))))
(update :content path/move-content (gpt/point (:x frame) (:y frame)))))
data)))