0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-20 19:51:23 -05:00

Merge pull request from penpot/feature/beziers

Bezier curves
This commit is contained in:
Andrey Antukh 2020-11-24 13:56:23 +01:00 committed by GitHub
commit ef9b0062dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 3058 additions and 1484 deletions

1
.gitignore vendored
View file

@ -30,3 +30,4 @@ node_modules
/media
/deploy
/web
/_dump

View file

@ -0,0 +1,71 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.attrs)
(defn get-attrs-multi
[shapes attrs]
;; Extract some attributes of a list of shapes.
;; For each attribute, if the value is the same in all shapes,
;; wll take this value. If there is any shape that is different,
;; the value of the attribute will be the keyword :multiple.
;;
;; If some shape has the value nil in any attribute, it's
;; considered a different value. If the shape does not contain
;; the attribute, it's ignored in the final result.
;;
;; Example:
;; (def shapes [{:stroke-color "#ff0000"
;; :stroke-width 3
;; :fill-color "#0000ff"
;; :x 1000 :y 2000 :rx nil}
;; {:stroke-width "#ff0000"
;; :stroke-width 5
;; :x 1500 :y 2000}])
;;
;; (get-attrs-multi shapes [:stroke-color
;; :stroke-width
;; :fill-color
;; :rx
;; :ry])
;; >>> {:stroke-color "#ff0000"
;; :stroke-width :multiple
;; :fill-color "#0000ff"
;; :rx nil
;; :ry nil}
;;
(let [defined-shapes (filter some? shapes)
combine-value (fn [v1 v2] (cond
(= v1 v2) v1
(= v1 :undefined) v2
(= v2 :undefined) v1
:else :multiple))
combine-values (fn [attrs shape values]
(map #(combine-value (get shape % :undefined)
(get values % :undefined)) attrs))
select-attrs (fn [shape attrs]
(zipmap attrs (map #(get shape % :undefined) attrs)))
reducer (fn [result shape]
(zipmap attrs (combine-values attrs shape result)))
combined (reduce reducer
(select-attrs (first defined-shapes) attrs)
(rest defined-shapes))
cleanup-value (fn [value]
(if (= value :undefined) nil value))
cleanup (fn [result]
(zipmap attrs (map #(cleanup-value (get result %)) attrs)))]
(cleanup combined)))

View file

@ -7,12 +7,14 @@
(ns app.common.data
"Data manipulation and query helper functions."
(:refer-clojure :exclude [concat read-string hash-map])
(:require [clojure.set :as set]
[linked.set :as lks]
#?(:cljs [cljs.reader :as r]
:clj [clojure.edn :as r])
#?(:cljs [cljs.core :as core]
:clj [clojure.core :as core]))
(:require
[clojure.set :as set]
[linked.set :as lks]
[app.common.math :as mth]
#?(:cljs [cljs.reader :as r]
:clj [clojure.edn :as r])
#?(:cljs [cljs.core :as core]
:clj [clojure.core :as core]))
#?(:clj
(:import linked.set.LinkedSet)))
@ -261,3 +263,21 @@
(defn coalesce
[val default]
(or val default))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn nilf
"Returns a new function that if you pass nil as any argument will
return nil"
[f]
(fn [& args]
(if (some nil? args)
nil
(apply f args))))
(defn check-num
"Function that checks if a number is nil or nan. Will return 0 when not
valid and the number otherwise."
[v]
(if (or (not v) (mth/nan? v)) 0 v))

View file

@ -0,0 +1,151 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.geom.align
(:require
[clojure.spec.alpha :as s]
[app.common.geom.shapes :as gsh]
[app.common.data :as d]))
;; --- Alignment
(s/def ::align-axis #{:hleft :hcenter :hright :vtop :vcenter :vbottom})
(declare calc-align-pos)
;; Duplicated from pages-helpers to remove cyclic dependencies
(defn- get-children [id objects]
(let [shapes (vec (get-in objects [id :shapes]))]
(if shapes
(d/concat shapes (mapcat #(get-children % objects) shapes))
[])))
(defn- recursive-move
"Move the shape and all its recursive children."
[shape dpoint objects]
(let [children-ids (get-children (:id shape) objects)
children (map #(get objects %) children-ids)]
(map #(gsh/move % dpoint) (cons shape children))))
(defn align-to-rect
"Move the shape so that it is aligned with the given rectangle
in the given axis. Take account the form of the shape and the
possible rotation. What is aligned is the rectangle that wraps
the shape with the given rectangle. If the shape is a group,
move also all of its recursive children."
[shape rect axis objects]
(let [wrapper-rect (gsh/selection-rect [shape])
align-pos (calc-align-pos wrapper-rect rect axis)
delta {:x (- (:x align-pos) (:x wrapper-rect))
:y (- (:y align-pos) (:y wrapper-rect))}]
(recursive-move shape delta objects)))
(defn calc-align-pos
[wrapper-rect rect axis]
(case axis
:hleft (let [left (:x rect)]
{:x left
:y (:y wrapper-rect)})
:hcenter (let [center (+ (:x rect) (/ (:width rect) 2))]
{:x (- center (/ (:width wrapper-rect) 2))
:y (:y wrapper-rect)})
:hright (let [right (+ (:x rect) (:width rect))]
{:x (- right (:width wrapper-rect))
:y (:y wrapper-rect)})
:vtop (let [top (:y rect)]
{:x (:x wrapper-rect)
:y top})
:vcenter (let [center (+ (:y rect) (/ (:height rect) 2))]
{:x (:x wrapper-rect)
:y (- center (/ (:height wrapper-rect) 2))})
:vbottom (let [bottom (+ (:y rect) (:height rect))]
{:x (:x wrapper-rect)
:y (- bottom (:height wrapper-rect))})))
;; --- Distribute
(s/def ::dist-axis #{:horizontal :vertical})
(defn distribute-space
"Distribute equally the space between shapes in the given axis. If
there is no space enough, it does nothing. It takes into account
the form of the shape and the rotation, what is distributed is
the wrapping recangles of the shapes. If any shape is a group,
move also all of its recursive children."
[shapes axis objects]
(let [coord (if (= axis :horizontal) :x :y)
other-coord (if (= axis :horizontal) :y :x)
size (if (= axis :horizontal) :width :height)
; The rectangle that wraps the whole selection
wrapper-rect (gsh/selection-rect shapes)
; Sort shapes by the center point in the given axis
sorted-shapes (sort-by #(coord (gsh/center-shape %)) shapes)
; Each shape wrapped in its own rectangle
wrapped-shapes (map #(gsh/selection-rect [%]) sorted-shapes)
; The total space between shapes
space (reduce - (size wrapper-rect) (map size wrapped-shapes))]
(if (<= space 0)
shapes
(let [unit-space (/ space (- (count wrapped-shapes) 1))
; Calculate the distance we need to move each shape.
; The new position of each one is the position of the
; previous one plus its size plus the unit space.
deltas (loop [shapes' wrapped-shapes
start-pos (coord wrapper-rect)
deltas []]
(let [first-shape (first shapes')
delta (- start-pos (coord first-shape))
new-pos (+ start-pos (size first-shape) unit-space)]
(if (= (count shapes') 1)
(conj deltas delta)
(recur (rest shapes')
new-pos
(conj deltas delta)))))]
(mapcat #(recursive-move %1 {coord %2 other-coord 0} objects)
sorted-shapes deltas)))))
;; Adjusto to viewport
(defn adjust-to-viewport
([viewport srect] (adjust-to-viewport viewport srect nil))
([viewport srect {:keys [padding] :or {padding 0}}]
(let [gprop (/ (:width viewport) (:height viewport))
srect (-> srect
(update :x #(- % padding))
(update :y #(- % padding))
(update :width #(+ % padding padding))
(update :height #(+ % padding padding)))
width (:width srect)
height (:height srect)
lprop (/ width height)]
(cond
(> gprop lprop)
(let [width' (* (/ width lprop) gprop)
padding (/ (- width' width) 2)]
(-> srect
(update :x #(- % padding))
(assoc :width width')))
(< gprop lprop)
(let [height' (/ (* height lprop) gprop)
padding (/ (- height' height) 2)]
(-> srect
(update :y #(- % padding))
(assoc :height height')))
:else srect))))

View file

@ -121,3 +121,13 @@
([m angle-x angle-y p]
(multiply m (skew-matrix angle-x angle-y p))))
(defn m-equal [m1 m2 threshold]
(let [th-eq (fn [a b] (<= (mth/abs (- a b)) threshold))
{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f} m1
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f} m2]
(and (th-eq m1a m2a)
(th-eq m1b m2b)
(th-eq m1c m2c)
(th-eq m1d m2d)
(th-eq m1e m2e)
(th-eq m1f m2f))))

View file

@ -26,6 +26,14 @@
[v]
(instance? Point v))
(defn ^boolean point-like?
[{:keys [x y] :as v}]
(and (map? v)
(not (nil? x))
(not (nil? y))
(number? x)
(number? y)))
(defn point
"Create a Point instance."
([] (Point. 0 0))
@ -37,9 +45,15 @@
(number? v)
(Point. v v)
(point-like? v)
(Point. (:x v) (:y v))
:else
(throw (ex-info "Invalid arguments" {:v v}))))
([x y] (Point. x y)))
([x y]
;;(assert (not (nil? x)))
;;(assert (not (nil? y)))
(Point. x y)))
(defn add
"Returns the addition of the supplied value to both

View file

@ -0,0 +1,62 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.geom.proportions
(:require
[clojure.spec.alpha :as s]
[app.common.spec :as us]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.transforms :as gtr]
[app.common.geom.shapes.rect :as gpr]
[app.common.math :as mth]
[app.common.data :as d]))
;; --- Proportions
(declare assign-proportions-path)
(declare assign-proportions-rect)
(defn assign-proportions
[{:keys [type] :as shape}]
(case type
:path (assign-proportions-path shape)
(assign-proportions-rect shape)))
(defn- assign-proportions-rect
[{:keys [width height] :as shape}]
(assoc shape :proportion (/ width height)))
;; --- Setup Proportions
(declare setup-proportions-const)
(declare setup-proportions-image)
(defn setup-proportions
[shape]
(case (:type shape)
:icon (setup-proportions-image shape)
:image (setup-proportions-image shape)
:text shape
(setup-proportions-const shape)))
(defn setup-proportions-image
[{:keys [metadata] :as shape}]
(let [{:keys [width height]} metadata]
(assoc shape
:proportion (/ width height)
:proportion-lock false)))
(defn setup-proportions-const
[shape]
(assoc shape
:proportion 1
:proportion-lock false))

View file

@ -13,63 +13,24 @@
[app.common.spec :as us]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.transforms :as gtr]
[app.common.geom.shapes.rect :as gpr]
[app.common.geom.shapes.path :as gsp]
[app.common.math :as mth]
[app.common.data :as d]))
(defn- nilf
"Returns a new function that if you pass nil as any argument will
return nil"
[f]
(fn [& args]
(if (some nil? args)
nil
(apply f args))))
;; --- Relative Movement
(declare move-rect)
(declare move-path)
(defn -chk
"Function that checks if a number is nil or nan. Will return 0 when not
valid and the number otherwise."
[v]
(if (or (not v) (mth/nan? v)) 0 v))
(defn move
"Move the shape relativelly to its current
position applying the provided delta."
[shape {dx :x dy :y}]
(let [inc-x (nilf (fn [x] (+ (-chk x) (-chk dx))))
inc-y (nilf (fn [y] (+ (-chk y) (-chk dy))))
inc-point (nilf (fn [p] (-> p
(update :x inc-x)
(update :y inc-y))))]
(let [dx (d/check-num dx)
dy (d/check-num dy)]
(-> shape
(update :x inc-x)
(update :y inc-y)
(update-in [:selrect :x] inc-x)
(update-in [:selrect :x1] inc-x)
(update-in [:selrect :x2] inc-x)
(update-in [:selrect :y] inc-y)
(update-in [:selrect :y1] inc-y)
(update-in [:selrect :y2] inc-y)
(update :points #(mapv inc-point %))
(update :segments #(mapv inc-point %)))))
;; Duplicated from pages-helpers to remove cyclic dependencies
(defn get-children [id objects]
(let [shapes (vec (get-in objects [id :shapes]))]
(if shapes
(d/concat shapes (mapcat #(get-children % objects) shapes))
[])))
(defn recursive-move
"Move the shape and all its recursive children."
[shape dpoint objects]
(let [children-ids (get-children (:id shape) objects)
children (map #(get objects %) children-ids)]
(map #(move % dpoint) (cons shape children))))
(assoc-in [:modifiers :displacement] (gmt/translate-matrix (gpt/point dx dy)))
(gtr/transform-shape))))
;; --- Absolute Movement
@ -77,116 +38,32 @@
(defn absolute-move
"Move the shape to the exactly specified position."
[shape position]
(case (:type shape)
(:curve :path) shape
(absolute-move-rect shape position)))
(defn- absolute-move-rect
"A specialized function for absolute moviment
for rect-like shapes."
[shape {:keys [x y] :as pos}]
(let [dx (if x (- (-chk x) (-chk (:x shape))) 0)
dy (if y (- (-chk y) (-chk (:y shape))) 0)]
[shape {:keys [x y]}]
(let [dx (- (d/check-num x) (-> shape :selrect :x))
dy (- (d/check-num y) (-> shape :selrect :y))]
(move shape (gpt/point dx dy))))
;; --- Center
(declare center-rect)
(declare center-path)
(defn center
"Calculate the center of the shape."
[shape]
(case (:type shape)
:curve (center-path shape)
:path (center-path shape)
(center-rect shape)))
(defn- center-rect
[{:keys [x y width height] :as shape}]
(gpt/point (+ x (/ width 2)) (+ y (/ height 2))))
(defn- center-path
[{:keys [segments] :as shape}]
(let [minx (apply min (map :x segments))
miny (apply min (map :y segments))
maxx (apply max (map :x segments))
maxy (apply max (map :y segments))]
(gpt/point (/ (+ minx maxx) 2) (/ (+ miny maxy) 2))))
(defn center->rect
"Creates a rect given a center and a width and height"
[center width height]
{:x (- (:x center) (/ width 2))
:y (- (:y center) (/ height 2))
:width width
:height height})
;; --- Proportions
(declare assign-proportions-path)
(declare assign-proportions-rect)
(defn assign-proportions
[{:keys [type] :as shape}]
(case type
:path (assign-proportions-path shape)
(assign-proportions-rect shape)))
(defn- assign-proportions-rect
[{:keys [width height] :as shape}]
(assoc shape :proportion (/ width height)))
;; --- Paths
(defn update-path-point
"Update a concrete point in the path.
The point should exists before, this function
does not adds it automatically."
[shape index point]
(assoc-in shape [:segments index] point))
;; --- Setup Proportions
(declare setup-proportions-const)
(declare setup-proportions-image)
(defn setup-proportions
[shape]
(case (:type shape)
:icon (setup-proportions-image shape)
:image (setup-proportions-image shape)
:text shape
(setup-proportions-const shape)))
(defn setup-proportions-image
[{:keys [metadata] :as shape}]
(let [{:keys [width height]} metadata]
(assoc shape
:proportion (/ width height)
:proportion-lock false)))
(defn setup-proportions-const
[shape]
(assoc shape
:proportion 1
:proportion-lock false))
;; --- Resize (Dimensions)
;; Fixme: Improve using modifiers instead of calculating the selrect/points
(defn resize
[shape width height]
(us/assert map? shape)
(us/assert number? width)
(us/assert number? height)
(-> shape
(assoc :width width :height height)
(update :selrect (fn [selrect]
(assoc selrect
:x2 (+ (:x1 selrect) width)
:y2 (+ (:y1 selrect) height))))))
(let [selrect (-> (:selrect shape)
(assoc :width width)
(assoc :height height)
(assoc :x2 (+ (-> shape :selrect :x1) width))
(assoc :y2 (+ (-> shape :selrect :y1) height)))
center (gco/center-selrect selrect)
points (-> selrect gpr/rect->points (gtr/transform-points center (:transform shape)))]
(-> shape
(assoc :width width)
(assoc :height height)
(assoc :selrect selrect)
(assoc :points points))))
(defn resize-rect
[shape attr value]
@ -207,31 +84,21 @@
(resize shape (:width new-size) (:height new-size))))
;; --- Setup (Initialize)
(declare setup-rect)
(declare setup-image)
(defn setup
"A function that initializes the first coordinates for
the shape. Used mainly for draw operations."
[shape props]
(case (:type shape)
:image (setup-image shape props)
(setup-rect shape props)))
(declare shape->points)
(declare points->selrect)
;; FIXME: Is this the correct place for these functions?
(defn- setup-rect
"A specialized function for setup rect-like shapes."
[shape {:keys [x y width height]}]
(as-> shape $
(assoc $ :x x
:y y
:width width
:height height)
(assoc $ :points (shape->points $))
(assoc $ :selrect (points->selrect (:points $)))))
(let [rect {:x x :y y :width width :height height}
points (gpr/rect->points rect)
selrect (gpr/points->selrect points)]
(assoc shape
:x x
:y y
:width width
:height height
:points points
:selrect selrect)))
(defn- setup-image
[{:keys [metadata] :as shape} {:keys [x y width height] :as props}]
@ -241,157 +108,13 @@
(:height metadata))
:proportion-lock true)))
;; --- Coerce to Rect-like shape.
(declare path->rect-shape)
(declare group->rect-shape)
(declare rect->rect-shape)
;; TODO: completly remove
(defn shape->rect-shape
"Coerce shape to rect like shape."
[{:keys [type] :as shape}]
(case type
(:curve :path) (path->rect-shape shape)
(rect->rect-shape shape)))
;; -- Points
(declare transform-shape-point)
(defn shape->points [shape]
(let [points (case (:type shape)
(:curve :path) (:segments shape)
(let [{:keys [x y width height]} shape]
[(gpt/point x y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y height))
(gpt/point x (+ y height))]))]
(->> points
(map #(transform-shape-point % shape (:transform shape (gmt/matrix))))
(map gpt/round)
(vec))))
(defn points->selrect [points]
(let [minx (transduce (map :x) min ##Inf points)
miny (transduce (map :y) min ##Inf points)
maxx (transduce (map :x) max ##-Inf points)
maxy (transduce (map :y) max ##-Inf points)]
{:x1 minx
:y1 miny
:x2 maxx
:y2 maxy
:x minx
:y miny
:width (- maxx minx)
:height (- maxy miny)
:type :rect}))
;; Shape->PATH
(declare rect->path)
(defn shape->path
[shape]
(defn setup
"A function that initializes the first coordinates for
the shape. Used mainly for draw operations."
[shape props]
(case (:type shape)
(:curve :path) shape
(rect->path shape)))
(defn rect->path
[{:keys [x y width height] :as shape}]
(let [points [(gpt/point x y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y height))
(gpt/point x (+ y height))
(gpt/point x y)]]
(-> shape
(assoc :type :path)
(assoc :segments points))))
;; --- SHAPE -> RECT
(defn- rect->rect-shape
[{:keys [x y width height] :as shape}]
(assoc shape
:x1 x
:y1 y
:x2 (+ x width)
:y2 (+ y height)))
(defn- path->rect-shape
[{:keys [segments] :as shape}]
(merge shape
{:type :rect}
(:selrect shape)))
;; --- Resolve Shape
(declare resolve-rect-shape)
(declare translate-from-frame)
(declare translate-to-frame)
(defn resolve-shape
[objects shape]
(case (:type shape)
:rect (resolve-rect-shape objects shape)
:group (resolve-rect-shape objects shape)
:frame (resolve-rect-shape objects shape)))
(defn- resolve-rect-shape
[objects {:keys [parent] :as shape}]
(loop [pobj (get objects parent)]
(if (= :frame (:type pobj))
(translate-from-frame shape pobj)
(recur (get objects (:parent pobj))))))
;; --- Transform Shape
(declare transform-rect)
(declare transform-path)
(defn transform
"Apply the matrix transformation to shape."
[{:keys [type] :as shape} xfmt]
(if (gmt/matrix? xfmt)
(case type
:path (transform-path shape xfmt)
:curve (transform-path shape xfmt)
(transform-rect shape xfmt))
shape))
(defn center-transform [shape matrix]
(let [shape-center (center shape)]
(-> shape
(transform
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply matrix)
(gmt/translate (gpt/negate shape-center)))))))
(defn- transform-rect
[{:keys [x y width height] :as shape} mx]
(let [tl (gpt/transform (gpt/point x y) mx)
tr (gpt/transform (gpt/point (+ x width) y) mx)
bl (gpt/transform (gpt/point x (+ y height)) mx)
br (gpt/transform (gpt/point (+ x width) (+ y height)) mx)
;; TODO: replace apply with transduce (performance)
minx (apply min (map :x [tl tr bl br]))
maxx (apply max (map :x [tl tr bl br]))
miny (apply min (map :y [tl tr bl br]))
maxy (apply max (map :y [tl tr bl br]))]
(assoc shape
:x minx
:y miny
:width (- maxx minx)
:height (- maxy miny))))
(defn- transform-path
[{:keys [segments] :as shape} xfmt]
(let [segments (mapv #(gpt/transform % xfmt) segments)]
(assoc shape :segments segments)))
:image (setup-image shape props)
(setup-rect shape props)))
;; --- Outer Rect
@ -399,24 +122,10 @@
"Returns a rect that contains all the shapes and is aware of the
rotation of each shape. Mainly used for multiple selection."
[shapes]
(let [shapes (map :selrect shapes)
minx (transduce (map :x1) min ##Inf shapes)
miny (transduce (map :y1) min ##Inf shapes)
maxx (transduce (map :x2) max ##-Inf shapes)
maxy (transduce (map :y2) max ##-Inf shapes)]
{:x1 minx
:y1 miny
:x2 maxx
:y2 maxy
:x minx
:y miny
:width (- maxx minx)
:height (- maxy miny)
:points [(gpt/point minx miny)
(gpt/point maxx miny)
(gpt/point maxx maxy)
(gpt/point minx maxy)]
:type :rect}))
(->> shapes
(gtr/transform-shape)
(map (comp gpr/points->selrect :points))
(gpr/join-selrects)))
(defn translate-to-frame
[shape {:keys [x y] :as frame}]
@ -426,117 +135,26 @@
[shape {:keys [x y] :as frame}]
(move shape (gpt/point x y)))
;; --- Alignment
(s/def ::align-axis #{:hleft :hcenter :hright :vtop :vcenter :vbottom})
(declare calc-align-pos)
(defn align-to-rect
"Move the shape so that it is aligned with the given rectangle
in the given axis. Take account the form of the shape and the
possible rotation. What is aligned is the rectangle that wraps
the shape with the given rectangle. If the shape is a group,
move also all of its recursive children."
[shape rect axis objects]
(let [wrapper-rect (selection-rect [shape])
align-pos (calc-align-pos wrapper-rect rect axis)
delta {:x (- (:x align-pos) (:x wrapper-rect))
:y (- (:y align-pos) (:y wrapper-rect))}]
(recursive-move shape delta objects)))
(defn calc-align-pos
[wrapper-rect rect axis]
(case axis
:hleft (let [left (:x rect)]
{:x left
:y (:y wrapper-rect)})
:hcenter (let [center (+ (:x rect) (/ (:width rect) 2))]
{:x (- center (/ (:width wrapper-rect) 2))
:y (:y wrapper-rect)})
:hright (let [right (+ (:x rect) (:width rect))]
{:x (- right (:width wrapper-rect))
:y (:y wrapper-rect)})
:vtop (let [top (:y rect)]
{:x (:x wrapper-rect)
:y top})
:vcenter (let [center (+ (:y rect) (/ (:height rect) 2))]
{:x (:x wrapper-rect)
:y (- center (/ (:height wrapper-rect) 2))})
:vbottom (let [bottom (+ (:y rect) (:height rect))]
{:x (:x wrapper-rect)
:y (- bottom (:height wrapper-rect))})))
;; --- Distribute
(s/def ::dist-axis #{:horizontal :vertical})
(defn distribute-space
"Distribute equally the space between shapes in the given axis. If
there is no space enough, it does nothing. It takes into account
the form of the shape and the rotation, what is distributed is
the wrapping recangles of the shapes. If any shape is a group,
move also all of its recursive children."
[shapes axis objects]
(let [coord (if (= axis :horizontal) :x :y)
other-coord (if (= axis :horizontal) :y :x)
size (if (= axis :horizontal) :width :height)
; The rectangle that wraps the whole selection
wrapper-rect (selection-rect shapes)
; Sort shapes by the center point in the given axis
sorted-shapes (sort-by #(coord (center %)) shapes)
; Each shape wrapped in its own rectangle
wrapped-shapes (map #(selection-rect [%]) sorted-shapes)
; The total space between shapes
space (reduce - (size wrapper-rect) (map size wrapped-shapes))]
(if (<= space 0)
shapes
(let [unit-space (/ space (- (count wrapped-shapes) 1))
; Calculate the distance we need to move each shape.
; The new position of each one is the position of the
; previous one plus its size plus the unit space.
deltas (loop [shapes' wrapped-shapes
start-pos (coord wrapper-rect)
deltas []]
(let [first-shape (first shapes')
delta (- start-pos (coord first-shape))
new-pos (+ start-pos (size first-shape) unit-space)]
(if (= (count shapes') 1)
(conj deltas delta)
(recur (rest shapes')
new-pos
(conj deltas delta)))))]
(mapcat #(recursive-move %1 {coord %2 other-coord 0} objects)
sorted-shapes deltas)))))
;; --- Helpers
(defn contained-in?
"Check if a shape is contained in the
provided selection rect."
[shape selrect]
(let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (shape->rect-shape selrect)
{rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (shape->rect-shape shape)]
(let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} selrect
{rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (:selrect shape)]
(and (neg? (- sy1 ry1))
(neg? (- sx1 rx1))
(pos? (- sy2 ry2))
(pos? (- sx2 rx2)))))
;; TODO: This not will work for rotated shapes
(defn overlaps?
"Check if a shape overlaps with provided selection rect."
[shape selrect]
(let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (shape->rect-shape selrect)
{rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (shape->rect-shape shape)]
[shape rect]
(let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (gpr/rect->selrect rect)
{rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (gpr/points->selrect (:points shape))]
(and (< rx1 sx2)
(> rx2 sx1)
(< ry1 sy2)
@ -564,43 +182,6 @@
:type :rect}]
(overlaps? shape selrect)))
(defn calculate-rec-path-skew-angle
[path-shape]
(let [p1 (get-in path-shape [:segments 2])
p2 (get-in path-shape [:segments 3])
p3 (get-in path-shape [:segments 4])
v1 (gpt/to-vec p1 p2)
v2 (gpt/to-vec p2 p3)]
(- 90 (gpt/angle-with-other v1 v2))))
(defn calculate-rec-path-height
"Calculates the height of a paralelogram given by the path"
[path-shape]
(let [p1 (get-in path-shape [:segments 2])
p2 (get-in path-shape [:segments 3])
p3 (get-in path-shape [:segments 4])
v1 (gpt/to-vec p1 p2)
v2 (gpt/to-vec p2 p3)
angle (gpt/angle-with-other v1 v2)]
(* (gpt/length v2) (mth/sin (mth/radians angle)))))
(defn calculate-rec-path-rotation
[path-shape1 path-shape2 resize-vector]
(let [idx-1 0
idx-2 (cond (and (neg? (:x resize-vector)) (pos? (:y resize-vector))) 1
(and (neg? (:x resize-vector)) (neg? (:y resize-vector))) 2
(and (pos? (:x resize-vector)) (neg? (:y resize-vector))) 3
:else 0)
p1 (get-in path-shape1 [:segments idx-1])
p2 (get-in path-shape2 [:segments idx-2])
v1 (gpt/to-vec (center path-shape1) p1)
v2 (gpt/to-vec (center path-shape2) p2)
rot-angle (gpt/angle-with-other v1 v2)
rot-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)]
(* rot-sign rot-angle)))
(defn pad-selrec
([selrect] (pad-selrec selrect 1))
([selrect size]
@ -658,310 +239,29 @@
(and (>= s1c1 s2c1) (<= s1c1 s2c2))
(and (>= s1c2 s2c1) (<= s1c2 s2c2)))))
(defn transform-shape-point
"Transform a point around the shape center"
[point shape transform]
(let [shape-center (center shape)]
(gpt/transform
point
(-> (gmt/multiply
(gmt/translate-matrix shape-center)
transform
(gmt/translate-matrix (gpt/negate shape-center)))))))
(defn transform-apply-modifiers
[shape]
(let [modifiers (:modifiers shape)
ds-modifier (:displacement modifiers (gmt/matrix))
{res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1))
;; Normalize x/y vector coordinates because scale by 0 is infinite
res-x (cond
(and (< res-x 0) (> res-x -0.01)) -0.01
(and (>= res-x 0) (< res-x 0.01)) 0.01
:else res-x)
res-y (cond
(and (< res-y 0) (> res-y -0.01)) -0.01
(and (>= res-y 0) (< res-y 0.01)) 0.01
:else res-y)
resize (gpt/point res-x res-y)
origin (:resize-origin modifiers (gpt/point 0 0))
resize-transform (:resize-transform modifiers (gmt/matrix))
resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix))
rt-modif (or (:rotation modifiers) 0)
shape (-> shape
(transform ds-modifier))
shape-center (center shape)]
(-> (shape->path shape)
(transform (-> (gmt/matrix)
;; Applies the current resize transformation
(gmt/translate origin)
(gmt/multiply resize-transform)
(gmt/scale resize)
(gmt/multiply resize-transform-inverse)
(gmt/translate (gpt/negate origin))
;; Applies the stacked transformations
(gmt/translate shape-center)
(gmt/multiply (gmt/rotate-matrix rt-modif))
(gmt/multiply (:transform shape (gmt/matrix)))
(gmt/translate (gpt/negate shape-center)))))))
(defn rect-path-dimensions [rect-path]
(let [seg (:segments rect-path)
[width height] (mapv (fn [[c1 c2]] (gpt/distance c1 c2)) (take 2 (d/zip seg (rest seg))))]
{:width width
:height height}))
(defn calculate-stretch [shape-path transform-inverse]
(let [shape-center (center shape-path)
shape-path-temp (transform
shape-path
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply transform-inverse)
(gmt/translate (gpt/negate shape-center))))
shape-path-temp-rec (shape->rect-shape shape-path-temp)
shape-path-temp-dim (rect-path-dimensions shape-path-temp)]
(gpt/divide (gpt/point (:width shape-path-temp-rec) (:height shape-path-temp-rec))
(gpt/point (:width shape-path-temp-dim) (:height shape-path-temp-dim)))))
(defn fix-invalid-rect-values
[rect-shape]
(letfn [(check [num]
(if (or (nil? num) (mth/nan? num) (= ##Inf num) (= ##-Inf num)) 0 num))
(to-positive [num] (if (< num 1) 1 num))]
(-> rect-shape
(update :x check)
(update :y check)
(update :width (comp to-positive check))
(update :height (comp to-positive check)))))
(defn transform-rect-shape
[shape]
(let [;; Apply modifiers to the rect as a path so we have the end shape expected
shape-path (transform-apply-modifiers shape)
shape-center (center shape-path)
resize-vector (-> (get-in shape [:modifiers :resize-vector] (gpt/point 1 1))
(update :x #(if (zero? %) 1 %))
(update :y #(if (zero? %) 1 %)))
;; Reverse the current transformation stack to get the base rectangle
shape-path-temp (center-transform shape-path (:transform-inverse shape (gmt/matrix)))
shape-path-temp-dim (rect-path-dimensions shape-path-temp)
shape-path-temp-rec (shape->rect-shape shape-path-temp)
;; This rectangle is the new data for the current rectangle. We want to change our rectangle
;; to have this width, height, x, y
rec (center->rect shape-center (:width shape-path-temp-dim) (:height shape-path-temp-dim))
rec (fix-invalid-rect-values rec)
rec-path (rect->path rec)
;; The next matrix is a series of transformations we have to do to the previous rec so that
;; after applying them the end result is the `shape-path-temp`
;; This is compose of three transformations: skew, resize and rotation
stretch-matrix (gmt/matrix)
skew-angle (calculate-rec-path-skew-angle shape-path-temp)
;; When one of the axis is flipped we have to reverse the skew
skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
h1 (calculate-rec-path-height shape-path-temp)
h2 (calculate-rec-path-height (center-transform rec-path stretch-matrix))
h3 (/ h1 h2)
h3 (if (mth/nan? h3) 1 h3)
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point 1 h3)))
rotation-angle (calculate-rec-path-rotation (center-transform rec-path stretch-matrix)
shape-path-temp resize-vector)
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
;; This is the inverse to be able to remove the transformation
stretch-matrix-inverse (-> (gmt/matrix)
(gmt/scale (gpt/point 1 h3))
(gmt/skew (- skew-angle) 0)
(gmt/rotate (- rotation-angle)))
new-shape (as-> shape $
(merge $ rec)
(update $ :x #(mth/precision % 0))
(update $ :y #(mth/precision % 0))
(update $ :width #(mth/precision % 0))
(update $ :height #(mth/precision % 0))
(update $ :transform #(gmt/multiply (or % (gmt/matrix)) stretch-matrix))
(update $ :transform-inverse #(gmt/multiply stretch-matrix-inverse (or % (gmt/matrix))))
(assoc $ :points (shape->points $))
(assoc $ :selrect (points->selrect (:points $)))
(update $ :selrect fix-invalid-rect-values)
(update $ :rotation #(mod (+ (or % 0)
(or (get-in $ [:modifiers :rotation]) 0)) 360)))]
new-shape))
(declare update-path-selrect)
(defn transform-path-shape
[shape]
(-> shape
transform-apply-modifiers
update-path-selrect)
;; TODO: Addapt for paths is not working
#_(let [shape-path (transform-apply-modifiers shape)
shape-path-center (center shape-path)
shape-transform-inverse' (-> (gmt/matrix)
(gmt/translate shape-path-center)
(gmt/multiply (:transform-inverse shape (gmt/matrix)))
(gmt/multiply (gmt/rotate-matrix (- (:rotation-modifier shape 0))))
(gmt/translate (gpt/negate shape-path-center)))]
(-> shape-path
(transform shape-transform-inverse')
(add-rotate-transform (:rotation-modifier shape 0)))))
(defn transform-shape
"Transform the shape properties given the modifiers"
([shape] (transform-shape nil shape))
([frame shape]
(let [new-shape
(if (:modifiers shape)
(-> (case (:type shape)
(:curve :path) (transform-path-shape shape)
(transform-rect-shape shape))
(dissoc :modifiers))
shape)]
(cond-> new-shape
frame (translate-to-frame frame)))))
(defn transform-matrix
"Returns a transformation matrix without changing the shape properties.
The result should be used in a `transform` attribute in svg"
([{:keys [x y] :as shape}]
(let [shape-center (center shape)]
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply (:transform shape (gmt/matrix)))
(gmt/translate (gpt/negate shape-center))))))
(defn update-path-selrect [shape]
(as-> shape $
(assoc $ :points (shape->points $))
(assoc $ :selrect (points->selrect (:points $)))
(assoc $ :x (get-in $ [:selrect :x]))
(assoc $ :y (get-in $ [:selrect :y]))
(assoc $ :width (get-in $ [:selrect :width]))
(assoc $ :height (get-in $ [:selrect :height]))))
(defn adjust-to-viewport
([viewport srect] (adjust-to-viewport viewport srect nil))
([viewport srect {:keys [padding] :or {padding 0}}]
(let [gprop (/ (:width viewport) (:height viewport))
srect (-> srect
(update :x #(- % padding))
(update :y #(- % padding))
(update :width #(+ % padding padding))
(update :height #(+ % padding padding)))
width (:width srect)
height (:height srect)
lprop (/ width height)]
(cond
(> gprop lprop)
(let [width' (* (/ width lprop) gprop)
padding (/ (- width' width) 2)]
(-> srect
(update :x #(- % padding))
(assoc :width width')))
(< gprop lprop)
(let [height' (/ (* height lprop) gprop)
padding (/ (- height' height) 2)]
(-> srect
(update :y #(- % padding))
(assoc :height height')))
:else srect))))
(defn get-attrs-multi
[shapes attrs]
;; Extract some attributes of a list of shapes.
;; For each attribute, if the value is the same in all shapes,
;; wll take this value. If there is any shape that is different,
;; the value of the attribute will be the keyword :multiple.
;;
;; If some shape has the value nil in any attribute, it's
;; considered a different value. If the shape does not contain
;; the attribute, it's ignored in the final result.
;;
;; Example:
;; (def shapes [{:stroke-color "#ff0000"
;; :stroke-width 3
;; :fill-color "#0000ff"
;; :x 1000 :y 2000 :rx nil}
;; {:stroke-width "#ff0000"
;; :stroke-width 5
;; :x 1500 :y 2000}])
;;
;; (get-attrs-multi shapes [:stroke-color
;; :stroke-width
;; :fill-color
;; :rx
;; :ry])
;; >>> {:stroke-color "#ff0000"
;; :stroke-width :multiple
;; :fill-color "#0000ff"
;; :rx nil
;; :ry nil}
;;
(let [defined-shapes (filter some? shapes)
combine-value (fn [v1 v2] (cond
(= v1 v2) v1
(= v1 :undefined) v2
(= v2 :undefined) v1
:else :multiple))
combine-values (fn [attrs shape values]
(map #(combine-value (get shape % :undefined)
(get values % :undefined)) attrs))
select-attrs (fn [shape attrs]
(zipmap attrs (map #(get shape % :undefined) attrs)))
reducer (fn [result shape]
(zipmap attrs (combine-values attrs shape result)))
combined (reduce reducer
(select-attrs (first defined-shapes) attrs)
(rest defined-shapes))
cleanup-value (fn [value]
(if (= value :undefined) nil value))
cleanup (fn [result]
(zipmap attrs (map #(cleanup-value (get result %)) attrs)))]
(cleanup combined)))
(defn setup-selrect [{:keys [x y width height] :as shape}]
(-> shape
(assoc :selrect {:x x :y y
:width width :height height
:x1 x :y1 y
:x2 (+ x width) :y2 (+ y height)})))
(let [selrect (gpr/rect->selrect shape)
points (gpr/rect->points shape)]
(-> shape
(assoc :selrect selrect
:points points))))
;; EXPORTS
(defn center-shape [shape] (gco/center-shape shape))
(defn center-selrect [selrect] (gco/center-selrect selrect))
(defn center-rect [rect] (gco/center-rect rect))
(defn rect->selrect [rect] (gpr/rect->selrect rect))
(defn rect->points [rect] (gpr/rect->points rect))
(defn points->selrect [points] (gpr/points->selrect points))
(defn transform-shape [shape] (gtr/transform-shape shape))
(defn transform-matrix [shape] (gtr/transform-matrix shape))
(defn transform-point-center [point center transform] (gtr/transform-point-center point center transform))
(defn transform-rect [rect mtx] (gtr/transform-rect rect mtx))
;; PATHS
(defn content->points [content] (gsp/content->points content))
(defn content->selrect [content] (gsp/content->selrect content))

View file

@ -0,0 +1,52 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.geom.shapes.common
(:require
[clojure.spec.alpha :as s]
[app.common.spec :as us]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.math :as mth]
[app.common.data :as d]))
(defn center-rect
[{:keys [x y width height]}]
(when (and (mth/finite? x)
(mth/finite? y)
(mth/finite? width)
(mth/finite? height))
(gpt/point (+ x (/ width 2))
(+ y (/ height 2)))))
(defn center-selrect
"Calculate the center of the shape."
[selrect]
(center-rect selrect))
(defn center-points [points]
(let [minx (transduce (map :x) min ##Inf points)
miny (transduce (map :y) min ##Inf points)
maxx (transduce (map :x) max ##-Inf points)
maxy (transduce (map :y) max ##-Inf points)]
(gpt/point (/ (+ minx maxx) 2)
(/ (+ miny maxy) 2))))
(defn center-shape
"Calculate the center of the shape."
[shape]
(center-rect (:selrect shape)))
(defn make-centered-rect
"Creates a rect given a center and a width and height"
[center width height]
{:x (- (:x center) (/ width 2))
:y (- (:y center) (/ height 2))
:width width
:height height})

View file

@ -0,0 +1,162 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.geom.shapes.path
(:require
[clojure.spec.alpha :as s]
[app.common.spec :as us]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.rect :as gpr]
[app.common.math :as mth]
[app.common.data :as d]))
(defn content->points [content]
(->> content
(map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y))))
(remove nil?)
(into [])))
;; https://medium.com/@Acegikmo/the-ever-so-lovely-b%C3%A9zier-curve-eb27514da3bf
;; https://en.wikipedia.org/wiki/Bernstein_polynomial
(defn curve-values
"Parametric equation for cubic beziers. Given a start and end and
two intermediate points returns points for values of t.
If you draw t on a plane you got the bezier cube"
[start end h1 h2 t]
(let [t2 (* t t) ;; t square
t3 (* t2 t) ;; t cube
start-v (+ (- t3) (* 3 t2) (* -3 t) 1)
h1-v (+ (* 3 t3) (* -6 t2) (* 3 t))
h2-v (+ (* -3 t3) (* 3 t2))
end-v t3
coord-v (fn [coord]
(+ (* (coord start) start-v)
(* (coord h1) h1-v)
(* (coord h2) h2-v)
(* (coord end) end-v)))]
(gpt/point (coord-v :x) (coord-v :y))))
;; https://pomax.github.io/bezierinfo/#extremities
(defn curve-extremities
"Given a cubic bezier cube finds its roots in t. This are the extremities
if we calculate its values for x, y we can find a bounding box for the curve."
[start end h1 h2]
(let [coords [[(:x start) (:x h1) (:x h2) (:x end)]
[(:y start) (:y h1) (:y h2) (:y end)]]
coord->tvalue
(fn [[c0 c1 c2 c3]]
(let [a (+ (* -3 c0) (* 9 c1) (* -9 c2) (* 3 c3))
b (+ (* 6 c0) (* -12 c1) (* 6 c2))
c (+ (* 3 c1) (* -3 c0))
sqrt-b2-4ac (mth/sqrt (- (* b b) (* 4 a c)))]
(cond
(and (mth/almost-zero? a)
(not (mth/almost-zero? b)))
;; When the term a is close to zero we have a linear equation
[(/ (- c) b)]
;; If a is not close to zero return the two roots for a cuadratic
(not (mth/almost-zero? a))
[(/ (+ (- b) sqrt-b2-4ac)
(* 2 a))
(/ (- (- b) sqrt-b2-4ac)
(* 2 a))]
;; If a and b close to zero we can't find a root for a constant term
:else
[])))]
(->> coords
(mapcat coord->tvalue)
;; Only values in the range [0, 1] are valid
(filter #(and (>= % 0) (<= % 1)))
;; Pass t-values to actual points
(map #(curve-values start end h1 h2 %)))
))
(defn command->point
([command] (command->point command nil))
([{params :params} coord]
(let [prefix (if coord (name coord) "")
xkey (keyword (str prefix "x"))
ykey (keyword (str prefix "y"))
x (get params xkey)
y (get params ykey)]
(gpt/point x y))))
(defn content->selrect [content]
(let [calc-extremities
(fn [command prev]
(case (:command command)
:close-path []
:move-to [(command->point command)]
;; If it's a line we add the beginning point and endpoint
:line-to [(command->point prev)
(command->point command)]
;; We return the bezier extremities
:curve-to (d/concat
[(command->point prev)
(command->point command)]
(curve-extremities (command->point prev)
(command->point command)
(command->point command :c1)
(command->point command :c2)))))
extremities (mapcat calc-extremities
content
(d/concat [nil] content))]
(gpr/points->selrect extremities)))
(defn transform-content [content transform]
(let [set-tr (fn [params px py]
(let [tr-point (-> (gpt/point (get params px) (get params py))
(gpt/transform transform))]
(assoc params
px (:x tr-point)
py (:y tr-point))))
transform-params
(fn [{:keys [x y c1x c1y c2x c2y] :as params}]
(cond-> params
(not (nil? x)) (set-tr :x :y)
(not (nil? c1x)) (set-tr :c1x :c1y)
(not (nil? c2x)) (set-tr :c2x :c2y)))]
(mapv #(update % :params transform-params) content)))
(defn segments->content
([segments]
(segments->content segments false))
([segments closed?]
(let [initial (first segments)
lines (rest segments)]
(d/concat [{:command :move-to
:params (select-keys initial [:x :y])}]
(->> lines
(mapv #(hash-map :command :line-to
:params (select-keys % [:x :y]))))
(when closed?
[{:command :close-path}])))))

View file

@ -0,0 +1,60 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.geom.shapes.rect
(:require
[clojure.spec.alpha :as s]
[app.common.spec :as us]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.math :as mth]
[app.common.data :as d]))
(defn rect->points [{:keys [x y width height]}]
[(gpt/point x y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y height))
(gpt/point x (+ y height))])
(defn points->rect [points]
(let [minx (transduce (comp (map :x) (remove nil?)) min ##Inf points)
miny (transduce (comp (map :y) (remove nil?)) min ##Inf points)
maxx (transduce (comp (map :x) (remove nil?)) max ##-Inf points)
maxy (transduce (comp (map :y) (remove nil?)) max ##-Inf points)]
{:x minx
:y miny
:width (- maxx minx)
:height (- maxy miny)}))
(defn points->selrect [points]
(let [{:keys [x y width height] :as rect} (points->rect points)]
(assoc rect
:x1 x
:x2 (+ x width)
:y1 y
:y2 (+ y height))))
(defn rect->selrect [rect]
(-> rect rect->points points->selrect))
(defn join-selrects [selrects]
(let [minx (transduce (comp (map :x1) (remove nil?)) min ##Inf selrects)
miny (transduce (comp (map :y1) (remove nil?)) min ##Inf selrects)
maxx (transduce (comp (map :x2) (remove nil?)) max ##-Inf selrects)
maxy (transduce (comp (map :y2) (remove nil?)) max ##-Inf selrects)]
{:x minx
:y miny
:x1 minx
:y1 miny
:x2 maxx
:y2 maxy
:width (- maxx minx)
:height (- maxy miny)}))

View file

@ -0,0 +1,263 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.geom.shapes.transforms
(:require
[clojure.spec.alpha :as s]
[app.common.spec :as us]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.path :as gpa]
[app.common.geom.shapes.rect :as gpr]
[app.common.math :as mth]
[app.common.data :as d]))
(defn transform-matrix
"Returns a transformation matrix without changing the shape properties.
The result should be used in a `transform` attribute in svg"
([{:keys [x y] :as shape}]
(let [shape-center (or (gco/center-shape shape)
(gpt/point 0 0))]
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply (:transform shape (gmt/matrix)))
(gmt/translate (gpt/negate shape-center))))))
(defn transform-point-center
"Transform a point around the shape center"
[point center matrix]
(gpt/transform
point
(gmt/multiply (gmt/translate-matrix center)
matrix
(gmt/translate-matrix (gpt/negate center)))))
(defn transform-points
([points matrix]
(transform-points points nil matrix))
([points center matrix]
(let [prev (if center (gmt/translate-matrix center) (gmt/matrix))
post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix))
tr-point (fn [point]
(gpt/transform point (gmt/multiply prev matrix post)))]
(mapv tr-point points))))
(defn transform-rect
"Transform a rectangles and changes its attributes"
[{:keys [x y width height] :as rect} matrix]
(let [points (-> (gpr/rect->points rect)
(transform-points matrix))]
(gpr/points->rect points)))
(defn normalize-scale
"We normalize the scale so it's not too close to 0"
[scale]
(cond
(and (< scale 0) (> scale -0.01)) -0.01
(and (>= scale 0) (< scale 0.01)) 0.01
:else scale))
(defn modifiers->transform
([center modifiers]
(modifiers->transform (gmt/matrix) center modifiers))
([current-transform center modifiers]
(let [ds-modifier (:displacement modifiers (gmt/matrix))
{res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1))
;; Normalize x/y vector coordinates because scale by 0 is infinite
res-x (normalize-scale res-x)
res-y (normalize-scale res-y)
resize (gpt/point res-x res-y)
origin (:resize-origin modifiers (gpt/point 0 0))
resize-transform (:resize-transform modifiers (gmt/matrix))
resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix))
rt-modif (or (:rotation modifiers) 0)
center (gpt/transform center ds-modifier)
transform (-> (gmt/matrix)
;; Applies the current resize transformation
(gmt/translate origin)
(gmt/multiply resize-transform)
(gmt/scale resize)
(gmt/multiply resize-transform-inverse)
(gmt/translate (gpt/negate origin))
;; Applies the stacked transformations
(gmt/translate center)
(gmt/multiply (gmt/rotate-matrix rt-modif))
(gmt/translate (gpt/negate center))
;; Displacement
(gmt/multiply ds-modifier))]
transform)))
(defn- calculate-skew-angle
"Calculates the skew angle of the paralelogram given by the points"
[[p1 p2 p3 p4]]
(let [v1 (gpt/to-vec p3 p4)
v2 (gpt/to-vec p4 p1)]
(- 90 (gpt/angle-with-other v1 v2))))
(defn- calculate-height
"Calculates the height of a paralelogram given by the points"
[[p1 p2 p3 p4]]
(let [v1 (gpt/to-vec p3 p4)
v2 (gpt/to-vec p4 p1)
angle (gpt/angle-with-other v1 v2)]
(* (gpt/length v2) (mth/sin (mth/radians angle)))))
(defn- calculate-rotation
"Calculates the rotation between two shapes given the resize vector direction"
[points-shape1 points-shape2 flip-x flip-y]
(let [idx-1 0
idx-2 (cond (and flip-x (not flip-y)) 1
(and flip-x flip-y) 2
(and (not flip-x) flip-y) 3
:else 0)
p1 (nth points-shape1 idx-1)
p2 (nth points-shape2 idx-2)
v1 (gpt/to-vec (gco/center-points points-shape1) p1)
v2 (gpt/to-vec (gco/center-points points-shape2) p2)
rot-angle (gpt/angle-with-other v1 v2)
rot-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)]
(* rot-sign rot-angle)))
(defn- calculate-dimensions
[[p1 p2 p3 p4]]
(let [width (gpt/distance p1 p2)
height (gpt/distance p2 p3)]
{:width width :height height}))
(defn calculate-adjust-matrix
"Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that
after applying them the end result is the `shape-pathn-temp`.
This is compose of three transformations: skew, resize and rotation"
[points-temp points-rec flip-x flip-y]
(let [center (gco/center-points points-temp)
stretch-matrix (gmt/matrix)
skew-angle (calculate-skew-angle points-temp)
;; When one of the axis is flipped we have to reverse the skew
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
skew-angle (if (and (or flip-x flip-y)
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
h1 (calculate-height points-temp)
h2 (calculate-height (transform-points points-rec center stretch-matrix))
h3 (/ h1 h2)
h3 (if (mth/nan? h3) 1 h3)
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point 1 h3)))
rotation-angle (calculate-rotation
(transform-points points-rec (gco/center-points points-rec) stretch-matrix)
points-temp
flip-x
flip-y)
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
;; This is the inverse to be able to remove the transformation
stretch-matrix-inverse (-> (gmt/matrix)
(gmt/scale (gpt/point 1 (/ 1 h3)))
(gmt/skew (- skew-angle) 0)
(gmt/rotate (- rotation-angle)))]
[stretch-matrix stretch-matrix-inverse]))
(defn apply-transform-path
[shape transform]
(let [content (gpa/transform-content (:content shape) transform)
selrect (gpa/content->selrect content)
points (gpr/rect->points selrect)
rotation (mod (+ (:rotation shape 0)
(or (get-in shape [:modifiers :rotation]) 0))
360)]
(assoc shape
:content content
:points points
:selrect selrect)))
(defn apply-transform-rect
"Given a new set of points transformed, set up the rectangle so it keeps
its properties. We adjust de x,y,width,height and create a custom transform"
[shape transform]
;;
(let [points (-> shape :points (transform-points transform))
center (gco/center-points points)
;; Reverse the current transformation stack to get the base rectangle
tr-inverse (:transform-inverse shape (gmt/matrix))
modifiers (:modifiers shape)
points-temp (transform-points points center tr-inverse)
points-temp-dim (calculate-dimensions points-temp)
;; This rectangle is the new data for the current rectangle. We want to change our rectangle
;; to have this width, height, x, y
rect-shape (gco/make-centered-rect center
(:width points-temp-dim)
(:height points-temp-dim))
rect-points (gpr/rect->points rect-shape)
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))]
(as-> shape $
(merge $ rect-shape)
(update $ :x #(mth/precision % 0))
(update $ :y #(mth/precision % 0))
(update $ :width #(mth/precision % 0))
(update $ :height #(mth/precision % 0))
(update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix))
(update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix))))
(assoc $ :points (into [] points))
(assoc $ :selrect (gpr/rect->selrect rect-shape))
(update $ :rotation #(mod (+ (or % 0)
(or (get-in $ [:modifiers :rotation]) 0)) 360)))))
(defn apply-transform [shape transform]
(let [apply-transform-fn
(case (:type shape)
:path apply-transform-path
apply-transform-rect)]
(apply-transform-fn shape transform)))
(defn set-flip [shape modifiers]
(let [rx (get-in modifiers [:resize-vector :x])
ry (get-in modifiers [:resize-vector :y])]
(cond-> shape
(and rx (< rx 0)) (update :flip-x not)
(and ry (< ry 0)) (update :flip-y not))))
(defn transform-shape [shape]
(let [center (gco/center-shape shape)]
(if (and (:modifiers shape) center)
(let [transform (modifiers->transform (:transform shape (gmt/matrix)) center (:modifiers shape))]
(-> shape
(set-flip (:modifiers shape))
(apply-transform transform)
(dissoc :modifiers)))
shape)))

View file

@ -23,8 +23,8 @@
(defn finite?
[v]
#?(:cljs (js/isFinite v)
:clj (Double/isFinite v)))
#?(:cljs (and (not (nil? v)) (js/isFinite v))
:clj (and (not (nil? v)) (Double/isFinite v))))
(defn abs
[v]
@ -135,3 +135,6 @@
(if (< num from)
from
(if (> num to) to num)))
(defn almost-zero? [num]
(< (abs num) 1e-8))

View file

@ -20,7 +20,7 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]))
(def file-version 2)
(def file-version 3)
(def max-safe-int 9007199254740991)
(def min-safe-int -9007199254740991)
@ -273,7 +273,9 @@
(s/every uuid? :kind vector?))
(s/def ::shape-attrs
(s/keys :opt-un [:internal.shape/blocked
(s/keys :req-un [:internal.shape/selrect
:internal.shape/points]
:opt-un [:internal.shape/blocked
:internal.shape/collapsed
:internal.shape/content
:internal.shape/fill-color
@ -309,8 +311,6 @@
:internal.shape/width
:internal.shape/height
:internal.shape/interactions
:internal.shape/selrect
:internal.shape/points
:internal.shape/masked-group?
:internal.shape/shadow
:internal.shape/blur]))
@ -611,8 +611,7 @@
:stroke-alignment :center
:stroke-width 2
:stroke-color "#000000"
:stroke-opacity 1
:segments []}
:stroke-opacity 1}
{:type :frame
:name "Artboard"
@ -624,44 +623,37 @@
:stroke-color "#000000"
:stroke-opacity 0}
{:type :curve
:name "Path"
:fill-color "#000000"
:fill-opacity 0
:stroke-style :solid
:stroke-alignment :center
:stroke-width 2
:stroke-color "#000000"
:stroke-opacity 1
:segments []}
{:type :text
:name "Text"
:content nil}])
(defn make-minimal-shape
[type]
(let [shape (d/seek #(= type (:type %)) minimal-shapes)]
(let [type (cond (= type :curve) :path
:else type)
shape (d/seek #(= type (:type %)) minimal-shapes)]
(when-not shape
(ex/raise :type :assertion
:code :shape-type-not-implemented
:context {:type type}))
(assoc shape
:id (uuid/next)
:x 0
:y 0
:width 1
:height 1
:selrect {:x 0
:x1 0
:x2 1
:y 0
:y1 0
:y2 1
:width 1
:height 1}
:points []
:segments [])))
(cond-> shape
:always
(assoc :id (uuid/next))
(not= :path (:type shape))
(assoc :x 0
:y 0
:width 1
:height 1
:selrect {:x 0
:y 0
:x1 0
:y1 0
:x2 1
:y2 1
:width 1
:height 1}))))
(defn make-minimal-group
[frame-id selection-rect group-name]
@ -764,13 +756,14 @@
(defn rotation-modifiers
[center shape angle]
(let [displacement (let [shape-center (geom/center shape)]
(let [displacement (let [shape-center (geom/center-shape shape)]
(-> (gmt/matrix)
(gmt/rotate angle center)
(gmt/rotate (- angle) shape-center)))]
{:rotation angle
:displacement displacement}))
;; reg-objects operation "regenerates" the values for the parent groups
(defmethod process-change :reg-objects
[data {:keys [page-id shapes]}]
(letfn [(reg-objects [objects]
@ -783,7 +776,7 @@
(distinct))
shapes)))
(update-group [group objects]
(let [gcenter (geom/center group)
(let [gcenter (geom/center-shape group)
gxfm (comp
(map #(get objects %))
(map #(-> %
@ -798,10 +791,10 @@
;; Rotate the group shape change the data and rotate back again
(-> group
(assoc-in [:modifiers :rotation] (- (:rotation group 0)))
(geom/transform-shape)
(assoc :selrect selrect)
(assoc :points (geom/rect->points selrect))
(merge (select-keys selrect [:x :y :width :height]))
(assoc-in [:modifiers :rotation] (:rotation group))
(assoc-in [:modifiers :rotation] (:rotation group 0))
(geom/transform-shape))))]
(d/update-in-when data [:pages-index page-id :objects] reg-objects)))

View file

@ -2,6 +2,7 @@
(:require
[app.common.pages :as cp]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gsp]
[app.common.geom.point :as gpt]
[app.common.geom.matrix :as gmt]
[app.common.spec :as us]
@ -35,7 +36,6 @@
;; -- MIGRATIONS --
;; Ensure that all :shape attributes on shapes are vectors.
(defmethod migrate 2
[data]
(letfn [(update-object [id object]
@ -49,3 +49,63 @@
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))
;; Changes paths formats
(defmethod migrate 3
[data]
(letfn [(migrate-path [shape]
(if-not (contains? shape :content)
(let [content (gsp/segments->content (:segments shape) (:close? shape))
selrect (gsh/content->selrect content)
points (gsh/rect->points selrect)]
(-> shape
(dissoc :segments)
(dissoc :close?)
(assoc :content content)
(assoc :selrect selrect)
(assoc :points points)))
;; If the shape contains :content is already in the new format
shape))
(fix-frames-selrects [frame]
(if (= (:id frame) uuid/zero)
frame
(let [frame-rect (select-keys frame [:x :y :width :height])]
(-> frame
(assoc :selrect (gsh/rect->selrect frame-rect))
(assoc :points (gsh/rect->points frame-rect))))))
(fix-empty-points [shape]
(let [shape (cond-> shape
(empty? (:selrect shape)) (gsh/setup-selrect))]
(cond-> shape
(empty? (:points shape))
(assoc :points (gsh/rect->points (:selrect shape))))))
(update-object [id object]
(cond-> object
(= :curve (:type object))
(assoc :type :path)
(or (#{:curve :path} (:type object)))
(migrate-path)
(= :frame (:type object))
(fix-frames-selrects)
(and (empty? (:points object)) (not= (:id object) uuid/zero))
(fix-empty-points)
:always
(->
;; Setup an empty transformation to re-calculate selrects
;; and points data
(assoc :modifiers {:displacement (gmt/matrix)})
(gsh/transform-shape))
))
(update-page [id page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))

View file

@ -0,0 +1,4 @@
<svg width="24px" height="24px" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="m6.9 12.519-2.5349 2.0222-0.17039-2.8021c-0.94178-0.55996-2.933-2.2948-3.3621-4.7515-0.42907-2.4551 2.6642-4.7755 4.2636-5.6282 3.7036-0.49567 10.862-0.12159 9.8685 5.3362-0.99444 5.4578-5.7908 6.1564-8.0655 5.8234z" fill="#fff"/>
<path d="m7.8642 0.38698c-2.26 0.027882-4.5687 0.88679-6.1014 2.586-1.0146 1.1052-1.6148 2.6201-1.4816 4.1296 0.10533 1.7488 1.1292 3.3396 2.4939 4.3929 0.25093 0.19827 0.51271 0.3826 0.77991 0.55841 0.029431 0.98902-0.026333 1.9858 0.072802 2.9694 0.16342 0.39731 0.73731 0.49567 1.0432 0.20369 0.81244-0.66993 1.6334-1.3306 2.4466-2.0013 0.76055 0.07745 1.5319 0.03486 2.2878-0.06815 2.1283-0.3462 4.1807-1.4994 5.3749-3.3257 0.81089-1.2214 1.1772-2.7618 0.85581-4.2055-0.31212-1.4847-1.2764-2.7711-2.4962-3.6447-1.5164-1.1036-3.4101-1.6303-5.2758-1.5947zm0.17426 1.3089c1.8696 0.00697 3.8027 0.65677 5.1217 2.0307 0.80392 0.82251 1.3282 1.9533 1.2686 3.1072-0.02478 1.3089-0.72647 2.5364-1.6961 3.3892-1.3848 1.2314-3.2792 1.8216-5.1101 1.7209-0.31599 0.01084-0.63973-0.08829-0.94642-0.04957-0.59868 0.48715-1.1966 0.97431-1.7937 1.4622-0.01549-0.6893-0.012392-1.3794-0.020137-2.0694-1.3677-0.68929-2.6093-1.8046-3.0871-3.2939-0.41203-1.2547-0.17503-2.7068 0.64515-3.7563 1.2911-1.7372 3.51-2.5519 5.6181-2.5403z" color="#000000"/>
</svg>

After

(image error) Size: 1.4 KiB

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16.621 21.506">
<path fill="#fff" d="M.719.719l.5 1.8v.1a45.985 45.985 0 011 4.7c.2 1.7.9 3.2 1.9 4.2 1 1.1 2.4 1.7 4.1 1.7.7 0-.001-.101.699-.301l3 3 4-4.1-3-3 .3-.6c0-1.7-.6-3.099-1.8-4.099-1-1.1-2.5-1.7-4.2-1.9l-3.4-.6c-.438-.116-.87-.25-1.298-.4L.719.719zm3.629 13.808a3.49 3.49 0 000 6.979 3.49 3.49 0 000-6.979zm0 1.866a1.624 1.624 0 11.002 3.248 1.624 1.624 0 01-.002-3.248z" color="#000" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1"/>
<path fill="#000" fill-rule="evenodd" d="M.719.719l.5 1.8v.1a45.985 45.985 0 011 4.7c.2 1.7.9 3.2 1.9 4.2 1 1.1 2.4 1.7 4.1 1.7.333 0 .639-.028.937-.063l2.762 2.762 4-4.1-2.729-2.728c.018-.28.03-.562.03-.871 0-1.7-.601-3.1-1.801-4.1-1-1.1-2.5-1.7-4.2-1.9l-3.4-.6c-.438-.116-.87-.25-1.298-.4L.719.719zm2.537 1.736c1.31.306 2.63.573 3.963.764 3 .4 5 2.1 5 5l-.1 1.3.1.1.699.7 1.602 1.599-2.602 2.602-1.6-1.602-.004-.004-.695-.695-.047.047-.052-.047H8.219c-2.9 0-4.6-2-5-5-.2-1.7-.5-2.999-.7-3.899L6.33 7.13c-.34.884.12 2.089 1.389 2.089 2 0 2-3 0-3-.224 0-.419.04-.592.107l-3.871-3.87zM4.348 14.95a3.067 3.067 0 100 6.135 3.067 3.067 0 100-6.135zm0 1.024a2.045 2.045 0 110 4.09 2.045 2.045 0 010-4.09z" clip-rule="evenodd" color="#000" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1"/>
</svg>

After

(image error) Size: 2.4 KiB

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 26" width="14.516" height="26.17">
<path d="M1 13.6V2.4l8.2 8.2H4z" fill="#fff"/>
<path d="M0 16V0l11.6 11.6H4.4zm4-5.4h5.2L1 2.4v11.2z" clip-rule="evenodd" fill="#231f20" fill-rule="evenodd"/>
<path d="M8.937 14.17l-2.842 2.842 1.263 1.263.526-.526v1.263h-.947l.526-.527L6.2 17.222l-2.842 2.842L6.2 22.907l1.263-1.264-.526-.526h.947v1.474l-.526-.632-1.263 1.369 2.842 2.842 2.842-2.842-1.263-1.264-.527.527v-1.474h.948l-.527.526 1.264 1.264 2.842-2.843-2.842-2.842-1.264 1.263.527.527h-.948v-1.263l.527.526 1.263-1.263z" fill="#fff"/>
<path d="M8.937 14.907l-2.105 2.105.526.526 1.052-1.053v3.053H5.674l1.052-1.053-.526-.526-2.105 2.105L6.2 22.17l.526-.527-1.052-1.052H8.41v3.263L7.358 22.8l-.526.527 2.105 2.105 2.105-2.105-.526-.527-1.053 1.053V20.59H12.2l-1.053 1.052.527.527 2.105-2.106-2.105-2.105-.527.526 1.053 1.053H9.463v-3.053l1.053 1.053.526-.526z" fill="#000"/>
</svg>

After

(image error) Size: 946 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12.632" height="21.293" fill="none"><path d="M1 13.6V2.4l8.2 8.2H4z" fill="#fff"/><path d="M0 16V0l11.6 11.6H4.4zm4-5.4h5.2L1 2.4v11.2z" clip-rule="evenodd" fill="#231f20" fill-rule="evenodd"/><path d="M9.143 19.429a1.624 1.624 0 100-3.248 1.624 1.624 0 000 3.248zm3.489-1.624a3.489 3.489 0 11-6.978 0 3.489 3.489 0 016.978 0z" clip-rule="evenodd" fill="#fff" fill-rule="evenodd"/><path d="M12.21 17.805a3.068 3.068 0 11-6.135 0 3.068 3.068 0 016.135 0zm-1.022 0a2.045 2.045 0 11-4.09 0 2.045 2.045 0 014.09 0z" clip-rule="evenodd" fill="#000" fill-rule="evenodd"/></svg>

After

(image error) Size: 618 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M369.205 7.23c-17.208.104-47.593 2.472-64.201 5.287-20.773 3.52-52.639 11.881-69.555 18.25l-14.548 5.476-7.122-5.436c-30.798-23.51-75.188-27.152-111.754-9.173-39.687 19.513-61.526 54.611-61.526 98.88 0 18.37 2.388 29.96 9.19 44.577l4.4 9.453-7.692 12.354c-16.232 26.07-30.749 58.736-37.94 85.375-4.02 14.893-8.44 39.23-8.45 46.528L0 323.085h17.745c19.1 0 19.638-.241 19.638-8.762 0-11.84 10.285-47.998 19.87-69.847 5.487-12.511 20.226-39.771 21.502-39.771.372 0 4.006 2.368 8.077 5.266 33.324 23.72 76.736 26.357 114.547 6.957 18.167-9.32 36.82-28.2 45.929-46.491 9.176-18.427 11.88-29.97 11.852-50.623-.024-18.36-2.286-29.476-9.157-45.013l-3.925-8.875 5.188-2.169c8.812-3.682 39.855-11.478 59.482-14.94 11.955-2.11 28.655-3.6 46.34-4.135 15.206-.46 27.713-.914 27.793-1.013.08-.099.979-8.028 1.996-17.617l1.85-17.436L378.68 7.54c-2.236-.239-5.506-.334-9.477-.31zM153.993 47.255c5.439.115 10.535.69 14.48 1.752 24.16 6.506 43.998 25.871 51.372 50.149 2.71 8.92 3.163 13.509 2.531 25.582-.695 13.29-1.381 15.947-7.112 27.478-10.13 20.38-27.473 34.334-48.661 39.153-5.872 1.336-14.045 2.37-18.16 2.297-32.296-.571-61.53-23.72-69.734-55.217-9.66-37.085 12.4-76.769 48.947-88.057 7.251-2.24 17.272-3.328 26.337-3.137zm194.549 141.863c-4.948 0-7.23 1.055-11.473 5.297l-5.293 5.296v123.363l-58.022.016c-34.96.005-60.316.647-63.793 1.612-12.302 3.417-16.2 17.153-7.6 26.781l4.546 5.09 62.043.779 62.047.779.779 61.79.779 61.792 3.894 4.19c2.141 2.303 5.296 4.766 7.009 5.475 1.713.709 3.662 1.333 4.332 1.388 2.784.225 11.463-4.39 14.36-7.637 2.987-3.348 3.134-6.05 3.586-65.636l.472-62.141h123.199l5.296-5.297c4.242-4.242 5.297-6.525 5.297-11.472 0-7.85-4.523-13.764-12.145-15.88-3.477-.966-28.829-1.613-63.79-1.613l-58.021-.015-.01-58.022c-.004-34.961-.646-60.313-1.612-63.79-2.117-7.622-8.03-12.145-15.88-12.145z"/>
</svg>

After

(image error) Size: 1.8 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M240.326 76.886c-7.638-.006-15.27.99-21.109 2.996-22.013 7.56-39.34 26.373-44.518 48.333-4.5 19.09-.083 40.81 11.353 55.816l3.04 3.988-43.821 54.644c-34.23 42.682-44.261 54.454-45.828 53.78-24.235-10.42-43.288-10.479-63.935-.196-14.994 7.466-28.572 24.15-33.606 41.289-2.234 7.604-2.571 25.97-.633 34.384 5.389 23.385 27.168 44.357 51.567 49.65 22.91 4.972 45.72-2.198 62.206-19.553 13.156-13.85 18.529-27.186 18.555-46.053.017-12.292-2.3-22.187-7.466-31.88l-3.726-6.988 44.836-55.895 44.837-55.897 7.058 2.59c5.629 2.063 9.92 2.594 21.163 2.62 12.469.028 15.087-.353 22.59-3.289 4.669-1.826 9.252-3.954 10.187-4.73 1.33-1.104 2.311-.766 4.517 1.557 2.064 2.173 81.858 93.81 97.285 111.724 1.585 1.84 1.306 3.028-2.505 10.647-17.899 35.777.792 80.37 39.04 93.145 16.958 5.664 33.836 4.499 50.65-3.499 36.251-17.243 49.065-62.73 27.251-96.724-5.382-8.387-17.147-19.038-25.927-23.47-18.635-9.407-42.082-9.637-59.938-.589l-5.64 2.858-42.274-48.494c-23.25-26.672-46.063-52.846-50.7-58.166l-8.43-9.673 3.884-7.892c7-14.22 8.867-28.714 5.65-43.896-4.87-22.983-22.14-42.445-44.467-50.11-5.866-2.014-13.509-3.022-21.147-3.027zm.929 28.928c5.153.09 10.157 1.013 14.05 2.795 14.35 6.57 21.879 17.418 22.734 32.755.893 16.027-6.018 28.847-19.518 36.202-5.483 2.988-8.298 3.655-16.996 4.022-9.491.4-11.086.119-17.831-3.14-28.557-13.797-29.16-54.64-1.018-68.915 5.08-2.577 11.953-3.836 18.579-3.72zM431.067 317.35c3.412-.105 6.882.28 10.354 1.169 12.355 3.165 20.235 9.339 25.942 20.327 2.416 4.65 2.867 7.35 2.867 17.168 0 10.626-.32 12.227-3.643 18.227-4.347 7.847-11.71 14.195-20.215 17.427-3.837 1.458-9.14 2.336-13.865 2.297-21.382-.179-37.694-16.329-37.694-37.316 0-11.152 2.56-18.641 8.797-25.726 7.532-8.557 17.223-13.258 27.457-13.573zm-364.48.122c15.176.033 28.573 8.515 35.241 22.31 3.905 8.078 4.064 22.996.333 31.096-3.607 7.831-10.761 15.386-18.138 19.154-6.632 3.387-21.483 4.956-27.668 2.922-33.797-11.112-38.072-55.336-6.889-71.243 7.245-3.695 9.505-4.256 17.12-4.239z"/>
</svg>

After

(image error) Size: 2 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M244.577 79.225a61.421 61.421 0 00-15.436 1.605c-21.988 5.173-41.986 23.143-48.381 43.474l-2.134 6.78H74.925v-20.543H0V185.466H74.925v-25.378h31.66c17.415 0 31.146.455 30.514 1.012-.631.558-4.41 3.48-8.398 6.496-10.714 8.103-26.93 25.517-34.648 37.205-15.054 22.799-25.433 49.97-30.521 79.91-.689 4.05-1.132 4.442-5.856 5.197-13.48 2.154-31.265 14.57-40.243 28.097-7.4 11.147-10.57 21.889-10.638 36.041-.09 18.774 6.174 34.098 19.329 47.293 8.576 8.603 23.71 16.734 34.3 18.43 22.872 3.661 42.976-2.674 58.645-18.482 13.554-13.673 19.846-28.637 19.886-47.29.054-26.118-17.983-52.66-41.525-61.103-2.576-.924-4.944-2.356-5.261-3.182-.785-2.046 2.847-17.999 6.989-30.702 12.624-38.72 37.846-68.227 71.689-83.864 4.98-2.3 9.596-4.182 10.257-4.182.662 0 2.466 2.568 4.008 5.71 4.408 8.979 17.8 22.3 27.592 27.445 9.31 4.892 21.229 7.888 31.406 7.893 23.718.01 48.035-15.184 58.641-36.644 2.511-5.08 5.162-9.234 5.891-9.228.73.006 7.094 2.368 14.14 5.247 36.538 14.927 63.7 43.54 79.59 83.841 4.193 10.636 11.574 35.522 10.635 35.863-.174.063-4.469 2.174-9.543 4.692-15.007 7.447-27.018 20.919-32.555 36.511-3.566 10.043-4.46 27.758-1.905 37.734 8.049 31.416 38.098 53.27 69.477 50.531 32.832-2.865 57.406-27.371 61.24-61.074 1.837-16.142-5.55-36.943-17.82-50.187-9.88-10.664-21.227-16.86-37.328-20.386-2.781-.609-3.341-1.606-4.647-8.263-2.366-12.061-10.426-35.322-17.03-49.146-11.797-24.697-30.196-48.268-49.313-63.175-4.413-3.441-8.54-6.703-9.172-7.249-.631-.545 10.925-.991 25.68-.991h26.83v25.378h76.132V110.54H416.92v20.544H308.54l-1.536-5.136c-8.14-27.222-34.67-46.113-62.427-46.724zm.248 28.594c20.076.653 38.59 17.524 36.445 41.178-1.165 12.848-9.105 24.698-20.494 30.592-6.773 3.504-23.247 4.32-30.837 1.529-7.329-2.695-16.54-11.483-20.483-19.54-2.762-5.646-3.354-8.461-3.354-15.956 0-12.011 3.38-19.722 12.17-27.755 7.978-7.291 17.427-10.344 26.553-10.048zM73.66 316.54c22.939.427 41.895 22.783 35.85 46.377-3.136 12.24-9.355 20.058-20.549 25.836-6.296 3.25-22.152 4.16-28.694 1.647-35.199-13.525-32.897-63.035 3.378-72.64a36.407 36.407 0 0110.015-1.22zm359.468.18c7.514-.066 9.858.475 16.756 3.87 14.635 7.205 22.116 19.953 21.124 36-.883 14.288-8.57 25.912-21.226 32.104-6.893 3.372-21.443 4.308-28.172 1.81-11.325-4.203-20.124-12.775-23.648-23.034-8.6-25.039 9.058-50.521 35.166-50.75zM561.389 720.95c-6.416-.046-13.085.744-17.701 2.318-17.355 5.916-31.094 19.83-36.004 36.462l-1.254 4.25h-78.684v23h78.684l1.271 4.25c5.172 17.284 20.647 32.463 38.045 37.317 6.08 1.696 22.323 1.724 28.614.049 18.293-4.871 34.106-20.609 38.933-38.746 1.939-7.284 1.856-22.146-.16-28.938-5.54-18.663-19.656-32.778-38.318-38.318-3.599-1.069-8.436-1.61-13.426-1.645zm164.267.081c-5.306.012-10.518.528-13.978 1.545-20.853 6.13-35.946 22.898-39.584 43.979-2.39 13.854.716 28.39 8.478 39.673 7.049 10.245 20.848 20.071 31.793 22.641 6.605 1.55 22.482 1.27 28.45-.502 17.81-5.287 32.264-19.166 37.634-36.137l1.663-5.25h78.634v-23H780.063l-1.254-4.25c-5.059-17.133-21.186-32.682-38.604-37.218-3.84-1-9.242-1.493-14.549-1.48zm.3 23.324c4.762-.047 9.503.84 13.29 2.666 9.532 4.598 15.446 12.427 17.776 23.536.971 4.63.886 6.578-.52 11.937-.924 3.523-2.807 8.191-4.185 10.375-3.33 5.274-10.7 10.775-16.905 12.617-13.61 4.042-27.272-.938-35.388-12.9-3.856-5.683-5.262-10.31-5.272-17.318-.012-11.357 7.273-22.822 17.78-27.98 3.875-1.904 8.66-2.885 13.423-2.933zm-165.71.174c6.784-.041 8.86.373 13.66 2.73 6.529 3.207 12.148 8.977 15.188 15.598 2.973 6.476 2.973 18.77 0 25.246-3.013 6.563-8.648 12.388-14.977 15.485-10.64 5.206-24.218 3.858-33.166-3.291-18.457-14.748-14.793-43.673 6.795-53.64 3.325-1.536 6.59-2.092 12.5-2.128zm.5 150.451v36h39v-36h-19.5zm117 0v36h40v-36h-20zm-117 57l.016 20.25c.01 14.304.447 22.087 1.488 26.504 4.069 17.256 17.425 33.307 34.617 41.602 9.678 4.669 17.897 7.183 28.352 8.674 9.227 1.315 28.846.274 38.527-2.045 4.675-1.12 12.775-4.145 18-6.723 17.883-8.823 30.709-24.722 34.496-42.762.995-4.738 1.475-13.22 1.487-26.25l.017-19.25h-40v18.635c0 11.707-.452 20.164-1.216 22.75-1.754 5.934-9.862 13.753-17.053 16.444-16.938 6.337-39.908 3.58-50.35-6.045-7.89-7.273-8.802-10.487-9.224-32.534l-.37-19.25h-19.392z"/>
</svg>

After

(image error) Size: 4.1 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M346.165 49.901c-16.103 0-32.178 6.191-44.549 18.575-12.774 12.787-18.57 26.914-18.545 45.198.025 17.844 5.108 30.457 17.546 43.552 10.039 10.568 22.881 17.471 36.377 19.553 11.454 1.766 28.106-1.306 40.022-7.386 11.49-5.862 25.093-22.04 29.546-35.14l2.247-6.614h91.154V99.8h-91.267l-1.454-4.93c-2.681-9.08-8.7-18.786-16.362-26.388-12.483-12.386-28.613-18.58-44.715-18.582zm-192.833.023c-25.936 0-51.304 18.185-59.43 42.602L91.483 99.8H.037v27.839h91.455l1.389 4.35c6.9 21.58 25.856 39.02 47.507 43.71 15.063 3.261 26.846 1.762 41.53-5.286 39.44-18.931 47.91-71.2 16.531-102.023-12.554-12.332-27.54-18.466-45.116-18.466zm.293 27.39c7.232-.064 9.457.454 16.182 3.765 27.18 13.38 27.066 51.98-.195 65.318-5.817 2.846-20.076 4.093-25.745 2.251-10.381-3.371-19.176-11.16-23.56-20.867-3.633-8.042-3.4-20.744.534-29.14 6.2-13.232 18.452-21.202 32.784-21.328zm192.737.054c13.171.05 26.326 7.058 33.064 20.998 3.82 7.903 3.83 21.67.02 30.165-3.68 8.21-13.364 17.033-21.27 19.381-7.653 2.273-18.634 2.12-25.158-.35-7.3-2.765-16.618-11.672-19.909-19.03-3.906-8.736-3.864-22.34.093-30.422 6.801-13.887 19.988-20.793 33.16-20.742zM241.3 161.276v106.11l-22.34-22.317c-20.314-20.294-22.7-22.317-26.314-22.317-5.177 0-9.342 3.83-9.342 8.588 0 3.093 4.032 7.578 31.63 35.176 29.126 29.126 31.949 31.63 35.618 31.63 3.66 0 6.431-2.435 33.989-29.867 16.502-16.428 30.775-31.328 31.718-33.115 3.133-5.934-.815-12.412-7.564-12.412-3.854 0-5.796 1.637-26.497 22.317l-22.34 22.317v-106.11h-9.279zm104.972 162.391c-18.015 0-32.164 5.828-44.777 18.441-7.684 7.685-13.285 16.865-16.17 26.508l-1.475 4.93-33.745-.014-33.744-.011-2.186-7.012c-5.88-18.86-24-35.894-43.782-41.159-8.405-2.237-25.676-2.152-33.565.166-20.361 5.98-38.903 24.167-43.937 43.096l-1.31 4.93H.038V400.22H91.628l.734 3.19c1.88 8.15 8.983 20.39 15.915 27.423 8.186 8.305 15.494 12.922 26.311 16.624 9.042 3.094 26.063 3.546 35.806.953 19.77-5.262 38.566-22.993 44.009-41.517l1.96-6.67 33.743-.001h33.745l1.474 4.93c2.886 9.642 8.487 18.823 16.171 26.508 12.613 12.612 26.762 18.438 44.777 18.438 18.016 0 32.165-5.826 44.777-18.438 7.685-7.685 13.286-16.866 16.171-26.508l1.475-4.93h91.267V373.544h-91.267l-1.475-4.93c-2.885-9.643-8.486-18.823-16.17-26.508-12.613-12.612-26.762-18.439-44.778-18.439zm0 27.256c7.8 0 10.327.514 15.845 3.224 7.572 3.718 14.09 10.412 17.616 18.092 3.45 7.511 3.45 21.772 0 29.283-3.495 7.613-10.03 14.37-17.371 17.96-8.399 4.11-19.516 4.851-27.884 1.86-30.644-10.952-33.172-52.894-4.05-67.195 5.518-2.71 8.045-3.224 15.844-3.224zm-192.546.03c7.834-.025 10.3.472 15.844 3.194 7.217 3.544 13.363 9.644 17.444 17.313 2.198 4.128 2.653 6.769 2.651 15.42-.002 9.06-.412 11.217-3.092 16.314-3.725 7.082-10.937 13.843-18.187 17.05-15.315 6.775-34.753 1.303-44.211-12.444-4.534-6.59-7.441-17.776-6.459-24.848 1.07-7.698 6.966-19.164 11.993-23.318 7.897-6.526 13.774-8.65 24.017-8.681z"/>
</svg>

After

(image error) Size: 2.9 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M153.658 49.909c-9.284-.067-18.58 2.134-28 6.604-14.775 7.01-27.865 21.741-32.482 36.555l-2.122 6.813-45.527.011L0 99.902v27.838h91.267l1.456 4.929c4.692 15.893 17.009 30.16 32.684 37.858 5.348 2.627 12.594 5.236 16.103 5.798v-.002c14.512 2.323 27.764.414 40.597-5.852 9.498-4.637 22.415-17.376 27.548-27.168 16.568-31.6 3.864-70.693-28.069-86.384-9.372-4.606-18.644-6.942-27.928-7.01zm191.998.15c-40.132 0-71.217 38.53-62.144 77.025 5.775 24.503 24.788 43.62 48.515 48.774 30.871 6.706 62.672-10.702 73.33-40.144l2.676-7.395 45.656-.58 45.656-.58.326-13.615.329-13.616-45.944-.303-45.943-.304-2.048-5.8c-3.738-10.572-7.3-16.152-15.628-24.485-12.693-12.7-27.501-18.977-44.781-18.977zm-192.248 27.19c18.884-.143 35.996 15.864 36.123 35.411.095 14.556-6.684 26.231-19.177 33.03-8.38 4.562-21.758 5.304-30.835 1.711-23.34-9.239-29.882-41.303-12.05-59.063 6.167-6.142 11.358-8.866 20.038-10.512a32.999 32.999 0 015.901-.577zm192.792.179c4.012.054 8.122.746 12.204 2.13 8.695 2.946 18.563 12.748 21.386 21.247 5.898 17.758-1.087 36.285-16.93 44.908-5.048 2.748-7.826 3.415-15.448 3.716-15.35.605-26.662-5.572-33.622-18.36-2.73-5.015-3.47-8-3.87-15.622-.441-8.41-.15-10.197 2.726-16.604 6.134-13.67 19.223-21.61 33.554-21.415zm-104.937 83.949v52.776c0 29.027-.396 52.776-.881 52.776s-10.789-9.917-22.897-22.038c-18.922-18.942-22.51-22.038-25.539-22.038-5.519 0-8.679 2.872-8.679 7.888 0 4.05 1.673 5.949 31.601 35.892 27.256 27.27 32.075 31.614 35.063 31.614 2.988 0 7.816-4.35 35.097-31.63 26.934-26.935 31.63-32.134 31.63-35.013 0-4.827-4.09-8.751-9.123-8.751-3.748 0-5.902 1.813-26.477 22.29l-22.396 22.29V161.376h-8.7zm8.167 162.402c-17.663.025-30.355 5.073-43.182 17.172-8.831 8.331-12.776 14.25-16.676 25.022l-2.567 7.09-93.502.296L0 373.656v26.651l93.552.296 93.555.297 1.976 5.813c6.953 20.456 23.444 36.374 43.193 41.692 8.445 2.274 26.75 2.245 34.585-.057 20.168-5.924 36.966-22.166 43.372-41.938l1.971-6.09h187.721v-26.678H312.328l-2.62-7.25c-3.843-10.623-7.807-16.832-15.752-24.675-12.44-12.279-26.547-17.963-44.526-17.938zm-.022 27.28c7.558-.03 9.757.474 16.463 3.775 12.855 6.329 19.843 17.664 19.852 32.202.008 14.208-7.952 26.899-20.6 32.844-10.227 4.808-23.042 4.302-33.857-1.336-5.449-2.84-13.91-12.902-15.994-19.017-2.432-7.134-2.353-18.112.179-25.228 2.752-7.739 11.453-17.055 19.12-20.475 4.62-2.061 8.243-2.737 14.837-2.764z"/>
</svg>

After

(image error) Size: 2.4 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M382.316 7.667c-15.872-.235-47.714 1.104-61.212 2.962-32.005 4.406-69.244 13.896-91.83 23.404-5.342 2.248-5.66 2.17-12.837-3.097-19.326-14.187-46.622-22.07-71.126-20.543-28.405 1.77-51.076 12.049-71.622 32.469C62.492 53.99 58.965 58.777 52.85 71.166c-4.049 8.201-8.392 19.858-9.654 25.903-4.806 23.036-2.052 48.437 7.618 70.27l4.002 9.037-8.038 12.944C22.47 228.47 5.202 276.39.954 316.487L0 325.514h37.706v-4.857c0-8.476 6.08-35.068 11.862-51.898 5.203-15.145 23.164-53.485 28.07-59.924 2.055-2.695 2.593-2.546 9.982 2.763 18.245 13.108 39.461 19.651 63.818 19.688 19.01.028 32.058-2.936 48.77-11.083 20.733-10.106 37.148-25.702 48.62-46.2 16.004-28.594 17.686-68.546 4.11-97.557-2.555-5.46-4.16-10.218-3.566-10.57 4.07-2.412 34.919-11.042 50.904-14.241 25.404-5.084 45.493-7.163 69.148-7.163h19.396l1.837-17.56c1.01-9.656 1.43-17.958.935-18.448-.472-.467-3.986-.719-9.276-.797zm-232 39.589c31.396-.398 60.367 18.651 70.892 49.243 14.655 42.593-12.027 88.727-56.154 97.096-40.307 7.644-79.611-20.22-86.55-61.359-5.849-34.673 16.49-71.17 49.886-81.5 7.307-2.26 14.68-3.388 21.926-3.48zm248.571 276.676c-14.803 0-31.883.013-51.597.013H205.39l-5.339 5.339c-4.147 4.147-5.339 6.633-5.339 11.146 0 4.514 1.192 7 5.34 11.147l5.338 5.34h142.037c140.026 0 142.093-.046 146.06-3.165 5.961-4.69 8.104-12.502 5.28-19.261-4.135-9.896 3.743-10.55-99.88-10.559z"/>
</svg>

After

(image error) Size: 1.4 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M152.054 49.928a62.098 62.098 0 00-11.937 1.287c-20.8 4.306-41.307 22.573-47.077 41.934l-1.986 6.667-45.527.002H0v27.838h45.817c44.105 0 45.817.082 45.817 2.186 0 1.202 2.162 6.507 4.805 11.787 8.528 17.04 25.217 30.27 43.07 34.15v.002c31.335 6.808 63.86-11.457 74.061-41.592l2.016-5.951h67.693l2.677 7.392c8.759 24.188 33.964 41.864 59.7 41.864 26.285 0 54.078-20.373 60.488-44.34l1.463-5.471 45.87-.304 45.868-.301.326-13.615.329-13.618-45.984-.302-45.983-.303-2.591-7.161c-11.009-30.405-43.206-47.787-74.745-40.353-20.388 4.806-38.75 21.616-45.547 41.698l-1.969 5.816-33.622.306-33.621.308-1.398-4.368c-8.83-27.617-34.783-45.852-62.486-45.559zm193.93 27.322c8.762.005 17.597 3.217 24.615 10.276 8.755 8.806 10.435 13.027 10.435 26.211 0 10.05-.303 11.568-3.446 17.243-3.541 6.392-9.705 12.199-16.853 15.876-6.239 3.21-21.047 3.48-28.506.523-15.69-6.22-25.068-22.733-22.422-39.475 3-18.972 19.452-30.662 36.177-30.654zm-192.682.1c21.682.255 41.471 20.325 35.316 44.35-3.002 11.72-8.978 19.23-19.714 24.77-3.396 1.754-7.455 2.58-14.055 2.855-11.988.502-19.297-2.217-27.199-10.12-15.693-15.693-13.671-41.92 4.232-54.919 6.757-4.906 14.193-7.022 21.42-6.937zm86.801 83.944v106.11l-22.34-22.317c-18.392-18.375-22.912-22.317-25.592-22.317-7.412 0-11.254 5.433-8.448 11.945.88 2.043 15.142 17.154 31.693 33.581 27.498 27.292 30.434 29.868 34.037 29.868 3.614 0 6.58-2.635 35.575-31.63 26.934-26.935 31.63-32.134 31.63-35.013 0-4.827-4.09-8.751-9.123-8.751-3.748 0-5.902 1.813-26.477 22.29l-22.396 22.29V161.293h-9.28zm-86.946 162.402c-17.286.024-30.241 5.04-42.398 16.411-8.6 8.044-13.832 15.762-17.435 25.713l-2.592 7.159-45.366.304L0 373.586v26.624l45.366.304 45.366.303 2.567 7.091c3.905 10.787 7.848 16.69 16.737 25.077 8.172 7.709 18.697 13.76 27.553 15.84 7.612 1.787 26.073 1.472 32.998-.562 10.323-3.032 19.754-8.704 27.78-16.708 24.805-24.738 24.797-64.496-.02-89.314-12.771-12.77-26.903-18.57-45.19-18.545zm189.792.01c-4.188.032-8.127.367-11.285 1.042-27.91 5.971-49.77 33.268-49.77 62.151 0 27.744 20.105 54.286 46.495 61.383 9.722 2.615 27.46 2.33 36.152-.582 20.148-6.75 36.39-23.049 42.038-42.181l1.389-4.7 45.98-.304 45.977-.302V373.587l-45.977-.303-45.98-.304-1.39-4.7c-5.19-17.586-19.866-33.307-38.104-40.822-5.902-2.432-16.312-3.823-25.525-3.752zm-189.815 27.26c7.545-.02 9.77.49 16.463 3.786 12.843 6.323 19.783 17.585 19.918 32.32.067 7.257-.425 9.275-3.955 16.195-4.678 9.171-12.244 15.706-21.352 18.448-14.165 4.263-28 .237-38.212-11.121-12.826-14.266-11.536-37.323 2.814-50.287 7.517-6.791 14.083-9.313 24.324-9.34zm192.522.02c11.51.038 18.931 3.3 26.603 11.695 7.279 7.964 9.754 15.076 9.162 26.336-.724 13.78-7.532 24.283-19.76 30.48-15.68 7.945-36.509 2.49-46.167-12.091-12.337-18.625-5.23-43.961 14.981-53.416 5.212-2.437 8.187-3.027 15.18-3.004z"/>
</svg>

After

(image error) Size: 2.8 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M155.038 59.376c-7.443-.052-15.18.864-20.535 2.69-20.133 6.863-36.071 23.005-41.768 42.3l-1.455 4.93H0V135.979h91.28l1.475 4.931c6 20.05 23.953 37.66 44.136 43.29 7.054 1.969 25.896 2 33.194.057 21.222-5.65 39.566-23.908 45.167-44.949 2.249-8.45 2.153-25.69-.186-33.57-6.428-21.65-22.802-38.026-44.453-44.453-4.174-1.24-9.786-1.867-15.575-1.908zm190.565.095c-6.156.014-12.202.613-16.216 1.793-24.192 7.11-41.7 26.563-45.921 51.019-2.774 16.072.83 32.936 9.836 46.025 8.177 11.885 24.185 23.284 36.883 26.265 7.662 1.8 26.08 1.473 33.003-.582 20.662-6.134 37.43-22.234 43.66-41.922l1.928-6.09H500v-26.683h-91.28l-1.455-4.93c-5.868-19.876-24.577-37.914-44.784-43.177-4.455-1.16-10.721-1.732-16.878-1.718zm.347 27.059c5.526-.055 11.025.973 15.419 3.092 11.058 5.335 17.918 14.416 20.621 27.303 1.127 5.372 1.028 7.631-.603 13.85-1.072 4.085-3.256 9.502-4.855 12.035-3.862 6.118-12.413 12.5-19.61 14.637-15.79 4.69-31.64-1.088-41.055-14.966-4.473-6.592-6.104-11.96-6.115-20.09-.015-13.175 8.437-26.475 20.625-32.46 4.496-2.208 10.048-3.346 15.573-3.401zm-192.238.201c7.87-.048 10.279.433 15.847 3.168 7.574 3.72 14.092 10.413 17.62 18.095 3.449 7.512 3.449 21.775 0 29.287-3.496 7.614-10.033 14.372-17.375 17.964-12.344 6.04-28.096 4.476-38.476-3.818-21.412-17.11-17.161-50.665 7.883-62.228 3.857-1.781 7.645-2.426 14.501-2.468zm.58 174.538V303.032h45.244V261.269h-22.622zm135.731 0V303.032h46.404V261.269h-23.202zm-135.73 66.125l.017 23.492c.013 16.594.519 25.622 1.727 30.747 4.72 20.018 20.215 38.639 40.16 48.261 11.226 5.417 20.761 8.334 32.89 10.063 10.704 1.526 33.464.318 44.695-2.372 5.423-1.3 14.82-4.809 20.882-7.8 20.746-10.235 35.624-28.679 40.018-49.607 1.154-5.497 1.71-15.336 1.724-30.452l.02-22.332H290.024v21.618c0 13.581-.525 23.392-1.411 26.392-2.034 6.884-11.44 15.955-19.783 19.076-19.65 7.352-46.297 4.152-58.41-7.013-9.153-8.437-10.211-12.165-10.702-37.741l-.428-22.332h-22.497z"/>
</svg>

After

(image error) Size: 1.9 KiB

View file

@ -0,0 +1,3 @@
<svg width="500" height="500" version="1.1" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<path d="m1.75e-5 -0.034425 1.8524 6.6668c19.855 70.353 37.923 148.23 48.145 211.81-5e-3 -0.0421-0.01033-0.0839-0.01528-0.12604l0.02101 0.1566c-0.0017-0.0103-0.0041-0.0203-0.0058-0.0305 6.5425 55.391 29.322 104.35 62.178 137.23 32.904 36.138 79.123 55.863 134.67 55.863 10.162 0 19.793-0.54966 28.999-1.545l90.041 90.041 134.11-137.47-88.933-88.935c0.46511-8.606 0.50799-17.513 0.50799-26.818 0-55.505-19.76-101.81-59.111-134.71-32.95-36.112-81.987-55.605-137.1-62.126-77.669-8.773-142.22-29.696-215.36-50.013zm99.234 67.97c37.995 8.7741 76.301 16.145 114.87 21.925l0.0325 0.0039 0.0305 0.0059c47.638 6.3518 87.018 22.988 114.41 49.203 27.369 26.196 42.991 61.95 43.014 107.64l-3.3401 43.402 75.699 75.699-78.093 78.093-73.976-73.976-1.6786 1.6786-0.0669-0.0573h-43.283c-45.738 0-81.521-15.631-107.74-43.02-26.214-27.389-42.852-66.766-49.203-114.4-5.5829-47.428-13.468-84.629-19.683-112.68l111.63 111.63c-12.123 33.534 12.706 69.76 48.904 69.805h6e-3c28.759 0 52.156-23.399 52.156-52.158 0-28.759-23.397-52.156-52.156-52.156h-2e-3c-6.0543 3e-3 -12.035 1.1211-17.714 3.1777z" clip-rule="evenodd" fill-rule="evenodd"/>
</svg>

After

(image error) Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M108.957 0v500l8.142-8.142 129.769-129.77h224.175zm39.35 94.576l228.162 228.163H230.994l-1.398 1.395-81.29 81.29z"/>
</svg>

After

(image error) Size: 198 B

View file

@ -206,6 +206,12 @@
&.menu {
margin-right: 0;
width: 2rem;
height: 2rem;
display: flex;
justify-content: flex-end;
align-items: flex-end;
flex-direction: column;
svg {
fill: $color-gray-60;

View file

@ -225,3 +225,88 @@
padding: $x-small;
}
}
.viewport-actions {
position: absolute;
margin-left: auto;
width: 100%;
margin-top: 2rem;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.path-actions {
display: flex;
flex-direction: row;
background: white;
border-radius: 3px;
padding: 0.5rem;
border: 1px solid $color-gray-20;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.viewport-actions-group {
display: flex;
flex-direction: row;
border-right: 1px solid $color-gray-20;
}
.viewport-actions-entry {
width: 28px;
height: 28px;
margin: 0 0.25rem;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
border-radius: 3px;
svg {
width: 20px;
height: 20px;
}
&:hover svg {
fill: $color-primary;
}
&.is-disabled {
opacity: 0.3;
&:hover svg {
fill: initial;
}
}
&.is-toggled {
background: $color-black;
svg {
fill: $color-primary;
}
}
}
.viewport-actions-entry-wide {
width: 27px;
height: 20px;
svg {
width: 27px;
height: 20px;
}
}
.path-actions > :first-child .viewport-actions-entry {
margin-left: 0;
}
.path-actions > :last-child {
border: none;
}
.path-actions > :last-child .viewport-actions-entry {
margin-right: 0;
}
}

View file

@ -13,7 +13,9 @@
[app.common.exceptions :as ex]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.geom.shapes :as gsh]
[app.common.geom.proportions :as gpr]
[app.common.geom.align :as gal]
[app.common.math :as mth]
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
@ -29,6 +31,8 @@
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.drawing.path :as dwdp]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.streams :as ms]
@ -339,7 +343,7 @@
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
shapes (cph/select-toplevel-shapes objects {:include-frames? true})
srect (geom/selection-rect shapes)
srect (gsh/selection-rect shapes)
local (assoc local :vport size :zoom 1)]
(cond
(or (not (mth/finite? (:width srect)))
@ -348,7 +352,7 @@
(or (> (:width srect) width)
(> (:height srect) height))
(let [srect (geom/adjust-to-viewport size srect {:padding 40})
(let [srect (gal/adjust-to-viewport size srect {:padding 40})
zoom (/ (:width size) (:width srect))]
(-> local
(assoc :zoom zoom)
@ -471,10 +475,10 @@
(let [vbox (update vbox :x + (:left-offset vbox))
new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom)
old-zoom (:zoom local)
center (if center center (geom/center vbox))
center (if center center (gsh/center-rect vbox))
scale (/ old-zoom new-zoom)
mtx (gmt/scale-matrix (gpt/point scale) center)
vbox' (geom/transform vbox mtx)
vbox' (gsh/transform-rect vbox mtx)
vbox' (update vbox' :x - (:left-offset vbox))]
(-> local
(assoc :zoom new-zoom)
@ -510,14 +514,14 @@
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
shapes (cph/select-toplevel-shapes objects {:include-frames? true})
srect (geom/selection-rect shapes)]
srect (gsh/selection-rect shapes)]
(if (or (mth/nan? (:width srect))
(mth/nan? (:height srect)))
state
(update state :workspace-local
(fn [{:keys [vbox vport] :as local}]
(let [srect (geom/adjust-to-viewport vport srect {:padding 40})
(let [srect (gal/adjust-to-viewport vport srect {:padding 40})
zoom (/ (:width vport) (:width srect))]
(-> local
(assoc :zoom zoom)
@ -534,10 +538,10 @@
objects (dwc/lookup-page-objects state page-id)
srect (->> selected
(map #(get objects %))
(geom/selection-rect))]
(gsh/selection-rect))]
(update state :workspace-local
(fn [{:keys [vbox vport] :as local}]
(let [srect (geom/adjust-to-viewport vport srect {:padding 40})
(let [srect (gal/adjust-to-viewport vport srect {:padding 40})
zoom (/ (:width vport) (:width srect))]
(-> local
(assoc :zoom zoom)
@ -545,50 +549,6 @@
;; --- Add shape to Workspace
(declare start-edition-mode)
(defn add-shape
[attrs]
(us/verify ::shape-attrs attrs)
(ptk/reify ::add-shape
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
id (uuid/next)
shape (geom/setup-proportions attrs)
unames (dwc/retrieve-used-names objects)
name (dwc/generate-unique-name unames (:name shape))
frame-id (or (:frame-id attrs)
(cph/frame-id-by-position objects attrs))
shape (merge
(if (= :frame (:type shape))
cp/default-frame-attrs
cp/default-shape-attrs)
(assoc shape
:id id
:name name))
rchange {:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:obj shape}
uchange {:type :del-obj
:page-id page-id
:id id}]
(rx/concat
(rx/of (dwc/commit-changes [rchange] [uchange] {:commit-local? true})
(dws/select-shapes (d/ordered-set id)))
(when (= :text (:type attrs))
(->> (rx/of (start-edition-mode id))
(rx/observe-on :async))))))))
(defn- viewport-center
[state]
(let [{:keys [x y width height]} (get-in state [:workspace-local :vbox])]
@ -614,8 +574,8 @@
(merge data)
(merge {:x x :y y})
(assoc :frame-id frame-id)
(geom/setup-selrect))]
(rx/of (add-shape shape))))))
(gsh/setup-selrect))]
(rx/of (dwc/add-shape shape))))))
;; --- Update Shape Attrs
@ -953,7 +913,7 @@
(defn align-objects
[axis]
(us/verify ::geom/align-axis axis)
(us/verify ::gal/align-axis axis)
(ptk/reify :align-objects
ptk/WatchEvent
(watch [_ state stream]
@ -991,17 +951,17 @@
[objects object-id axis]
(let [object (get objects object-id)
frame (get objects (:frame-id object))]
(geom/align-to-rect object frame axis objects)))
(gal/align-to-rect object frame axis objects)))
(defn align-objects-list
[objects selected axis]
(let [selected-objs (map #(get objects %) selected)
rect (geom/selection-rect selected-objs)]
(mapcat #(geom/align-to-rect % rect axis objects) selected-objs)))
rect (gsh/selection-rect selected-objs)]
(mapcat #(gal/align-to-rect % rect axis objects) selected-objs)))
(defn distribute-objects
[axis]
(us/verify ::geom/dist-axis axis)
(us/verify ::gal/dist-axis axis)
(ptk/reify :align-objects
ptk/WatchEvent
(watch [_ state stream]
@ -1009,7 +969,7 @@
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
moved (-> (map #(get objects %) selected)
(geom/distribute-space axis objects))]
(gal/distribute-space axis objects))]
(loop [moved (seq moved)
rchanges []
uchanges []]
@ -1034,62 +994,6 @@
:operations ops2
:id (:id curr)})))))))))
;; --- Start shape "edition mode"
(declare clear-edition-mode)
(defn start-edition-mode
[id]
(us/assert ::us/uuid id)
(ptk/reify ::start-edition-mode
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :edition] id))
ptk/WatchEvent
(watch [_ state stream]
(->> stream
(rx/filter dwc/interrupt?)
(rx/take 1)
(rx/map (constantly clear-edition-mode))))))
(def clear-edition-mode
(ptk/reify ::clear-edition-mode
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :edition))))
;; --- Select for Drawing
(def clear-drawing
(ptk/reify ::clear-drawing
ptk/UpdateEvent
(update [_ state]
(update state :workspace-drawing dissoc :tool :object))))
(defn select-for-drawing
([tool] (select-for-drawing tool nil))
([tool data]
(ptk/reify ::select-for-drawing
ptk/UpdateEvent
(update [_ state]
(update state :workspace-drawing assoc :tool tool :object data))
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter (ptk/type? ::clear-drawing) stream)]
(rx/merge
(rx/of (dws/deselect-all))
;; NOTE: comments are a special case and they manage they
;; own interrupt cycle.
(when (not= tool :comments)
(->> stream
(rx/filter dwc/interrupt?)
(rx/take 1)
(rx/map (constantly clear-drawing))
(rx/take-until stoper)))))))))
;; --- Update Dimensions
;; Event mainly used for handling user modification of the size of the
@ -1103,7 +1007,7 @@
(ptk/reify ::update-dimensions
ptk/WatchEvent
(watch [_ state stream]
(rx/of (dwc/update-shapes ids #(geom/resize-rect % attr value))))))
(rx/of (dwc/update-shapes ids #(gsh/resize-rect % attr value))))))
;; --- Shape Proportions
@ -1117,7 +1021,7 @@
(if-not lock
(assoc shape :proportion-lock false)
(-> (assoc shape :proportion-lock true)
(geom/assign-proportions)))))))))
(gpr/assign-proportions)))))))))
;; --- Update Shape Position
(s/def ::x number?)
@ -1142,23 +1046,6 @@
(rx/of (dwt/set-modifiers [id] {:displacement displ})
(dwt/apply-modifiers [id]))))))
;; --- Path Modifications
(defn update-path
"Update a concrete point in the path shape."
[id index delta]
(us/verify ::us/uuid id)
(us/verify ::us/integer index)
(us/verify gpt/point? delta)
(js/alert "TODO: broken")
#_(ptk/reify ::update-path
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)]
(-> state
(update-in [:workspace-data page-id :objects id :segments index] gpt/add delta)
(update-in [:workspace-data page-id :objects id] geom/update-path-selrect))))))
;; --- Shape attrs (Layers Sidebar)
(defn toggle-collapse
@ -1290,7 +1177,7 @@
;; When the parent frame is not selected we change to relative
;; coordinates
(let [frame (get objects (:frame-id shape))]
(geom/translate-to-frame shape frame))
(gsh/translate-to-frame shape frame))
shape))
(prepare [result objects selected id]
@ -1329,7 +1216,7 @@
ptk/WatchEvent
(watch [_ state stream]
(let [selected-objs (map #(get objects %) selected)
wrapper (geom/selection-rect selected-objs)
wrapper (gsh/selection-rect selected-objs)
orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper))
mouse-pos @ms/mouse-position
@ -1359,7 +1246,7 @@
(map #(get-in % [:obj :id]))
(into (d/ordered-set)))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes selected))))))
(dwc/select-shapes selected))))))
(defn- image-uploaded
[image]
@ -1446,7 +1333,7 @@
page-id (:current-page-id state)
frame-id (-> (dwc/lookup-page-objects state page-id)
(cph/frame-id-by-position @ms/mouse-position))
shape (geom/setup-selrect
shape (gsh/setup-selrect
{:id id
:type :text
:name "Text"
@ -1459,7 +1346,7 @@
:content (as-content text)})]
(rx/of dwc/start-undo-transaction
(dws/deselect-all)
(add-shape shape)
(dwc/add-shape shape)
dwc/commit-undo-transaction)))))
(defn update-shape-flags
@ -1490,7 +1377,7 @@
(when-not (empty? shapes)
(let [[group rchanges uchanges] (dws/prepare-create-group page-id shapes "Group-" false)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(dwc/select-shapes (d/ordered-set (:id group))))))))))
(def ungroup-selected
(ptk/reify ::ungroup-selected
@ -1568,7 +1455,7 @@
:val (:fill-color mask)}]}))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(dwc/select-shapes (d/ordered-set (:id group))))))))))
(def unmask-group
(ptk/reify ::unmask-group
@ -1595,7 +1482,7 @@
:val (:masked-group? group)}]}]]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(dwc/select-shapes (d/ordered-set (:id group))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1718,11 +1605,16 @@
(def select-shape dws/select-shape)
(def deselect-all dws/deselect-all)
(def select-shapes dws/select-shapes)
(def select-shapes dwc/select-shapes)
(def duplicate-selected dws/duplicate-selected)
(def handle-selection dws/handle-selection)
(def select-inside-group dws/select-inside-group)
(def select-for-drawing dwd/select-for-drawing)
(def clear-edition-mode dwc/clear-edition-mode)
(def add-shape dwc/add-shape)
(def start-edition-mode dwc/start-edition-mode)
(defn start-path-edit [id] (dwdp/start-path-edit id))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts
@ -1730,6 +1622,18 @@
;; Shortcuts impl https://github.com/ccampbell/mousetrap
(defn esc-pressed []
(ptk/reify :esc-pressed
ptk/WatchEvent
(watch [_ state stream]
;; Not interrupt when we're editing a path
(let [edition-id (get-in state [:workspace-local :edition])
path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])]
(if-not (= :draw path-edit-mode)
(rx/of :interrupt
(deselect-all true))
(rx/empty))))))
(def shortcuts
{"ctrl+i" #(st/emit! (toggle-layout-flags :assets))
"ctrl+l" #(st/emit! (toggle-layout-flags :sitemap :layers))
@ -1753,15 +1657,16 @@
"ctrl+shift+z" #(st/emit! dwc/redo)
"ctrl+y" #(st/emit! dwc/redo)
"ctrl+q" #(st/emit! dwc/reinitialize-undo)
"a" #(st/emit! (select-for-drawing :frame))
"b" #(st/emit! (select-for-drawing :rect))
"e" #(st/emit! (select-for-drawing :circle))
"a" #(st/emit! (dwd/select-for-drawing :frame))
"b" #(st/emit! (dwd/select-for-drawing :rect))
"e" #(st/emit! (dwd/select-for-drawing :circle))
"t" #(st/emit! dwtxt/start-edit-if-selected
(select-for-drawing :text))
(dwd/select-for-drawing :text))
"p" #(st/emit! (dwd/select-for-drawing :path))
"ctrl+c" #(st/emit! copy-selected)
"ctrl+v" #(st/emit! paste)
"ctrl+x" #(st/emit! copy-selected delete-selected)
"escape" #(st/emit! :interrupt (deselect-all true))
"escape" #(st/emit! (esc-pressed))
"del" #(st/emit! delete-selected)
"backspace" #(st/emit! delete-selected)
"ctrl+up" #(st/emit! (vertical-order-selected :up))
@ -1777,4 +1682,3 @@
"right" #(st/emit! (dwt/move-selected :right false))
"left" #(st/emit! (dwt/move-selected :left false))
"i" #(st/emit! (mdc/picker-for-selected-shape ))})

View file

@ -20,8 +20,12 @@
[app.common.uuid :as uuid]
[app.main.worker :as uw]
[app.util.timers :as ts]
[app.common.geom.shapes :as geom]))
[app.common.geom.proportions :as gpr]
[app.common.geom.shapes :as gsh]))
(s/def ::shape-attrs ::cp/shape-attrs)
(s/def ::set-of-string (s/every string? :kind set?))
(s/def ::ordered-set-of-uuid (s/every uuid? :kind d/ordered-set?))
;; --- Protocols
(declare setup-selection-index)
@ -158,7 +162,7 @@
(defn get-frame-at-point
[objects point]
(let [frames (cph/select-frames objects)]
(d/seek #(geom/has-point? % point) frames)))
(d/seek #(gsh/has-point? % point) frames)))
(defn- extract-numeric-suffix
@ -171,8 +175,6 @@
[objects]
(into #{} (map :name) (vals objects)))
(s/def ::set-of-string
(s/every string? :kind set?))
(defn generate-unique-name
"A unique name generator"
@ -434,3 +436,92 @@
[rchanges uchanges] (impl-gen-changes objects page-id (seq ids))]
(rx/of (commit-changes rchanges uchanges {:commit-local? true})))))))
(defn select-shapes
[ids]
(us/verify ::ordered-set-of-uuid ids)
(ptk/reify ::select-shapes
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :selected] ids))
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (lookup-page-objects state page-id)]
(rx/of (expand-all-parents ids objects))))))
;; --- Start shape "edition mode"
(declare clear-edition-mode)
(defn start-edition-mode
[id]
(us/assert ::us/uuid id)
(ptk/reify ::start-edition-mode
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data :pages-index page-id :objects])]
;; Can only edit objects that exist
(if (contains? objects id)
(-> state
(assoc-in [:workspace-local :selected] #{id})
(assoc-in [:workspace-local :edition] id))
state)))
ptk/WatchEvent
(watch [_ state stream]
(->> stream
(rx/filter interrupt?)
(rx/take 1)
(rx/map (constantly clear-edition-mode))))))
(def clear-edition-mode
(ptk/reify ::clear-edition-mode
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :edition))))
(defn add-shape
[attrs]
(us/verify ::shape-attrs attrs)
(ptk/reify ::add-shape
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (lookup-page-objects state page-id)
id (or (:id attrs) (uuid/next))
shape (gpr/setup-proportions attrs)
unames (retrieve-used-names objects)
name (generate-unique-name unames (:name shape))
frame-id (or (:frame-id attrs)
(cph/frame-id-by-position objects attrs))
shape (merge
(if (= :frame (:type shape))
cp/default-frame-attrs
cp/default-shape-attrs)
(assoc shape
:id id
:name name))
rchange {:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:obj shape}
uchange {:type :del-obj
:page-id page-id
:id id}]
(rx/concat
(rx/of (commit-changes [rchange] [uchange] {:commit-local? true})
(select-shapes (d/ordered-set id)))
(when (= :text (:type attrs))
(->> (rx/of (start-edition-mode id))
(rx/observe-on :async))))))))

View file

@ -12,24 +12,48 @@
(:require
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.spec :as us]
[app.common.pages :as cp]
[app.common.uuid :as uuid]
[app.common.pages-helpers :as cph]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.snap :as snap]
[app.main.streams :as ms]
[app.util.geom.path :as path]))
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.drawing.common :as common]
[app.main.data.workspace.drawing.path :as path]
[app.main.data.workspace.drawing.curve :as curve]
[app.main.data.workspace.drawing.box :as box]))
(declare start-drawing)
(declare handle-drawing)
(declare handle-drawing-generic)
(declare handle-drawing-path)
(declare handle-drawing-curve)
(declare handle-finish-drawing)
(declare conditional-align)
;; --- Select for Drawing
(defn select-for-drawing
([tool] (select-for-drawing tool nil))
([tool data]
(ptk/reify ::select-for-drawing
ptk/UpdateEvent
(update [_ state]
(update state :workspace-drawing assoc :tool tool :object data))
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter (ptk/type? ::clear-drawing) stream)]
(rx/merge
(rx/of (dws/deselect-all))
(when (= tool :path)
(rx/of (start-drawing :path)))
;; NOTE: comments are a special case and they manage they
;; own interrupt cycle.q
(when (and (not= tool :comments)
(not= tool :path))
(->> stream
(rx/filter dwc/interrupt?)
(rx/take 1)
(rx/map (constantly common/clear-drawing))
(rx/take-until stoper)))))))))
;; NOTE/TODO: when an exception is raised in some point of drawing the
;; draw lock is not released so the user need to refresh in order to
@ -38,20 +62,22 @@
(defn start-drawing
[type]
{:pre [(keyword? type)]}
(let [id (uuid/next)]
(let [lock-id (uuid/next)]
(ptk/reify ::start-drawing
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-drawing :lock] #(if (nil? %) id %)))
(update-in state [:workspace-drawing :lock] #(if (nil? %) lock-id %)))
ptk/WatchEvent
(watch [_ state stream]
(let [lock (get-in state [:workspace-drawing :lock])]
(when (= lock id)
(rx/merge (->> (rx/filter #(= % handle-finish-drawing) stream)
(rx/take 1)
(rx/map (fn [_] #(update % :workspace-drawing dissoc :lock))))
(rx/of (handle-drawing type)))))))))
(when (= lock lock-id)
(rx/merge
(rx/of (handle-drawing type))
(->> stream
(rx/filter (ptk/type? ::common/handle-finish-drawing) )
(rx/first)
(rx/map #(fn [state] (update state :workspace-drawing dissoc :lock)))))))))))
(defn handle-drawing
[type]
@ -63,248 +89,15 @@
ptk/WatchEvent
(watch [_ state stream]
(case type
:path (rx/of handle-drawing-path)
:curve (rx/of handle-drawing-curve)
(rx/of handle-drawing-generic)))))
(rx/of (case type
:path
(path/handle-new-shape)
(def handle-drawing-generic
(letfn [(resize-shape [{:keys [x y width height] :as shape} point lock? point-snap]
(let [;; The new shape behaves like a resize on the bottom-right corner
initial (gpt/point (+ x width) (+ y height))
shapev (gpt/point width height)
deltav (gpt/to-vec initial point-snap)
scalev (gpt/divide (gpt/add shapev deltav) shapev)
scalev (if lock?
(let [v (max (:x scalev) (:y scalev))]
(gpt/point v v))
scalev)]
(-> shape
(assoc ::click-draw? false)
(assoc-in [:modifiers :resize-vector] scalev)
(assoc-in [:modifiers :resize-origin] (gpt/point x y))
(assoc-in [:modifiers :resize-rotation] 0))))
:curve
(curve/handle-drawing-curve)
(update-drawing [state point lock? point-snap]
(update-in state [:workspace-drawing :object] resize-shape point lock? point-snap))]
(ptk/reify ::handle-drawing-generic
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [flags]} (:workspace-local state)
stoper? #(or (ms/mouse-up? %) (= % :interrupt))
stoper (rx/filter stoper? stream)
initial @ms/mouse-position
;; default
(box/handle-drawing-box))))))
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
layout (get state :workspace-layout)
frames (cph/select-frames objects)
fid (or (->> frames
(filter #(geom/has-point? % initial))
first
:id)
uuid/zero)
shape (-> state
(get-in [:workspace-drawing :object])
(geom/setup {:x (:x initial) :y (:y initial) :width 1 :height 1})
(assoc :frame-id fid)
(assoc ::initialized? true)
(assoc ::click-draw? true))]
(rx/concat
;; Add shape to drawing state
(rx/of #(assoc-in state [:workspace-drawing :object] shape))
;; Initial SNAP
(->> (snap/closest-snap-point page-id [shape] layout initial)
(rx/map (fn [{:keys [x y]}]
#(update-in % [:workspace-drawing :object] assoc :x x :y y))))
(->> ms/mouse-position
(rx/filter #(> (gpt/distance % initial) 2))
(rx/with-latest vector ms/mouse-position-ctrl)
(rx/switch-map
(fn [[point :as current]]
(->> (snap/closest-snap-point page-id [shape] layout point)
(rx/map #(conj current %)))))
(rx/map
(fn [[pt ctrl? point-snap]]
#(update-drawing % pt ctrl? point-snap)))
(rx/take-until stoper))
(rx/of handle-finish-drawing)))))))
(def handle-drawing-path
(letfn [(stoper-event? [{:keys [type shift] :as event}]
(or (= event :path/end-path-drawing)
(= event :interrupt)
(and (ms/mouse-event? event)
(or (= type :double-click)
(= type :context-menu)))
(and (ms/keyboard-event? event)
(= type :down)
(= 13 (:key event)))))
(initialize-drawing [state point]
(-> state
(assoc-in [:workspace-drawing :object :segments] [point point])
(assoc-in [:workspace-drawing :object ::initialized?] true)))
(insert-point-segment [state point]
(-> state
(update-in [:workspace-drawing :object :segments] (fnil conj []) point)))
(update-point-segment [state index point]
(let [segments (count (get-in state [:workspace-drawing :object :segments]))
exists? (< -1 index segments)]
(cond-> state
exists? (assoc-in [:workspace-drawing :object :segments index] point))))
(finish-drawing-path [state]
(update-in
state [:workspace-drawing :object]
(fn [shape] (-> shape
(update :segments #(vec (butlast %)))
(geom/update-path-selrect)))))]
(ptk/reify ::handle-drawing-path
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [flags]} (:workspace-local state)
last-point (volatile! @ms/mouse-position)
stoper (->> (rx/filter stoper-event? stream)
(rx/share))
mouse (rx/sample 10 ms/mouse-position)
points (->> stream
(rx/filter ms/mouse-click?)
(rx/filter #(false? (:shift %)))
(rx/with-latest vector mouse)
(rx/map second))
counter (rx/merge (rx/scan #(inc %) 1 points) (rx/of 1))
stream' (->> mouse
(rx/with-latest vector ms/mouse-position-ctrl)
(rx/with-latest vector counter)
(rx/map flatten))
imm-transform #(vector (- % 7) (+ % 7) %)
immanted-zones (vec (concat
(map imm-transform (range 0 181 15))
(map (comp imm-transform -) (range 0 181 15))))
align-position (fn [angle pos]
(reduce (fn [pos [a1 a2 v]]
(if (< a1 angle a2)
(reduced (gpt/update-angle pos v))
pos))
pos
immanted-zones))]
(rx/merge
(rx/of #(initialize-drawing % @last-point))
(->> points
(rx/take-until stoper)
(rx/map (fn [pt] #(insert-point-segment % pt))))
(rx/concat
(->> stream'
(rx/take-until stoper)
(rx/map (fn [[point ctrl? index :as xxx]]
(let [point (if ctrl?
(as-> point $
(gpt/subtract $ @last-point)
(align-position (gpt/angle $) $)
(gpt/add $ @last-point))
point)]
#(update-point-segment % index point)))))
(rx/of finish-drawing-path
handle-finish-drawing))))))))
(def simplify-tolerance 0.3)
(def handle-drawing-curve
(letfn [(stoper-event? [{:keys [type shift] :as event}]
(ms/mouse-event? event) (= type :up))
(initialize-drawing [state]
(assoc-in state [:workspace-drawing :object ::initialized?] true))
(insert-point-segment [state point]
(update-in state [:workspace-drawing :object :segments] (fnil conj []) point))
(finish-drawing-curve [state]
(update-in
state [:workspace-drawing :object]
(fn [shape]
(-> shape
(update :segments #(path/simplify % simplify-tolerance))
(geom/update-path-selrect)))))]
(ptk/reify ::handle-drawing-curve
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [flags]} (:workspace-local state)
stoper (rx/filter stoper-event? stream)
mouse (rx/sample 10 ms/mouse-position)]
(rx/concat
(rx/of initialize-drawing)
(->> mouse
(rx/map (fn [pt] #(insert-point-segment % pt)))
(rx/take-until stoper))
(rx/of finish-drawing-curve
handle-finish-drawing)))))))
(def handle-finish-drawing
(ptk/reify ::handle-finish-drawing
ptk/WatchEvent
(watch [_ state stream]
(let [shape (get-in state [:workspace-drawing :object])]
(rx/concat
(rx/of dw/clear-drawing)
(when (::initialized? shape)
(let [shape-click-width (case (:type shape)
:text 3
20)
shape-click-height (case (:type shape)
:text 16
20)
shape (if (::click-draw? shape)
(-> shape
(assoc-in [:modifiers :resize-vector]
(gpt/point shape-click-width shape-click-height))
(assoc-in [:modifiers :resize-origin]
(gpt/point (:x shape) (:y shape))))
shape)
shape (cond-> shape
(= (:type shape) :text) (assoc :grow-type
(if (::click-draw? shape) :auto-width :fixed)))
shape (-> shape
geom/transform-shape
(dissoc ::initialized? ::click-draw?))]
;; Add & select the created shape to the workspace
(rx/concat
(if (= :text (:type shape))
(rx/of dwc/start-undo-transaction)
(rx/empty))
(rx/of (dw/deselect-all)
(dw/add-shape shape))))))))))
(def close-drawing-path
(ptk/reify ::close-drawing-path
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-drawing :object :close?] true))))

View file

@ -0,0 +1,92 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.workspace.drawing.box
(:require
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.uuid :as uuid]
[app.common.pages-helpers :as cph]
[app.main.data.workspace.common :as dwc]
[app.main.snap :as snap]
[app.main.streams :as ms]
[app.main.data.workspace.drawing.common :as common]))
(defn resize-shape [{:keys [x y width height] :as shape} point lock? point-snap]
(let [;; The new shape behaves like a resize on the bottom-right corner
initial (gpt/point (+ x width) (+ y height))
shapev (gpt/point width height)
deltav (gpt/to-vec initial point-snap)
scalev (gpt/divide (gpt/add shapev deltav) shapev)
scalev (if lock?
(let [v (max (:x scalev) (:y scalev))]
(gpt/point v v))
scalev)]
(-> shape
(assoc :click-draw? false)
(assoc-in [:modifiers :resize-vector] scalev)
(assoc-in [:modifiers :resize-origin] (gpt/point x y))
(assoc-in [:modifiers :resize-rotation] 0))))
(defn update-drawing [state point lock? point-snap]
(update-in state [:workspace-drawing :object] resize-shape point lock? point-snap))
(defn handle-drawing-box []
(ptk/reify ::handle-drawing-box
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [flags]} (:workspace-local state)
stoper? #(or (ms/mouse-up? %) (= % :interrupt))
stoper (rx/filter stoper? stream)
initial @ms/mouse-position
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
layout (get state :workspace-layout)
frames (cph/select-frames objects)
fid (or (->> frames
(filter #(gsh/has-point? % initial))
first
:id)
uuid/zero)
shape (-> state
(get-in [:workspace-drawing :object])
(gsh/setup {:x (:x initial) :y (:y initial) :width 1 :height 1})
(assoc :frame-id fid)
(assoc :initialized? true)
(assoc :click-draw? true))]
(rx/concat
;; Add shape to drawing state
(rx/of #(assoc-in state [:workspace-drawing :object] shape))
;; Initial SNAP
(->> (snap/closest-snap-point page-id [shape] layout initial)
(rx/map (fn [{:keys [x y]}]
#(update-in % [:workspace-drawing :object] gsh/absolute-move (gpt/point x y))
)))
(->> ms/mouse-position
(rx/filter #(> (gpt/distance % initial) 2))
(rx/with-latest vector ms/mouse-position-ctrl)
(rx/switch-map
(fn [[point :as current]]
(->> (snap/closest-snap-point page-id [shape] layout point)
(rx/map #(conj current %)))))
(rx/map
(fn [[pt ctrl? point-snap]]
#(update-drawing % pt ctrl? point-snap)))
(rx/take-until stoper))
(rx/of common/handle-finish-drawing))))))

View file

@ -0,0 +1,62 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.workspace.drawing.common
(:require
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.selection :as dws]
[app.main.streams :as ms]))
(def clear-drawing
(ptk/reify ::clear-drawing
ptk/UpdateEvent
(update [_ state]
(update state :workspace-drawing dissoc :tool :object))))
(def handle-finish-drawing
(ptk/reify ::handle-finish-drawing
ptk/WatchEvent
(watch [_ state stream]
(let [shape (get-in state [:workspace-drawing :object])]
(rx/concat
(rx/of clear-drawing)
(when (:initialized? shape)
(let [shape-click-width (case (:type shape)
:text 3
20)
shape-click-height (case (:type shape)
:text 16
20)
shape (if (:click-draw? shape)
(-> shape
(assoc-in [:modifiers :resize-vector]
(gpt/point shape-click-width shape-click-height))
(assoc-in [:modifiers :resize-origin]
(gpt/point (:x shape) (:y shape))))
shape)
shape (cond-> shape
(= (:type shape) :text) (assoc :grow-type
(if (:click-draw? shape) :auto-width :fixed)))
shape (-> shape
(gsh/transform-shape)
(dissoc :initialized? :click-draw?))]
;; Add & select the created shape to the workspace
(rx/concat
(if (= :text (:type shape))
(rx/of dwc/start-undo-transaction)
(rx/empty))
(rx/of (dws/deselect-all)
(dwc/add-shape shape))))))))))

View file

@ -0,0 +1,63 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.workspace.drawing.curve
(:require
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gsp]
[app.main.streams :as ms]
[app.util.geom.path :as path]
[app.main.data.workspace.drawing.common :as common]))
(def simplify-tolerance 0.3)
(defn stoper-event? [{:keys [type shift] :as event}]
(ms/mouse-event? event) (= type :up))
(defn initialize-drawing [state]
(assoc-in state [:workspace-drawing :object :initialized?] true))
(defn insert-point-segment [state point]
(update-in state [:workspace-drawing :object :segments] (fnil conj []) point))
(defn curve-to-path [{:keys [segments] :as shape}]
(let [content (gsp/segments->content segments)
selrect (gsh/content->selrect content)
points (gsh/rect->points selrect)]
(-> shape
(dissoc :segments)
(assoc :content content)
(assoc :selrect selrect)
(assoc :points points))))
(defn finish-drawing-curve [state]
(update-in
state [:workspace-drawing :object]
(fn [shape]
(-> shape
(update :segments #(path/simplify % simplify-tolerance))
(curve-to-path)))))
(defn handle-drawing-curve []
(ptk/reify ::handle-drawing-curve
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [flags]} (:workspace-local state)
stoper (rx/filter stoper-event? stream)
mouse (rx/sample 10 ms/mouse-position)]
(rx/concat
(rx/of initialize-drawing)
(->> mouse
(rx/map (fn [pt] #(insert-point-segment % pt)))
(rx/take-until stoper))
(rx/of finish-drawing-curve
common/handle-finish-drawing))))))

View file

@ -0,0 +1,688 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.workspace.drawing.path
(:require
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.math :as mth]
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.util.data :as ud]
[app.common.data :as cd]
[app.util.geom.path :as ugp]
[app.main.streams :as ms]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing.common :as common]
[app.common.geom.shapes.path :as gsp]))
;; CONSTANTS
(defonce enter-keycode 13)
;; PRIVATE METHODS
(defn get-path-id
"Retrieves the currently editing path id"
[state]
(or (get-in state [:workspace-local :edition])
(get-in state [:workspace-drawing :object :id])))
(defn get-path
"Retrieves the location of the path object and additionaly can pass
the arguments. This location can be used in get-in, assoc-in... functions"
[state & path]
(let [edit-id (get-in state [:workspace-local :edition])
page-id (:current-page-id state)]
(cd/concat
(if edit-id
[:workspace-data :pages-index page-id :objects edit-id]
[:workspace-drawing :object])
path)))
(defn update-selrect
"Updates the selrect and points for a path"
[shape]
(let [selrect (gsh/content->selrect (:content shape))
points (gsh/rect->points selrect)]
(assoc shape :points points :selrect selrect)))
(defn next-node
"Calculates the next-node to be inserted."
[shape position prev-point prev-handler]
(let [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 (ugp/make-curve-params position prev-handler)}
:else {:command :move-to
:params position})))
(defn append-node
"Creates a new node in the path. Usualy used when drawing."
[shape position prev-point prev-handler]
(let [command (next-node shape position prev-point prev-handler)]
(-> shape
(update :content (fnil conj []) command)
(update-selrect))))
(defn move-handler-modifiers [content index prefix match-opposite? dx dy]
(let [[cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])
[ocx ocy] (if (= prefix :c1) [:c2x :c2y] [:c1x :c1y])
opposite-index (ugp/opposite-index content index prefix)]
(cond-> {}
:always
(update index assoc cx dx cy dy)
(and match-opposite? opposite-index)
(update opposite-index assoc ocx (- dx) ocy (- dy)))))
(defn end-path-event? [{:keys [type shift] :as event}]
(or (= event ::end-path)
(= (ptk/type event) :esc-pressed)
(= event :interrupt) ;; ESC
(and (ms/keyboard-event? event)
(= type :down)
;; TODO: Enter now finish path but can finish drawing/editing as well
(= enter-keycode (:key event)))))
;; EVENTS
(defn init-path [id]
(ptk/reify ::init-path))
(defn finish-path [id]
(ptk/reify ::finish-path
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:workspace-local :edit-path id] dissoc :last-point :prev-handler :drag-handler :preview)))))
(defn preview-next-point [{:keys [x y]}]
(ptk/reify ::preview-next-point
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)
position (gpt/point x y)
shape (get-in state (get-path state))
{:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])
command (next-node shape position last-point prev-handler)]
(assoc-in state [:workspace-local :edit-path id :preview] command)))))
(defn add-node [{:keys [x y]}]
(ptk/reify ::add-node
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)
position (gpt/point x y)
{:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id])]
(-> state
(assoc-in [:workspace-local :edit-path id :last-point] position)
(update-in [:workspace-local :edit-path id] dissoc :prev-handler)
(update-in (get-path state) append-node position last-point prev-handler))))))
(defn start-drag-handler []
(ptk/reify ::start-drag-handler
ptk/UpdateEvent
(update [_ state]
(let [content (get-in state (get-path state :content))
index (dec (count content))
command (get-in state (get-path state :content index :command))
make-curve
(fn [command]
(let [params (ugp/make-curve-params
(get-in content [index :params])
(get-in content [(dec index) :params]))]
(-> command
(assoc :command :curve-to :params params))))]
(cond-> state
(= command :line-to)
(update-in (get-path state :content index) make-curve))))))
(defn drag-handler [{:keys [x y]}]
(ptk/reify ::drag-handler
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)
handler-position (gpt/point x y)
shape (get-in state (get-path state))
content (:content shape)
index (dec (count content))
node-position (ugp/command->point (nth content index))
{dx :x dy :y} (gpt/subtract handler-position node-position)
match-opposite? true
modifiers (move-handler-modifiers content (inc index) :c1 match-opposite? dx dy)]
(-> state
(assoc-in [:workspace-local :edit-path id :content-modifiers] modifiers)
(assoc-in [:workspace-local :edit-path id :prev-handler] handler-position)
(assoc-in [:workspace-local :edit-path id :drag-handler] handler-position))))))
(defn finish-drag []
(ptk/reify ::finish-drag
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)
modifiers (get-in state [:workspace-local :edit-path id :content-modifiers])
handler (get-in state [:workspace-local :edit-path id :drag-handler])]
(-> state
(update-in (get-path state :content) ugp/apply-content-modifiers modifiers)
(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 (get-path state) update-selrect))))
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-path-id state)
handler (get-in state [:workspace-local :edit-path id :prev-handler])]
;; Update the preview because can be outdated after the dragging
(rx/of (preview-next-point handler))))))
(defn close-path [position]
(ptk/reify ::close-path
ptk/WatchEvent
(watch [_ state stream]
(rx/of (add-node position)
::end-path))))
(defn close-path-drag-start [position]
(ptk/reify ::close-path-drag-start
ptk/WatchEvent
(watch [_ state stream]
(let [zoom (get-in state [:workspace-local :zoom])
threshold (/ 5 zoom)
check-if-dragging
(fn [current-position]
(let [start (gpt/point position)
current (gpt/point current-position)]
(>= (gpt/distance start current) 100)))
stop-stream
(->> stream (rx/filter #(or (end-path-event? %)
(ms/mouse-up? %))))
position-stream
(->> ms/mouse-position
(rx/take-until stop-stream)
(rx/throttle 50))
drag-events-stream
(->> position-stream
(rx/map #(drag-handler %)))]
(rx/concat
(rx/of (close-path position))
(->> position-stream
(rx/filter check-if-dragging)
(rx/take 1)
(rx/merge-map
#(rx/concat
(rx/of (start-drag-handler))
drag-events-stream
(rx/of (finish-drag))))))))))
(defn close-path-drag-end [position]
(ptk/reify ::close-path-drag-end))
(defn path-pointer-enter [position]
(ptk/reify ::path-pointer-enter))
(defn path-pointer-leave [position]
(ptk/reify ::path-pointer-leave))
(defn start-path-from-point [position]
(ptk/reify ::start-path-from-point
ptk/WatchEvent
(watch [_ state stream]
(let [mouse-up (->> stream (rx/filter #(or (end-path-event? %)
(ms/mouse-up? %))))
drag-events (->> ms/mouse-position
(rx/take-until mouse-up)
(rx/map #(drag-handler %)))]
(rx/concat (rx/of (add-node position))
(rx/of (start-drag-handler))
drag-events
(rx/of (finish-drag))))
)))
;; EVENT STREAMS
(defn make-click-stream
[stream down-event]
(->> stream
(rx/filter ms/mouse-click?)
(rx/debounce 200)
(rx/first)
(rx/map #(add-node down-event))))
(defn make-drag-stream
[stream down-event]
(let [mouse-up (->> stream (rx/filter #(or (end-path-event? %)
(ms/mouse-up? %))))
drag-events (->> ms/mouse-position
(rx/take-until mouse-up)
(rx/map #(drag-handler %)))]
(->> (rx/timer 400)
(rx/merge-map #(rx/concat
(rx/of (add-node down-event))
(rx/of (start-drag-handler))
drag-events
(rx/of (finish-drag)))))))
(defn make-dbl-click-stream
[stream down-event]
(->> stream
(rx/filter ms/mouse-double-click?)
(rx/first)
(rx/merge-map
#(rx/of (add-node down-event)
::end-path))))
(defn make-node-events-stream
[stream]
(->> (rx/merge
(->> stream (rx/filter (ptk/type? ::close-path)))
(->> stream (rx/filter (ptk/type? ::close-path-drag-start))))
(rx/take 1)
(rx/merge-map #(rx/empty))))
;; MAIN ENTRIES
(defn handle-drawing-path
[id]
(ptk/reify ::handle-drawing-path
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)]
(-> state
(assoc-in [:workspace-local :edit-path id :edit-mode] :draw))))
ptk/WatchEvent
(watch [_ state stream]
(let [mouse-down (->> stream (rx/filter ms/mouse-down?))
end-path-events (->> stream (rx/filter end-path-event?))
;; Mouse move preview
mousemove-events
(->> ms/mouse-position
(rx/take-until end-path-events)
(rx/throttle 50)
(rx/map #(preview-next-point %)))
;; From mouse down we can have: click, drag and double click
mousedown-events
(->> mouse-down
(rx/take-until end-path-events)
(rx/throttle 50)
(rx/with-latest merge ms/mouse-position)
;; We change to the stream that emits the first event
(rx/switch-map
#(rx/race (make-node-events-stream stream)
(make-click-stream stream %)
(make-drag-stream stream %)
(make-dbl-click-stream stream %))))]
(rx/concat
(rx/of (init-path id))
(rx/merge mousemove-events
mousedown-events)
(rx/of (finish-path id)))))))
(defn stop-path-edit []
(ptk/reify ::stop-path-edit
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(update state :workspace-local dissoc :edit-path id)))))
(defn start-path-edit
[id]
(ptk/reify ::start-path-edit
ptk/UpdateEvent
(update [_ state]
;; Only edit if the object has been created
(if-let [id (get-in state [:workspace-local :edition])]
(assoc-in state [:workspace-local :edit-path id] {:edit-mode :move
:selected #{}
:snap-toggled true})
state))
ptk/WatchEvent
(watch [_ state stream]
(->> stream
(rx/filter #(= % :interrupt))
(rx/take 1)
(rx/map #(stop-path-edit))))))
(defn modify-point [index prefix dx dy]
(ptk/reify ::modify-point
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])
[cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])]
(-> state
(update-in [:workspace-local :edit-path id :content-modifiers (inc index)] assoc
:c1x dx :c1y dy)
(update-in [:workspace-local :edit-path id :content-modifiers index] assoc
:x dx :y dy :c2x dx :c2y dy)
)))))
(defn modify-handler [id index prefix dx dy match-opposite?]
(ptk/reify ::modify-point
ptk/UpdateEvent
(update [_ state]
(let [content (get-in state (get-path state :content))
[cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])
[ocx ocy] (if (= prefix :c1) [:c2x :c2y] [:c1x :c1y])
opposite-index (ugp/opposite-index content index prefix)]
(cond-> state
:always
(update-in [:workspace-local :edit-path id :content-modifiers index] assoc
cx dx cy dy)
(and match-opposite? opposite-index)
(update-in [:workspace-local :edit-path id :content-modifiers opposite-index] assoc
ocx (- dx) ocy (- dy)))))))
(defn apply-content-modifiers []
(ptk/reify ::apply-content-modifiers
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:workspace-local :edition])
page-id (:current-page-id state)
shape (get-in state [:workspace-data :pages-index page-id :objects id])
{old-content :content old-selrect :selrect old-points :points} shape
content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {})
new-content (ugp/apply-content-modifiers old-content content-modifiers)
new-selrect (gsh/content->selrect new-content)
new-points (gsh/rect->points new-selrect)
rch [{:type :mod-obj
:id id
:page-id page-id
:operations [{:type :set :attr :content :val new-content}
{:type :set :attr :selrect :val new-selrect}
{:type :set :attr :points :val new-points}]}
{:type :reg-objects
:page-id page-id
:shapes [id]}]
uch [{:type :mod-obj
:id id
:page-id page-id
:operations [{:type :set :attr :content :val old-content}
{:type :set :attr :selrect :val old-selrect}
{:type :set :attr :points :val old-points}]}
{:type :reg-objects
:page-id page-id
:shapes [id]}]]
(rx/of (dwc/commit-changes rch uch {:commit-local? true})
(fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers)))))))
(defn save-path-content []
(ptk/reify ::save-path-content
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:workspace-local :edition])
page-id (:current-page-id state)
old-content (get-in state [:workspace-local :edit-path id :old-content])
old-selrect (gsh/content->selrect old-content)
old-points (gsh/rect->points old-content)
shape (get-in state [:workspace-data :pages-index page-id :objects id])
{new-content :content new-selrect :selrect new-points :points} shape
rch [{:type :mod-obj
:id id
:page-id page-id
:operations [{:type :set :attr :content :val new-content}
{:type :set :attr :selrect :val new-selrect}
{:type :set :attr :points :val new-points}]}
{:type :reg-objects
:page-id page-id
:shapes [id]}]
uch [{:type :mod-obj
:id id
:page-id page-id
:operations [{:type :set :attr :content :val old-content}
{:type :set :attr :selrect :val old-selrect}
{:type :set :attr :points :val old-points}]}
{:type :reg-objects
:page-id page-id
:shapes [id]}]]
(rx/of (dwc/commit-changes rch uch {:commit-local? true}))))))
(declare start-draw-mode)
(defn check-changed-content []
(ptk/reify ::check-changed-content
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-path-id state)
content (get-in state (get-path state :content))
old-content (get-in state [:workspace-local :edit-path id :old-content])
mode (get-in state [:workspace-local :edit-path id :edit-mode])]
(cond
(not= content old-content) (rx/of (save-path-content)
(start-draw-mode))
(= mode :draw) (rx/of :interrupt)
:else (rx/of (finish-path id)))))))
(defn move-path-point [start-point end-point]
(ptk/reify ::move-point
ptk/UpdateEvent
(update [_ state]
(let [id (get-path-id state)
content (get-in state (get-path state :content))
{dx :x dy :y} (gpt/subtract end-point start-point)
handler-indices (-> (ugp/content->handlers content)
(get start-point))
command-for-point (fn [[index command]]
(let [point (ugp/command->point command)]
(= point start-point)))
point-indices (->> (d/enumerate content)
(filter command-for-point)
(map first))
point-reducer (fn [modifiers index]
(-> modifiers
(assoc-in [index :x] dx)
(assoc-in [index :y] dy)))
handler-reducer (fn [modifiers [index prefix]]
(let [cx (ud/prefix-keyword prefix :x)
cy (ud/prefix-keyword prefix :y)]
(-> modifiers
(assoc-in [index cx] dx)
(assoc-in [index cy] dy))))
modifiers (as-> (get-in state [:workspace-local :edit-path id :content-modifiers] {}) $
(reduce point-reducer $ point-indices)
(reduce handler-reducer $ handler-indices))]
(assoc-in state [:workspace-local :edit-path id :content-modifiers] modifiers)))))
(defn start-move-path-point
[position]
(ptk/reify ::start-move-path-point
ptk/WatchEvent
;; TODO REWRITE
(watch [_ state stream]
(let [stopper (->> stream (rx/filter ms/mouse-up?))]
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-path-point position %)))
(rx/of (apply-content-modifiers)))))))
(defn start-move-handler
[index prefix]
(ptk/reify ::start-move-handler
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:workspace-local :edition])
[cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])
start-point @ms/mouse-position
start-delta-x (get-in state [:workspace-local :edit-path id :content-modifiers index cx] 0)
start-delta-y (get-in state [:workspace-local :edit-path id :content-modifiers index cy] 0)]
(rx/concat
(->> ms/mouse-position
(rx/take-until (->> stream (rx/filter ms/mouse-up?)))
(rx/with-latest vector ms/mouse-position-alt)
(rx/map
(fn [[pos alt?]]
(modify-handler
id
index
prefix
(+ start-delta-x (- (:x pos) (:x start-point)))
(+ start-delta-y (- (:y pos) (:y start-point)))
(not alt?))))
)
(rx/concat (rx/of (apply-content-modifiers))))))))
(defn start-draw-mode []
(ptk/reify ::start-draw-mode
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])
page-id (:current-page-id state)
old-content (get-in state [:workspace-data :pages-index page-id :objects id :content])]
(-> state
(assoc-in [:workspace-local :edit-path id :old-content] old-content))))
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:workspace-local :edition])
edit-mode (get-in state [:workspace-local :edit-path id :edit-mode])]
(if (= :draw edit-mode)
(rx/concat
(rx/of (handle-drawing-path id))
(->> stream
(rx/filter (ptk/type? ::finish-path))
(rx/take 1)
(rx/merge-map #(rx/of (check-changed-content)))))
(rx/empty))))))
(defn change-edit-mode [mode]
(ptk/reify ::change-edit-mode
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(cond-> state
id (assoc-in [:workspace-local :edit-path id :edit-mode] mode))))
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-path-id state)]
(cond
(and id (= :move mode)) (rx/of ::end-path)
(and id (= :draw mode)) (rx/of (start-draw-mode))
:else (rx/empty))))))
(defn select-handler [index type]
(ptk/reify ::select-handler
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(-> state
(update-in [:workspace-local :edit-path id :selected] (fnil conj #{}) [index type]))))))
(defn select-node [position]
(ptk/reify ::select-node
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(-> state
(update-in [:workspace-local :edit-path id :selected-node] (fnil conj #{}) position))))))
(defn deselect-node [position]
(ptk/reify ::deselect-node
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]
(-> state
(update-in [:workspace-local :edit-path id :selected-node] (fnil disj #{}) position))))))
(defn add-to-selection-handler [index type]
(ptk/reify ::add-to-selection-handler
ptk/UpdateEvent
(update [_ state]
state)))
(defn add-to-selection-node [index]
(ptk/reify ::add-to-selection-node
ptk/UpdateEvent
(update [_ state]
state)))
(defn remove-from-selection-handler [index]
(ptk/reify ::remove-from-selection-handler
ptk/UpdateEvent
(update [_ state]
state)))
(defn remove-from-selection-node [index]
(ptk/reify ::remove-from-selection-handler
ptk/UpdateEvent
(update [_ state]
state)))
(defn handle-new-shape-result [shape-id]
(ptk/reify ::handle-new-shape-result
ptk/UpdateEvent
(update [_ state]
(let [content (get-in state [:workspace-drawing :object :content] [])]
(if (> (count content) 1)
(assoc-in state [:workspace-drawing :object :initialized?] true)
state)))
ptk/WatchEvent
(watch [_ state stream]
(->> (rx/of common/handle-finish-drawing
(dwc/start-edition-mode shape-id)
(start-path-edit shape-id)
(change-edit-mode :draw))))))
(defn handle-new-shape
"Creates a new path shape"
[]
(ptk/reify ::handle-new-shape
ptk/WatchEvent
(watch [_ state stream]
(let [shape-id (get-in state [:workspace-drawing :object :id])]
(rx/concat
(rx/of (handle-drawing-path shape-id))
(->> stream
(rx/filter (ptk/type? ::finish-path))
(rx/take 1)
(rx/observe-on :async)
(rx/map #(handle-new-shape-result shape-id))))))))

View file

@ -251,7 +251,7 @@
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id group))))))))))
(dwc/select-shapes (d/ordered-set (:id group))))))))))
(defn rename-component
[id new-name]
@ -407,7 +407,7 @@
new-shapes)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set (:id new-shape))))))))
(dwc/select-shapes (d/ordered-set (:id new-shape))))))))
(defn detach-component
"Remove all references to components in the shape with the given id,

View file

@ -17,6 +17,7 @@
[goog.object :as gobj]
[potok.core :as ptk]
[app.common.geom.shapes :as geom]
[app.common.attrs :as attrs]
[app.main.data.workspace.common :as dwc]
[app.main.fonts :as fonts]
[app.util.object :as obj]
@ -125,7 +126,7 @@
(map #(if (is-text-node? %)
(merge ut/default-text-attrs %)
%)))]
(geom/get-attrs-multi nodes attrs)))
(attrs/get-attrs-multi nodes attrs)))
(defn current-text-values
[{:keys [editor default attrs shape]}]

View file

@ -80,10 +80,11 @@
(defn start-resize
[handler initial ids shape]
(letfn [(resize [shape initial resizing-shapes [point lock? point-snap]]
(let [{:keys [width height rotation]} shape
(let [{:keys [width height]} (:selrect shape)
{:keys [rotation]} shape
shapev (-> (gpt/point width height))
rotation (if (#{:curve :path} (:type shape)) 0 rotation)
rotation (if (= :path (:type shape)) 0 rotation)
;; Vector modifiers depending on the handler
handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y))
@ -101,9 +102,11 @@
shape-transform (:transform shape (gmt/matrix))
shape-transform-inverse (:transform-inverse shape (gmt/matrix))
shape-center (gsh/center-shape shape)
;; Resize origin point given the selected handler
origin (-> (handler-resize-origin shape handler)
(gsh/transform-shape-point shape shape-transform))]
origin (-> (handler-resize-origin (:selrect shape) handler)
(gsh/transform-point-center shape-center shape-transform))]
(rx/of (set-modifiers ids
{:resize-vector scalev
@ -170,7 +173,7 @@
(watch [_ state stream]
(let [stoper (rx/filter ms/mouse-up? stream)
group (gsh/selection-rect shapes)
group-center (gsh/center group)
group-center (gsh/center-selrect group)
initial-angle (gpt/angle @ms/mouse-position group-center)
calculate-angle (fn [pos ctrl?]
(let [angle (- (gpt/angle pos group-center) initial-angle)
@ -403,7 +406,7 @@
#(reduce update-shape % ids-with-children)))))))
(defn rotation-modifiers [center shape angle]
(let [displacement (let [shape-center (gsh/center shape)]
(let [displacement (let [shape-center (gsh/center-shape shape)]
(-> (gmt/matrix)
(gmt/rotate angle center)
(gmt/rotate (- angle) shape-center)))]
@ -416,7 +419,7 @@
(defn set-rotation
([delta-rotation shapes]
(set-rotation delta-rotation shapes (-> shapes gsh/selection-rect gsh/center)))
(set-rotation delta-rotation shapes (-> shapes gsh/selection-rect gsh/center-selrect)))
([delta-rotation shapes center]
(letfn [(rotate-shape [objects angle shape center]

View file

@ -15,7 +15,8 @@
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.common.math :as mth]
[app.common.geom.shapes :as geom]
[app.common.geom.shapes :as gsh]
[app.common.geom.align :as gal]
[app.common.geom.point :as gpt]
[app.common.geom.matrix :as gmt]
[app.main.ui.shapes.filters :as filters]
@ -42,9 +43,9 @@
(defn- calculate-dimensions
[{:keys [objects] :as data} vport]
(let [shapes (cph/select-toplevel-shapes objects {:include-frames? true})]
(->> (geom/selection-rect shapes)
(geom/adjust-to-viewport vport)
(geom/fix-invalid-rect-values))))
(->> (gsh/selection-rect shapes)
(gal/adjust-to-viewport vport)
#_(gsh/fix-invalid-rect-values))))
(declare shape-wrapper-factory)
@ -55,7 +56,7 @@
(mf/fnc frame-wrapper
[{:keys [shape] :as props}]
(let [childs (mapv #(get objects %) (:shapes shape))
shape (geom/transform-shape shape)]
shape (gsh/transform-shape shape)]
[:> shape-container {:shape shape}
[:& frame-shape {:shape shape :childs childs}]]))))
@ -78,11 +79,11 @@
(let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects))
frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))]
(when (and shape (not (:hidden shape)))
(let [shape (geom/transform-shape frame shape)
(let [shape (-> (gsh/transform-shape shape)
(gsh/translate-to-frame frame))
opts #js {:shape shape}]
[:> shape-container {:shape shape}
(case (:type shape)
:curve [:> path/path-shape opts]
:text [:> text/text-shape opts]
:rect [:> rect/rect-shape opts]
:path [:> path/path-shape opts]

View file

@ -166,7 +166,7 @@
(rx/merge-map
(fn [[frame selrect]]
(let [areas (->> (gsh/selrect->areas (or (:selrect frame)
(gsh/rect->rect-shape @refs/vbox)) selrect)
(gsh/rect->selrect @refs/vbox)) selrect)
(d/mapm #(select-shapes-area page-id shapes objects %2)))
snap-x (search-snap-distance selrect :x (:left areas) (:right areas))
snap-y (search-snap-distance selrect :y (:top areas) (:bottom areas))]
@ -195,7 +195,7 @@
(or (filter-shapes id)
(not (contains? layout :dynamic-alignment)))))
shape (if (> (count shapes) 1)
(->> shapes (map gsh/transform-shape) gsh/selection-rect)
(->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect}))
(->> shapes (first)))
shapes-points (->> shape

View file

@ -41,11 +41,10 @@
(when *assert*
(defonce debug-subscription
(as-> stream $
#_(rx/filter ptk/event? $)
(rx/filter (fn [s] (debug? :events)) $)
(rx/subscribe $ (fn [event]
(println "[stream]: " (repr-event event)))))))
(->> stream
(rx/filter ptk/event?)
(rx/filter (fn [s] (debug? :events)))
(rx/subs #(println "[stream]: " (repr-event %))))))
(defn emit!
([] nil)
([event]
@ -73,6 +72,11 @@
(defn ^:export dump-state []
(logjs "state" @state))
(defn ^:export get-state [str-path]
(let [path (->> (str/split str-path " ")
(map d/read-string))]
(clj->js (get-in @state path))))
(defn ^:export dump-objects []
(let [page-id (get @state :current-page-id)]
(logjs "state" (get-in @state [:workspace-data :pages-index page-id :objects]))))

View file

@ -26,6 +26,11 @@
[v]
(instance? MouseEvent v))
(defn mouse-down?
[v]
(and (mouse-event? v)
(= :down (:type v))))
(defn mouse-up?
[v]
(and (mouse-event? v)
@ -36,6 +41,11 @@
(and (mouse-event? v)
(= :click (:type v))))
(defn mouse-double-click?
[v]
(and (mouse-event? v)
(= :double-click (:type v))))
(defrecord PointerEvent [source pt ctrl shift alt])
(defn pointer-event?

View file

@ -19,6 +19,7 @@
(def default-hotspot-x 12)
(def default-hotspot-y 12)
(def default-rotation 0)
(def default-height 20)
(defn parse-svg [svg-data]
(-> svg-data
@ -53,25 +54,27 @@
(str/replace #"\s+$" "")))
(defn encode-svg-cursor
[id rotation x y]
[id rotation x y height]
(let [svg-path (str cursor-folder "/" (name id) ".svg")
data (-> svg-path io/resource slurp parse-svg uri/percent-encode)
transform (if rotation (str " transform='rotate(" rotation ")'") "")
data (clojure.pprint/cl-format
nil
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='20px' height='20px'~A%3E~A%3C/svg%3E\") ~A ~A, auto"
transform data x y)]
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='20px' height='~Apx'~A%3E~A%3C/svg%3E\") ~A ~A, auto"
height transform data x y )]
data))
(defmacro cursor-ref
"Creates a static cursor given its name, rotation and x/y hotspot"
([id] (encode-svg-cursor id default-rotation default-hotspot-x default-hotspot-y))
([id rotation] (encode-svg-cursor id rotation default-hotspot-x default-hotspot-y))
([id rotation x y] (encode-svg-cursor id rotation x y)))
([id] (encode-svg-cursor id default-rotation default-hotspot-x default-hotspot-y default-height))
([id rotation] (encode-svg-cursor id rotation default-hotspot-x default-hotspot-y default-height))
([id rotation x y] (encode-svg-cursor id rotation x y default-height))
([id rotation x y height] (encode-svg-cursor id rotation x y height))
)
(defmacro cursor-fn
"Creates a dynamic cursor that can be rotated in runtime"
[id initial]
(let [cursor (encode-svg-cursor id "{{rotation}}" default-hotspot-x default-hotspot-y)]
(let [cursor (encode-svg-cursor id "{{rotation}}" default-hotspot-x default-hotspot-y default-height)]
`(fn [rot#]
(str/replace ~cursor "{{rotation}}" (+ ~initial rot#)))))

View file

@ -8,8 +8,7 @@
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.cursors
(:require-macros [app.main.ui.cursors :refer [cursor-ref
cursor-fn]])
(:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn]])
(:require [rumext.alpha :as mf]
[cuerdas.core :as str]
[app.util.timers :as ts]))
@ -33,6 +32,10 @@
(def rotate (cursor-fn :rotate 90))
(def text (cursor-ref :text))
(def picker (cursor-ref :picker 0 0 24))
(def pointer-node (cursor-ref :pointer-node 0 0 10 32))
(def pointer-move (cursor-ref :pointer-move 0 0 10 42))
(def pen-node (cursor-ref :pen-node 0 0 10 36))
(def comments (cursor-ref :comments 0 2 20))
(mf/defc debug-preview
{::mf/wrap-props false}
@ -49,7 +52,9 @@
[:div {:style {:width "100px"
:height "100px"
:background-image (-> value (str/replace #"(url\(.*\)).*" "$1"))
:background-size "cover"
:background-size "contain"
:background-repeat "no-repeat"
:background-position "center"
:cursor value}}]
[:span {:style {:white-space "nowrap"

View file

@ -28,7 +28,6 @@
:rect [:layout :fill :stroke :shadow :blur]
:circle [:layout :fill :stroke :shadow :blur]
:path [:layout :fill :stroke :shadow :blur]
:curve [:layout :fill :stroke :shadow :blur]
:image [:image :layout :shadow :blur]
:text [:layout :text :shadow :blur]})

View file

@ -122,11 +122,11 @@
(mf/deps objects)
#(group-container-factory objects))]
(when (and shape (not (:hidden shape)))
(let [shape (geom/transform-shape frame shape)
(let [shape (-> (geom/transform-shape shape)
(geom/translate-to-frame frame))
opts #js {:shape shape
:frame frame}]
(case (:type shape)
:curve [:> path-wrapper opts]
:text [:> text-wrapper opts]
:rect [:> rect-wrapper opts]
:path [:> path-wrapper opts]

View file

@ -128,6 +128,16 @@
(def checkbox-checked (icon-xref :checkbox-checked))
(def checkbox-unchecked (icon-xref :checkbox-unchecked))
(def code (icon-xref :code))
(def nodes-add (icon-xref :nodes-add))
(def nodes-corner (icon-xref :nodes-corner))
(def nodes-curve (icon-xref :nodes-curve))
(def nodes-join (icon-xref :nodes-join))
(def nodes-merge (icon-xref :nodes-merge))
(def nodes-remove (icon-xref :nodes-remove))
(def nodes-separate (icon-xref :nodes-separate))
(def nodes-snap (icon-xref :nodes-snap))
(def pen (icon-xref :pen))
(def pointer-inner (icon-xref :pointer-inner))
(def loader-pencil
(mf/html

View file

@ -23,7 +23,8 @@
(let [shape (unchecked-get props "shape")
base-props (unchecked-get props "base-props")
elem-name (unchecked-get props "elem-name")
{:keys [x y width height]} (geom/shape->rect-shape shape)
;; {:keys [x y width height]} (geom/shape->rect-shape shape)
{:keys [x y width height]} (:selrect shape)
mask-id (mf/use-ctx mask-id-ctx)
stroke-id (mf/use-var (uuid/next))
stroke-style (:stroke-style shape :none)

View file

@ -15,40 +15,21 @@
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.group :refer [mask-id-ctx]]
[app.common.geom.shapes :as geom]
[app.util.object :as obj]))
[app.util.object :as obj]
[app.util.geom.path :as ugp]))
;; --- Path Shape
(defn- render-path
[{:keys [segments close?] :as shape}]
(let [numsegs (count segments)]
(loop [buffer []
index 0]
(cond
(>= index numsegs)
(if close?
(str/join " " (conj buffer "Z"))
(str/join " " buffer))
(zero? index)
(let [{:keys [x y] :as segment} (nth segments index)
buffer (conj buffer (str/istr "M~{x},~{y}"))]
(recur buffer (inc index)))
:else
(let [{:keys [x y] :as segment} (nth segments index)
buffer (conj buffer (str/istr "L~{x},~{y}"))]
(recur buffer (inc index)))))))
(mf/defc path-shape
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
background? (unchecked-get props "background?")
{:keys [id x y width height]} (geom/shape->rect-shape shape)
;; {:keys [id x y width height]} (geom/shape->rect-shape shape)
{:keys [id x y width height]} (:selrect shape)
mask-id (mf/use-ctx mask-id-ctx)
transform (geom/transform-matrix shape)
pdata (render-path shape)
pdata (ugp/content->path (:content shape))
props (-> (attrs/extract-style-attrs shape)
(obj/merge!
#js {:transform transform

View file

@ -149,10 +149,10 @@
shape (unchecked-get props "shape")
frame (unchecked-get props "frame")]
(when (and shape (not (:hidden shape)))
(let [shape (geom/transform-shape frame shape)
(let [shape (-> (geom/transform-shape shape)
(geom/translate-to-frame frame))
opts #js {:shape shape}]
(case (:type shape)
:curve [:> path-wrapper opts]
:text [:> text-wrapper opts]
:rect [:> rect-wrapper opts]
:path [:> path-wrapper opts]

View file

@ -29,7 +29,7 @@
[app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]]
[app.main.ui.workspace.scroll :as scroll]
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
[app.main.ui.workspace.viewport :refer [viewport coordinates]]
[app.main.ui.workspace.viewport :refer [viewport viewport-actions coordinates]]
[app.util.dom :as dom]
[beicon.core :as rx]
[cuerdas.core :as str]
@ -65,6 +65,7 @@
(when (contains? layout :rules)
[:& workspace-rules {:local local}])
[:& viewport-actions]
[:& viewport {:file file
:local local
:layout layout}]]]

View file

@ -12,6 +12,7 @@
[app.main.data.workspace.drawing :as dd]
[app.main.store :as st]
[app.main.ui.workspace.shapes :as shapes]
[app.main.ui.workspace.shapes.path :refer [path-editor]]
[app.common.geom.shapes :as gsh]
[app.common.data :as d]
[app.util.dom :as dom]
@ -22,10 +23,13 @@
(mf/defc draw-area
[{:keys [shape zoom] :as props}]
(when (:id shape)
(case (:type shape)
(:path :curve) [:& path-draw-area {:shape shape}]
[:& generic-draw-area {:shape shape :zoom zoom}])))
[:g.draw-area
[:& shapes/shape-wrapper {:shape shape}]
(case (:type shape)
:path [:& path-editor {:shape shape :zoom zoom}]
#_:default [:& generic-draw-area {:shape shape :zoom zoom}])])
(mf/defc generic-draw-area
[{:keys [shape zoom]}]
@ -34,43 +38,10 @@
(not (d/nan? x))
(not (d/nan? y)))
[:g
[:& shapes/shape-wrapper {:shape shape}]
[:rect.main {:x x :y y
:width width
:height height
:style {:stroke "#1FDEA7"
:fill "transparent"
:stroke-width (/ 1 zoom)}}]])))
[:rect.main {:x x :y y
:width width
:height height
:style {:stroke "#1FDEA7"
:fill "transparent"
:stroke-width (/ 1 zoom)}}])))
(mf/defc path-draw-area
[{:keys [shape] :as props}]
(let [locale (i18n/use-locale)
on-click
(fn [event]
(dom/stop-propagation event)
(st/emit! (dw/assign-cursor-tooltip nil)
dd/close-drawing-path
:path/end-path-drawing))
on-mouse-enter
(fn [event]
(let [msg (t locale "workspace.viewport.click-to-close-path")]
(st/emit! (dw/assign-cursor-tooltip msg))))
on-mouse-leave
(fn [event]
(st/emit! (dw/assign-cursor-tooltip nil)))]
(when-let [{:keys [x y] :as segment} (first (:segments shape))]
[:g
[:& shapes/shape-wrapper {:shape shape}]
(when (not= :curve (:type shape))
[:circle.close-bezier
{:cx x
:cy y
:r 5
:on-click on-click
:on-mouse-enter on-mouse-enter
:on-mouse-leave on-mouse-leave}])])))

View file

@ -31,7 +31,8 @@
[app.common.geom.matrix :as gmt]
[app.util.debug :refer [debug?]]
[app.main.ui.workspace.shapes.outline :refer [outline]]
[app.main.ui.measurements :as msr]))
[app.main.ui.measurements :as msr]
[app.main.ui.workspace.shapes.path :refer [path-editor]]))
(def rotation-handler-size 25)
(def resize-point-radius 4)
@ -181,7 +182,7 @@
on-rotate (obj/get props "on-rotate")
current-transform (mf/deref refs/current-transform)
selrect (geom/shape->rect-shape shape)
selrect (:selrect shape)
transform (geom/transform-matrix shape)
tr-shape (geom/transform-shape shape)]
@ -214,44 +215,6 @@
:resize-side [:> resize-side-handler props])))])))
;; --- Selection Handlers (Component)
(mf/defc path-edition-selection-handlers
[{:keys [shape modifiers zoom color] :as props}]
(letfn [(on-mouse-down [event index]
(dom/stop-propagation event)
;; TODO: this need code ux refactor
(let [stoper (get-edition-stream-stoper)
stream (->> (ms/mouse-position-deltas @ms/mouse-position)
(rx/take-until stoper))]
;; (when @refs/selected-alignment
;; (st/emit! (dw/initial-path-point-align (:id shape) index)))
(rx/subscribe stream #(on-handler-move % index))))
(get-edition-stream-stoper []
(let [stoper? #(and (ms/mouse-event? %) (= (:type %) :up))]
(rx/merge
(rx/filter stoper? st/stream)
(->> st/stream
(rx/filter #(= % :interrupt))
(rx/take 1)))))
(on-handler-move [delta index]
(st/emit! (dw/update-path (:id shape) index delta)))]
(let [transform (geom/transform-matrix shape)
displacement (:displacement modifiers)
segments (cond->> (:segments shape)
displacement (map #(gpt/transform % displacement)))]
[:g.controls
(for [[index {:keys [x y]}] (map-indexed vector segments)]
(let [{:keys [x y]} (gpt/transform (gpt/point x y) transform)]
[:circle {:cx x :cy y
:r (/ 6.0 zoom)
:key index
:on-mouse-down #(on-mouse-down % index)
:fill "#ffffff"
:stroke color
:style {:cursor cur/move-pointer}}]))])))
;; TODO: add specs for clarity
(mf/defc text-edition-selection-handlers
@ -269,8 +232,8 @@
(mf/defc multiple-selection-handlers
[{:keys [shapes selected zoom color show-distances] :as props}]
(let [shape (geom/selection-rect shapes)
shape-center (geom/center shape)
(let [shape (geom/setup {:type :rect} (geom/selection-rect (->> shapes (map geom/transform-shape))))
shape-center (geom/center-shape shape)
hover-id (-> (mf/deref refs/current-hover) first)
hover-id (when-not (d/seek #(= hover-id (:id %)) shapes) hover-id)
@ -314,7 +277,7 @@
hover-id (when-not (= shape-id hover-id) hover-id)
hover-shape (mf/deref (refs/object-by-id hover-id))
shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape)
shape' (if (debug? :simple-selection) (geom/setup {:type :rect} (geom/selection-rect [shape])) shape)
on-resize (fn [current-position initial-position event]
(dom/stop-propagation event)
(st/emit! (dw/start-resize current-position initial-position #{shape-id} shape')))
@ -322,7 +285,6 @@
on-rotate
#(do (dom/stop-propagation %)
(st/emit! (dw/start-rotate [shape])))]
[:*
[:& controls {:shape shape'
:zoom zoom
@ -366,12 +328,11 @@
[:& text-edition-selection-handlers {:shape shape
:zoom zoom
:color color}]
(and (or (= type :path)
(= type :curve))
(and (= type :path)
(= edition (:id shape)))
[:& path-edition-selection-handlers {:shape shape
:zoom zoom
:color color}]
[:& path-editor {:zoom zoom
:shape shape}]
:else
[:& single-selection-handlers {:shape shape

View file

@ -82,7 +82,8 @@
(let [shape (unchecked-get props "shape")
frame (unchecked-get props "frame")
ghost? (unchecked-get props "ghost?")
shape (geom/transform-shape frame shape)
shape (-> (geom/transform-shape shape)
(geom/translate-to-frame frame))
opts #js {:shape shape
:frame frame}
alt? (mf/use-state false)
@ -107,7 +108,6 @@
:on-mouse-leave on-mouse-leave
:style {:cursor (if @alt? cur/duplicate nil)}}
(case (:type shape)
:curve [:> path/path-wrapper opts]
:path [:> path/path-wrapper opts]
:text [:> text/text-wrapper opts]
:group [:> group-wrapper opts]

View file

@ -42,7 +42,7 @@
(let [shape (unchecked-get props "shape")
frame (unchecked-get props "frame")
selrect (-> shape :selrect)
shape-center (geom/center shape)
shape-center (geom/center-shape shape)
line-color (rdcolor #js {:seed (str (:id shape))})
zoom (mf/deref refs/selected-zoom)]
[:g.bounding-box

View file

@ -23,6 +23,7 @@
(defn- on-mouse-down
[event {:keys [id type] :as shape}]
(let [selected @refs/selected-shapes
edition @refs/selected-edition
selected? (contains? selected id)
drawing? @refs/selected-drawing-tool
button (.-which (.-nativeEvent event))]
@ -35,9 +36,8 @@
nil
(= type :frame)
(when selected?
(dom/stop-propagation event)
(st/emit! (dw/start-move-selected)))
(do (dom/stop-propagation event)
(st/emit! (dw/start-move-selected)))
:else
(do
@ -50,7 +50,8 @@
(st/emit! (dw/deselect-all)))
(st/emit! (dw/select-shape id))))
(st/emit! (dw/start-move-selected)))))))
(when (not= edition id)
(st/emit! (dw/start-move-selected))))))))
(defn on-context-menu
[event shape]

View file

@ -131,7 +131,7 @@
(not (:hidden shape)))
[:g {:class (when selected? "selected")
:on-context-menu on-context-menu
:on-double-click on-double-click
;; :on-double-click on-double-click
:on-mouse-down on-mouse-down}
[:& frame-title {:frame shape

View file

@ -13,8 +13,8 @@
[app.common.geom.shapes :as gsh]
[app.util.object :as obj]
[rumext.util :refer [map->obj]]
[app.main.ui.shapes.path :as path]
[app.main.refs :as refs]))
[app.main.refs :as refs]
[app.util.geom.path :as ugp]))
(mf/defc outline
@ -28,7 +28,7 @@
outline-type (case (:type shape)
:circle "ellipse"
(:curve :path) "path"
:path "path"
"rect")
common {:fill "transparent"
@ -44,8 +44,8 @@
:rx (/ width 2)
:ry (/ height 2)}
(:curve :path)
{:d (path/render-path shape)}
:path
{:d (ugp/content->path (:content shape))}
{:x x
:y y

View file

@ -10,46 +10,290 @@
(ns app.main.ui.workspace.shapes.path
(:require
[rumext.alpha :as mf]
[app.common.data :as d]
[okulary.core :as l]
[app.util.data :as d]
[app.util.dom :as dom]
[app.util.timers :as ts]
[app.main.refs :as refs]
[app.main.streams :as ms]
[app.main.constants :as c]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.data.workspace :as dw]
[app.main.data.workspace.drawing :as dr]
[app.main.data.workspace.drawing.path :as drp]
[app.main.ui.keyboard :as kbd]
[app.main.ui.shapes.path :as path]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.workspace.shapes.common :as common]))
[app.main.ui.workspace.shapes.common :as common]
[app.util.geom.path :as ugp]
[app.common.geom.point :as gpt]
[app.main.ui.cursors :as cur]
[app.main.ui.icons :as i]))
(def primary-color "#1FDEA7")
(def secondary-color "#DB00FF")
(def black-color "#000000")
(def white-color "#FFFFFF")
(def gray-color "#B1B2B5")
(def current-edit-path-ref
(let [selfn (fn [local]
(let [id (:edition local)]
(get-in local [:edit-path id])))]
(l/derived selfn refs/workspace-local)))
(defn make-edit-path-ref [id]
(mf/use-memo
(mf/deps id)
(let [selfn #(get-in % [:edit-path id])]
#(l/derived selfn refs/workspace-local))))
(defn make-content-modifiers-ref [id]
(mf/use-memo
(mf/deps id)
(let [selfn #(get-in % [:edit-path id :content-modifiers])]
#(l/derived selfn refs/workspace-local))))
(mf/defc path-wrapper
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
hover? (or (mf/deref refs/current-hover) #{})
on-mouse-down (mf/use-callback
(mf/deps shape)
#(common/on-mouse-down % shape))
on-context-menu (mf/use-callback
(mf/deps shape)
#(common/on-context-menu % shape))
on-double-click (mf/use-callback
(mf/deps shape)
(fn [event]
(when (and (not (::dr/initialized? shape)) (hover? (:id shape)))
(when (not (::dr/initialized? shape))
(do
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dw/start-edition-mode (:id shape)))))))]
(st/emit! (dw/start-edition-mode (:id shape))
(dw/start-path-edit (:id shape)))))))
content-modifiers-ref (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))
shape (update shape :content ugp/apply-content-modifiers content-modifiers)]
[:> shape-container {:shape shape
:pointer-events (when editing? "none")
:on-double-click on-double-click
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
[:& path/path-shape {:shape shape
:background? true}]]))
(mf/defc path-actions [{:keys [shape]}]
(let [id (mf/deref refs/selected-edition)
{:keys [edit-mode selected snap-toggled] :as all} (mf/deref current-edit-path-ref)]
[:div.path-actions
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when (= edit-mode :draw) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :draw))} i/pen]
[:div.viewport-actions-entry {:class (when (= edit-mode :move) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :move))} i/pointer-inner]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-add]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-remove]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-merge]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-join]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-separate]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-corner]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-curve]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when snap-toggled "is-toggled")} i/nodes-snap]]]))
(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path?]}]
(let [{:keys [x y]} position
on-enter
(fn [event]
(st/emit! (drp/path-pointer-enter position)))
on-leave
(fn [event]
(st/emit! (drp/path-pointer-leave position)))
on-click
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(cond
(and (= edit-mode :move) (not selected?))
(st/emit! (drp/select-node position))
(and (= edit-mode :move) selected?)
(st/emit! (drp/deselect-node position))))
on-mouse-down
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(st/emit! (drp/start-move-path-point position))
(and (= edit-mode :draw) start-path?)
(st/emit! (drp/start-path-from-point position))
(and (= edit-mode :draw) (not start-path?))
(st/emit! (drp/close-path-drag-start position))))]
[:g.path-point
[:circle.path-point
{:cx x
:cy y
:r (/ 3 zoom)
:style {:cursor (when (= edit-mode :draw) cur/pen-node)
:stroke-width (/ 1 zoom)
:stroke (cond (or selected? hover?) black-color
preview? secondary-color
:else primary-color)
:fill (cond selected? primary-color
:else white-color)}}]
[:circle {:cx x
:cy y
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:style {:fill "transparent"}}]]))
(mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode]}]
(when (and point handler)
(let [{:keys [x y]} handler
on-click
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(drp/select-handler index prefix)))
on-mouse-down
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(st/emit! (drp/start-move-handler index prefix))))]
[:g.handler {:pointer-events (when (= edit-mode :draw))}
[:line
{:x1 (:x point)
:y1 (:y point)
:x2 x
:y2 y
:style {:stroke gray-color
:stroke-width (/ 1 zoom)}}]
[:rect
{:x (- x (/ 3 zoom))
:y (- y (/ 3 zoom))
:width (/ 6 zoom)
:height (/ 6 zoom)
:style {:cursor cur/pointer-move
:stroke-width (/ 1 zoom)
:stroke (cond (or selected? hover?) black-color
:else primary-color)
:fill (cond selected? primary-color
:else white-color)}}]
[:circle {:cx x
:cy y
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:style {:fill "transparent"}}]])))
(mf/defc path-preview [{:keys [zoom command from]}]
[:g.preview {:style {:pointer-events "none"}}
(when (not= :move-to (:command command))
[:path {:style {:fill "transparent"
:stroke secondary-color
:stroke-width (/ 1 zoom)}
:d (ugp/content->path [{:command :move-to
:params {:x (:x from)
:y (:y from)}}
command])}])
[:& path-point {:position (:params command)
:preview? true
:zoom zoom}]])
(mf/defc path-editor
[{:keys [shape zoom]}]
(let [edit-path-ref (make-edit-path-ref (:id shape))
{:keys [edit-mode selected drag-handler prev-handler preview content-modifiers last-point]} (mf/deref edit-path-ref)
{:keys [content]} shape
selected (or selected #{})
content (ugp/apply-content-modifiers content content-modifiers)
points (->> content ugp/content->points (into #{}))
last-command (last content)
last-p (->> content last ugp/command->point)
handlers (ugp/content->handlers content)]
[:g.path-editor
(when (and preview (not drag-handler))
[:& path-preview {:command preview
:from last-p
:zoom zoom}])
(for [position points]
[:g.path-node
[:& path-point {:position position
:selected? false
:zoom zoom
:edit-mode edit-mode
:start-path? (nil? last-point)}]
[:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")}
(for [[index prefix] (get handlers position)]
(let [command (get content index)
x (get-in command [:params (d/prefix-keyword prefix :x)])
y (get-in command [:params (d/prefix-keyword prefix :y)])
handler-position (gpt/point x y)]
[:& path-handler {:point position
:handler handler-position
:index index
:prefix prefix
:zoom zoom
:selected? false
:hover? false
:preview? false
:edit-mode edit-mode}]))]])
(when prev-handler
[:g.prev-handler {:pointer-events "none"}
[:& path-handler {:point last-p
:handler prev-handler
:zoom zoom
:selected false}]])
(when drag-handler
[:g.drag-handler {:pointer-events "none"}
(when (not= :move-to (:command last-command))
[:& path-handler {:point last-p
:handler (ugp/opposite-handler last-p drag-handler)
:zoom zoom
:selected false}])
[:& path-handler {:point last-p
:handler drag-handler
:zoom zoom
:selected false}]])]))

View file

@ -129,7 +129,6 @@
:rect i/box
:circle i/circle
:text i/text
:curve i/curve
:path i/curve
:frame i/artboard
:group i/folder
@ -141,7 +140,7 @@
i/layers))
(defn is-shape? [type]
#{:shape :rect :circle :text :curve :path :frame :group})
#{:shape :rect :circle :text :path :frame :group})
(defn parse-entry [{:keys [redo-changes]}]
(->> redo-changes

View file

@ -39,7 +39,6 @@
:circle i/circle
:path i/curve
:rect i/box
:curve i/curve
:text i/text
:group (if (some? (:component-id shape))
i/component

View file

@ -48,7 +48,6 @@
:icon [:& icon/options {:shape shape}]
:circle [:& circle/options {:shape shape}]
:path [:& path/options {:shape shape}]
:curve [:& path/options {:shape shape}]
:image [:& image/options {:shape shape}]
nil)
[:& exports-menu

View file

@ -11,6 +11,7 @@
(ns app.main.ui.workspace.sidebar.options.group
(:require
[rumext.alpha :as mf]
[app.common.attrs :as attrs]
[app.common.geom.shapes :as geom]
[app.common.pages-helpers :as cph]
[app.main.refs :as refs]
@ -43,7 +44,7 @@
(merge
;; All values extracted from the group shape, except
;; border radius, that needs to be looked up from children
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
measure-attrs
nil
@ -51,7 +52,7 @@
nil)
[shape])
measure-attrs)
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
[:rx :ry]
nil
@ -64,10 +65,10 @@
(select-keys shape component-attrs)
fill-values
(geom/get-attrs-multi shape-with-children fill-attrs)
(attrs/get-attrs-multi shape-with-children fill-attrs)
stroke-values
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
stroke-attrs
nil
@ -77,7 +78,7 @@
stroke-attrs)
font-values
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
nil
text-font-attrs
@ -87,7 +88,7 @@
text-font-attrs)
align-values
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
nil
text-align-attrs
@ -97,7 +98,7 @@
text-align-attrs)
spacing-values
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
nil
text-spacing-attrs
@ -107,7 +108,7 @@
text-spacing-attrs)
valign-values
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
nil
text-valign-attrs
@ -117,7 +118,7 @@
text-valign-attrs)
decoration-values
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
nil
text-decoration-attrs
@ -127,7 +128,7 @@
text-decoration-attrs)
transform-values
(geom/get-attrs-multi (map #(get-shape-attrs
(attrs/get-attrs-multi (map #(get-shape-attrs
%
nil
text-transform-attrs

View file

@ -43,11 +43,15 @@
old-shapes (deref (refs/objects-by-id ids))
frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes)
shapes (map gsh/transform-shape frames old-shapes)
values (cond-> values
(not= (:x values) :multiple) (assoc :x (:x (:selrect (first shapes))))
(not= (:y values) :multiple) (assoc :y (:y (:selrect (first shapes)))))
shapes (as-> old-shapes $
(map gsh/transform-shape $)
(map gsh/translate-to-frame $ frames))
values (let [{:keys [x y]} (-> shapes first :points gsh/points->selrect)]
(cond-> values
(not= (:x values) :multiple) (assoc :x x)
(not= (:y values) :multiple) (assoc :y y)))
proportion-lock (:proportion-lock values)
@ -65,7 +69,7 @@
do-position-change
(fn [shape' frame' value attr]
(let [from (-> shape' :selrect attr)
(let [from (-> shape' :points gsh/points->selrect attr)
to (+ value (attr frame'))
target (+ (attr shape') (- to from))]
(st/emit! (udw/update-position (:id shape') {attr target}))))

View file

@ -11,6 +11,7 @@
(:require
[rumext.alpha :as mf]
[app.common.geom.shapes :as geom]
[app.common.attrs :as attrs]
[app.main.data.workspace.texts :as dwt]
[app.main.ui.workspace.sidebar.options.measures :refer [measure-attrs measures-menu]]
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
@ -48,9 +49,9 @@
text-attrs
convert-attrs
extract-fn))]
(geom/get-attrs-multi (map mapfn shapes) (or attrs text-attrs))))
(attrs/get-attrs-multi (map mapfn shapes) (or attrs text-attrs))))
measure-values (geom/get-attrs-multi shapes measure-attrs)
measure-values (attrs/get-attrs-multi shapes measure-attrs)
fill-values (extract {:attrs fill-attrs
:text-attrs ot/text-fill-attrs

View file

@ -141,8 +141,9 @@
(fn [[selrect selected frame]]
(let [lt-side (if (= coord :x) :left :top)
gt-side (if (= coord :x) :right :bottom)
areas (gsh/selrect->areas (or (:selrect frame)
(gsh/rect->rect-shape @refs/vbox)) selrect)
container-selrec (or (:selrect frame)
(gsh/rect->selrect @refs/vbox))
areas (gsh/selrect->areas container-selrec selrect)
query-side (fn [side]
(->> (uw/ask! {:cmd :selection/query
:page-id page-id

View file

@ -58,7 +58,7 @@
(defn get-snap
[coord {:keys [shapes page-id filter-shapes local]}]
(let [shape (if (> (count shapes) 1)
(->> shapes (map gsh/transform-shape) gsh/selection-rect)
(->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect}))
(->> shapes (first)))
shape (if (:modifiers local)

View file

@ -1,4 +1,4 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
; 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/.
;;
@ -52,7 +52,8 @@
[goog.events :as events]
[potok.core :as ptk]
[promesa.core :as p]
[rumext.alpha :as mf])
[rumext.alpha :as mf]
[app.main.ui.workspace.shapes.path :refer [path-actions]])
(:import goog.events.EventType))
;; --- Coordinates Widget
@ -198,17 +199,21 @@
vport
vbox
edition
edit-path
tooltip
selected
panning
picking-color?]} local
page-id (mf/use-ctx ctx/current-page-id)
selrect-orig (->> (mf/deref refs/selected-objects)
(gsh/selection-rect))
selrect (-> selrect-orig
(assoc :modifiers (:modifiers local))
(gsh/transform-shape))
selected-objects (mf/deref refs/selected-objects)
selrect-orig (->> selected-objects
(gsh/selection-rect))
selrect (->> selected-objects
(map #(assoc % :modifiers (:modifiers local)))
(map gsh/transform-shape)
(gsh/selection-rect))
alt? (mf/use-state false)
viewport-ref (mf/use-ref nil)
@ -217,9 +222,9 @@
drawing (mf/deref refs/workspace-drawing)
drawing-tool (:tool drawing)
drawing-obj (:object drawing)
drawing-path? (and edition (= :draw (get-in edit-path [edition :edit-mode])))
zoom (or zoom 1)
on-mouse-down
(mf/use-callback
(mf/deps drawing-tool edition)
@ -231,14 +236,13 @@
alt? (kbd/alt? event)]
(st/emit! (ms/->MouseEvent :down ctrl? shift? alt?))
(cond
(and (= 1 (.-which event)))
(and (= 1 (.-which event)) (not edition))
(if drawing-tool
(when (not= drawing-tool :comments)
(when (not (#{:comments :path} drawing-tool))
(st/emit! (dd/start-drawing drawing-tool)))
(st/emit! dw/handle-selection))
(and (not edition)
(= 2 (.-which event)))
(and (= 2 (.-which event)))
(handle-viewport-positioning viewport-ref)))))
on-context-menu
@ -265,18 +269,18 @@
on-pointer-down
(mf/use-callback
(fn [event]
(fn [event]
(let [target (dom/get-target event)]
; Capture mouse pointer to detect the movements even if cursor
; leaves the viewport or the browser itself
; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
; Capture mouse pointer to detect the movements even if cursor
; leaves the viewport or the browser itself
; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
(.setPointerCapture target (.-pointerId event)))))
on-pointer-up
(mf/use-callback
(fn [event]
(fn [event]
(let [target (dom/get-target event)]
; Release pointer on mouse up
; Release pointer on mouse up
(.releasePointerCapture target (.-pointerId event)))))
on-click
@ -290,12 +294,16 @@
on-double-click
(mf/use-callback
(mf/deps edition edit-path)
(fn [event]
(dom/stop-propagation event)
(let [ctrl? (kbd/ctrl? event)
shift? (kbd/shift? event)
alt? (kbd/alt? event)]
(st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt?)))))
(st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt?))
(if (not drawing-path?)
(st/emit! dw/clear-edition-mode)))))
on-key-down
(mf/use-callback
@ -425,6 +433,7 @@
final-x (- (:x viewport-coord) (/ (:width shape) 2))
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
(st/emit! (dw/add-shape (-> shape
(assoc :id (uuid/next))
(assoc :x final-x)
(assoc :y final-y)))))
@ -527,12 +536,12 @@
:class (when drawing-tool "drawing")
:style {:cursor (cond
panning cur/hand
(= drawing-tool :comments) cur/hand
(= drawing-tool :comments) cur/comments
(= drawing-tool :frame) cur/create-artboard
(= drawing-tool :rect) cur/create-rectangle
(= drawing-tool :circle) cur/create-ellipse
(= drawing-tool :path) cur/pen
(= drawing-tool :curve)cur/pencil
(or (= drawing-tool :path) drawing-path?) cur/pen
(= drawing-tool :curve) cur/pencil
drawing-tool cur/create-shape
:else cur/pointer-inner)
:background-color (get options :background "#E8E9EA")}
@ -606,3 +615,13 @@
(when (= options-mode :prototype)
[:& interactions {:selected selected}])]]))
(mf/defc viewport-actions []
(let [edition (mf/deref refs/selected-edition)
selected (mf/deref refs/selected-objects)
shape (-> selected first)]
(when (and (= (count selected) 1)
(= (:id shape) edition)
(= :path (:type shape)))
[:div.viewport-actions
[:& path-actions {:shape shape}]])))

View file

@ -15,8 +15,8 @@
[app.util.worker :as uw]))
(defn on-error
[instance error]
(js/console.error "Error on worker" (.-data error)))
[error]
(js/console.error "Error on worker" error))
(defonce instance
(when (not= *target* "nodejs")

View file

@ -118,6 +118,33 @@
(into {}))
m1))
(defn with-next
"Given a collectin will return a new collection where each element
is paried with the next item in the collection
(with-next (range 5)) => [[0 1] [1 2] [2 3] [3 4] [4 nil]"
[coll]
(map vector
coll
(concat [] (rest coll) [nil])))
(defn with-prev
"Given a collectin will return a new collection where each element
is paried with the previous item in the collection
(with-prev (range 5)) => [[0 nil] [1 0] [2 1] [3 2] [4 3]"
[coll]
(map vector
coll
(concat [nil] coll)))
(defn with-prev-next
"Given a collection will return a new collection where every item is paired
with the previous and the next item of a collection
(with-prev-next (range 5)) => [[0 nil 1] [1 0 2] [2 1 3] [3 2 4] [4 3 nil]"
[coll]
(map vector
coll
(concat [nil] coll)
(concat [] (rest coll) [nil])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Numbers Parsing
@ -221,3 +248,7 @@
;; nil
;; (throw e#)))))))
(defn prefix-keyword [prefix kw]
(let [prefix (if (keyword? prefix) (name prefix) prefix)
kw (if (keyword? kw) (name kw) kw)]
(keyword (str prefix kw))))

View file

@ -8,7 +8,12 @@
;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz>
(ns app.util.geom.path
(:require [app.util.geom.path-impl-simplify :as impl-simplify]))
(:require
[cuerdas.core :as str]
[app.util.data :as d]
[app.common.data :as cd]
[app.common.geom.point :as gpt]
[app.util.geom.path-impl-simplify :as impl-simplify]))
(defn simplify
([points]
@ -16,3 +21,269 @@
([points tolerance]
(let [points (into-array points)]
(into [] (impl-simplify/simplify points tolerance true)))))
;;
(def commands-regex #"(?i)[a-z][^a-z]*")
;; Matches numbers for path values allows values like... -.01, 10, +12.22
;; 0 and 1 are special because can refer to flags
(def num-regex #"([+-]?(([1-9]\d*(\.\d+)?)|(\.\d+)|0|1))")
(defn coord-n [size]
(re-pattern (str "(?i)[a-z]\\s*"
(->> (range size)
(map #(identity num-regex))
(str/join "\\s+")))))
(defn parse-params [cmd-str num-params]
(let [fix-starting-dot (fn [arg] (str/replace arg #"([^\d]|^)\." "$10."))]
(->> (re-seq num-regex cmd-str)
(map first)
(map fix-starting-dot)
(map d/read-string)
(partition num-params))))
(defn command->param-list [{:keys [command params]}]
(case command
(:move-to :line-to :smooth-quadratic-bezier-curve-to)
(let [{:keys [x y]} params] [x y])
:close-path
[]
(:line-to-horizontal :line-to-vertical)
(let [{:keys [value]} params] [value])
:curve-to
(let [{:keys [c1x c1y c2x c2y x y]} params] [c1x c1y c2x c2y x y])
(:smooth-curve-to :quadratic-bezier-curve-to)
(let [{:keys [cx cy x y]} params] [cx cy x y])
:elliptical-arc
(let [{:keys [rx ry x-axis-rotation large-arc-flag sweep-flag x y]} params]
[rx ry x-axis-rotation large-arc-flag sweep-flag x y])))
;; Path specification
;; https://www.w3.org/TR/SVG11/paths.html
(defmulti parse-command (comp str/upper first))
(defmethod parse-command "M" [cmd]
(let [relative (str/starts-with? cmd "m")
params (parse-params cmd 2)]
(for [[x y] params]
{:command :move-to
:relative relative
:params {:x x :y y}})))
(defmethod parse-command "Z" [cmd]
[{:command :close-path}])
(defmethod parse-command "L" [cmd]
(let [relative (str/starts-with? cmd "l")
params (parse-params cmd 2)]
(for [[x y] params]
{:command :line-to
:relative relative
:params {:x x :y y}})))
(defmethod parse-command "H" [cmd]
(let [relative (str/starts-with? cmd "h")
params (parse-params cmd 1)]
(for [[value] params]
{:command :line-to-horizontal
:relative relative
:params {:value value}})))
(defmethod parse-command "V" [cmd]
(let [relative (str/starts-with? cmd "v")
params (parse-params cmd 1)]
(for [[value] params]
{:command :line-to-vertical
:relative relative
:params {:value value}})))
(defmethod parse-command "C" [cmd]
(let [relative (str/starts-with? cmd "c")
params (parse-params cmd 6)]
(for [[c1x c1y c2x c2y x y] params]
{:command :curve-to
:relative relative
:params {:c1x c1x
:c1y c1y
:c2x c2x
:c2y c2y
:x x
:y y}})))
(defmethod parse-command "S" [cmd]
(let [relative (str/starts-with? cmd "s")
params (parse-params cmd 4)]
(for [[cx cy x y] params]
{:command :smooth-curve-to
:relative relative
:params {:cx cx
:cy cy
:x x
:y y}})))
(defmethod parse-command "Q" [cmd]
(let [relative (str/starts-with? cmd "s")
params (parse-params cmd 4)]
(for [[cx cy x y] params]
{:command :quadratic-bezier-curve-to
:relative relative
:params {:cx cx
:cy cy
:x x
:y y}})))
(defmethod parse-command "T" [cmd]
(let [relative (str/starts-with? cmd "t")
params (parse-params cmd (coord-n 2))]
(for [[cx cy x y] params]
{:command :smooth-quadratic-bezier-curve-to
:relative relative
:params {:x x
:y y}})))
(defmethod parse-command "A" [cmd]
(let [relative (str/starts-with? cmd "a")
params (parse-params cmd 7)]
(for [[rx ry x-axis-rotation large-arc-flag sweep-flag x y] params]
{:command :elliptical-arc
:relative relative
:params {:rx rx
:ry ry
:x-axis-rotation x-axis-rotation
:large-arc-flag large-arc-flag
:sweep-flag sweep-flag
:x x
:y y}})))
(defn command->string [{:keys [command relative params] :as entry}]
(let [command-str (case command
:move-to "M"
:close-path "Z"
:line-to "L"
:line-to-horizontal "H"
:line-to-vertical "V"
:curve-to "C"
:smooth-curve-to "S"
:quadratic-bezier-curve-to "Q"
:smooth-quadratic-bezier-curve-to "T"
:elliptical-arc "A")
command-str (if relative (str/lower command-str) command-str)
param-list (command->param-list entry)]
(str/fmt "%s%s" command-str (str/join " " param-list))))
(defn path->content [string]
(let [clean-string (-> string
(str/trim)
;; Change "commas" for spaces
(str/replace #"," " ")
;; Remove all consecutive spaces
(str/replace #"\s+" " "))
commands (re-seq commands-regex clean-string)]
(mapcat parse-command commands)))
(defn content->path [content]
(->> content
(map command->string)
(str/join "")))
(defn make-curve-params
([point]
(make-curve-params point point point))
([point handler] (make-curve-params point handler point))
([point h1 h2]
{:x (:x point)
:y (:y point)
:c1x (:x h1)
:c1y (:y h1)
:c2x (:x h2)
:c2y (:y h2)}))
(defn opposite-handler
"Calculates the coordinates of the opposite handler"
[point handler]
(let [phv (gpt/to-vec point handler)]
(gpt/add point (gpt/negate phv))))
(defn opposite-handler-keep-distance
"Calculates the coordinates of the opposite handler but keeping the old distance"
[point handler old-opposite]
(let [old-distance (gpt/distance point old-opposite)
phv (gpt/to-vec point handler)
phv2 (gpt/multiply
(gpt/unit (gpt/negate phv))
(gpt/point old-distance))]
(gpt/add point phv2)))
(defn apply-content-modifiers [content modifiers]
(letfn [(apply-to-index [content [index params]]
(if (contains? content index)
(cond-> content
(and
(or (:c1x params) (:c1y params) (:c2x params) (:c2y params))
(= :line-to (get-in content [index :params :command])))
(-> (assoc-in [index :command] :curve-to)
(assoc-in [index :params] :curve-to) (make-curve-params
(get-in content [index :params])
(get-in content [(dec index) :params])))
(:x params) (update-in [index :params :x] + (:x params))
(:y params) (update-in [index :params :y] + (:y params))
(:c1x params) (update-in [index :params :c1x] + (:c1x params))
(:c1y params) (update-in [index :params :c1y] + (:c1y params))
(:c2x params) (update-in [index :params :c2x] + (:c2x params))
(:c2y params) (update-in [index :params :c2y] + (:c2y params)))
content))]
(reduce apply-to-index content modifiers)))
(defn command->point [{{:keys [x y]} :params}]
(gpt/point x y))
(defn content->points [content]
(->> content
(map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y))))
(remove nil?)
(into [])))
(defn content->handlers [content]
(->> (d/with-prev content) ;; [cmd, prev]
(d/enumerate) ;; [idx [cmd, prev]]
(mapcat (fn [[index [cur-cmd prev-cmd]]]
(if (and prev-cmd
(= :curve-to (:command cur-cmd)))
(let [cur-pos (command->point cur-cmd)
pre-pos (command->point prev-cmd)]
[[pre-pos [index :c1]]
[cur-pos [index :c2]]])
[])))
(group-by first)
(cd/mapm #(mapv second %2))))
(defn opposite-index [content index prefix]
(let [point (if (= prefix :c2)
(command->point (nth content index))
(command->point (nth content (dec index))))
handlers (-> (content->handlers content)
(get point))
opposite-prefix (if (= prefix :c1) :c2 :c1)
result (when (<= (count handlers) 2)
(->> handlers
(d/seek (fn [[index prefix]] (= prefix opposite-prefix)))
(first)))]
result))

View file

@ -14,22 +14,22 @@
[app.common.geom.shapes :as gsh]
[app.common.geom.point :as gpt]))
(defn- frame-snap-points [{:keys [x y width height] :as frame}]
(into #{(gpt/point x y)
(gpt/point (+ x (/ width 2)) y)
(gpt/point (+ x width) y)
(defn- selrect-snap-points [{:keys [x y width height]}]
#{(gpt/point x y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y height))
(gpt/point x (+ y height))})
(defn- frame-snap-points [{:keys [x y width height] :as selrect}]
(into (selrect-snap-points selrect)
#{(gpt/point (+ x (/ width 2)) y)
(gpt/point (+ x width) (+ y (/ height 2)))
(gpt/point (+ x width) (+ y height))
(gpt/point (+ x (/ width 2)) (+ y height))
(gpt/point x (+ y height))
(gpt/point x (+ y (/ height 2)))}))
(defn shape-snap-points
[shape]
(let [shape (gsh/transform-shape shape)
shape-center (gsh/center shape)]
(if (= :frame (:type shape))
(-> shape
(gsh/shape->rect-shape)
(frame-snap-points))
(into #{shape-center} (:points shape)))))
(let [shape (gsh/transform-shape shape)]
(case (:type shape)
:frame (-> shape :selrect frame-snap-points)
(into #{(gsh/center-shape shape)} (:points shape)))))

View file

@ -38,10 +38,12 @@
(fn [event]
(let [data (.-data event)
data (t/decode data)]
(rx/push! bus data))))
(if (:error data)
(on-error (:error data))
(rx/push! bus data)))))
(.addEventListener ins "error"
(fn [error]
(on-error wrk error)))
(on-error wrk (.-data error))))
wrk))

View file

@ -65,8 +65,7 @@
(defn- create-index
[objects]
(let [shapes (->> (cph/select-toplevel-shapes objects {:include-frames? true})
(map #(merge % (select-keys % [:x :y :width :height]))))
(let [shapes (cph/select-toplevel-shapes objects {:include-frames? true})
bounds (geom/selection-rect shapes)
bounds #js {:x (:x bounds)
:y (:y bounds)
@ -77,7 +76,8 @@
shapes)))
(defn- index-object
[index {:keys [id x y width height] :as obj}]
(let [rect #js {:x x :y y :width width :height height}]
[index obj]
(let [{:keys [id x y width height]} (:selrect obj)
rect #js {:x x :y y :width width :height height}]
(qdt/insert index rect obj)))