mirror of
https://github.com/penpot/penpot.git
synced 2026-05-30 20:28:07 +00:00
553 lines
14 KiB
Clojure
553 lines
14 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.common.geom.point
|
|
(:refer-clojure :exclude [divide min max abs])
|
|
(:require
|
|
#?(:clj [app.common.fressian :as fres])
|
|
#?(:cljs [cljs.core :as c]
|
|
:clj [clojure.core :as c])
|
|
#?(:cljs [cljs.pprint :as pp]
|
|
:clj [clojure.pprint :as pp])
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.math :as mth]
|
|
[app.common.record :as cr]
|
|
[app.common.schema :as sm]
|
|
[app.common.schema.generators :as sg]
|
|
[app.common.schema.openapi :as-alias oapi]
|
|
[app.common.spec :as us]
|
|
[app.common.transit :as t]
|
|
[clojure.spec.alpha :as s]
|
|
[cuerdas.core :as str])
|
|
#?(:clj
|
|
(:import
|
|
java.util.List)))
|
|
|
|
;; --- Point Impl
|
|
|
|
(cr/defrecord Point [x y])
|
|
|
|
(defn s
|
|
[pt]
|
|
(dm/str "(" (dm/get-prop pt :x) "," (dm/get-prop pt :y) ")"))
|
|
|
|
(defn point?
|
|
"Return true if `v` is Point instance."
|
|
[v]
|
|
(instance? Point v))
|
|
|
|
;; FIXME: deprecated
|
|
(s/def ::x ::us/safe-number)
|
|
(s/def ::y ::us/safe-number)
|
|
|
|
(s/def ::point-attrs
|
|
(s/keys :req-un [::x ::y]))
|
|
|
|
(s/def ::point
|
|
(s/and ::point-attrs point?))
|
|
|
|
(def ^:private schema:point-attrs
|
|
[:map {:title "PointAttrs"}
|
|
[:x ::sm/safe-number]
|
|
[:y ::sm/safe-number]])
|
|
|
|
(def valid-point-attrs?
|
|
(sm/validator schema:point-attrs))
|
|
|
|
(def valid-point?
|
|
(sm/validator
|
|
[:and [:fn point?] schema:point-attrs]))
|
|
|
|
(defn decode-point
|
|
[p]
|
|
(if (map? p)
|
|
(map->Point p)
|
|
(if (string? p)
|
|
(let [[x y] (->> (str/split p #",") (mapv parse-double))]
|
|
(pos->Point x y))
|
|
p)))
|
|
|
|
(defn point->str
|
|
[p]
|
|
(if (point? p)
|
|
(dm/str (dm/get-prop p :x) ","
|
|
(dm/get-prop p :y))
|
|
p))
|
|
|
|
(defn point->json
|
|
[p]
|
|
(if (point? p)
|
|
(into {} p)
|
|
p))
|
|
|
|
;; FIXME: make like matrix
|
|
(def schema:point
|
|
{:type :map
|
|
:pred valid-point?
|
|
:type-properties
|
|
{:title "point"
|
|
:description "Point"
|
|
:error/message "expected a valid point"
|
|
:gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int))
|
|
(sg/fmap #(apply pos->Point %)))
|
|
::oapi/type "string"
|
|
::oapi/format "point"
|
|
:decode/json decode-point
|
|
:decode/string decode-point
|
|
:encode/json point->json
|
|
:encode/string point->str}})
|
|
|
|
(sm/register! ::point schema:point)
|
|
|
|
(defn point-like?
|
|
[{:keys [x y] :as v}]
|
|
(and (map? v)
|
|
(d/num? x)
|
|
(d/num? y)))
|
|
|
|
(defn point
|
|
"Create a Point instance."
|
|
([] (pos->Point 0 0))
|
|
([v]
|
|
(cond
|
|
(point? v)
|
|
v
|
|
|
|
(number? v)
|
|
(point v v)
|
|
|
|
(point-like? v)
|
|
(pos->Point (:x v) (:y v))
|
|
|
|
:else
|
|
(ex/raise :hint "invalid arguments (on pointer constructor)" :value v)))
|
|
([x y]
|
|
(pos->Point x y)))
|
|
|
|
(defn close?
|
|
[p1 p2]
|
|
(and (mth/close? (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(mth/close? (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))))
|
|
|
|
(defn angle->point
|
|
[pt angle distance]
|
|
(point
|
|
(+ (dm/get-prop pt :x) (* distance (mth/cos angle)))
|
|
(- (dm/get-prop pt :y) (* distance (mth/sin angle)))))
|
|
|
|
(defn add
|
|
"Returns the addition of the supplied value to both
|
|
coordinates of the point as a new point."
|
|
[p1 p2]
|
|
(dm/assert!
|
|
"arguments should be point instance"
|
|
(and (point? p1)
|
|
(point? p2)))
|
|
|
|
(pos->Point (+ (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(+ (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))))
|
|
|
|
(defn subtract
|
|
"Returns the subtraction of the supplied value to both
|
|
coordinates of the point as a new point."
|
|
[p1 p2]
|
|
(dm/assert!
|
|
"arguments should be pointer instance"
|
|
(and (point? p1)
|
|
(point? p2)))
|
|
|
|
(pos->Point (- (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(- (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))))
|
|
|
|
(defn multiply
|
|
"Returns the subtraction of the supplied value to both
|
|
coordinates of the point as a new point."
|
|
[p1 p2]
|
|
(assert (and (point? p1)
|
|
(point? p2))
|
|
"arguments should be pointer instance")
|
|
(pos->Point (* (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(* (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))))
|
|
|
|
(defn divide
|
|
[p1 p2]
|
|
(assert (and (point? p1)
|
|
(point? p2))
|
|
"arguments should be pointer instance")
|
|
(pos->Point (/ (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(/ (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))))
|
|
|
|
(defn min
|
|
([] nil)
|
|
([p1] p1)
|
|
([p1 p2]
|
|
(cond
|
|
(nil? p1) p2
|
|
(nil? p2) p1
|
|
:else (pos->Point (c/min (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(c/min (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))))))
|
|
(defn max
|
|
([] nil)
|
|
([p1] p1)
|
|
([p1 p2]
|
|
(cond
|
|
(nil? p1) p2
|
|
(nil? p2) p1
|
|
:else (pos->Point (c/max (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(c/max (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))))))
|
|
(defn inverse
|
|
[pt]
|
|
(assert (point? pt) "point instance expected")
|
|
(pos->Point (/ 1.0 (dm/get-prop pt :x))
|
|
(/ 1.0 (dm/get-prop pt :y))))
|
|
|
|
(defn negate
|
|
[pt]
|
|
(assert (point? pt) "point instance expected")
|
|
(pos->Point (- (dm/get-prop pt :x))
|
|
(- (dm/get-prop pt :y))))
|
|
|
|
(defn distance
|
|
"Calculate the distance between two points."
|
|
[p1 p2]
|
|
(assert (and (point? p1)
|
|
(point? p2))
|
|
"arguments should be point instances")
|
|
(let [dx (- (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
dy (- (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))]
|
|
(mth/hypot dx dy)))
|
|
|
|
(defn distance-vector
|
|
"Calculate the distance, separated x and y."
|
|
[p1 p2]
|
|
(assert (and (point? p1)
|
|
(point? p2))
|
|
"arguments should be point instances")
|
|
(let [dx (- (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
dy (- (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))]
|
|
(pos->Point (mth/abs dx)
|
|
(mth/abs dy))))
|
|
|
|
(defn length
|
|
[pt]
|
|
(assert (point? pt) "point instance expected")
|
|
(let [x (dm/get-prop pt :x)
|
|
y (dm/get-prop pt :y)]
|
|
(mth/hypot x y)))
|
|
|
|
(defn angle
|
|
"Returns the smaller angle between two vectors.
|
|
If the second vector is not provided, the angle
|
|
will be measured from x-axis."
|
|
([pt]
|
|
(assert (point? pt) "point instance expected")
|
|
(let [x (dm/get-prop pt :x)
|
|
y (dm/get-prop pt :y)]
|
|
(-> (mth/atan2 y x)
|
|
(mth/degrees))))
|
|
([pt center]
|
|
(assert (point? pt) "point instance expected")
|
|
(assert (point? center) "point instance expected")
|
|
(let [x (- (dm/get-prop pt :x)
|
|
(dm/get-prop center :x))
|
|
y (- (dm/get-prop pt :y)
|
|
(dm/get-prop center :y))]
|
|
(-> (mth/atan2 y x)
|
|
(mth/degrees)))))
|
|
|
|
(defn angle-with-other
|
|
"Consider point as vector and calculate
|
|
the angle between two vectors."
|
|
[p1 p2]
|
|
(assert (and (point? p1)
|
|
(point? p2))
|
|
"arguments should be point instances")
|
|
(let [length-p1 (length p1)
|
|
length-p2 (length p2)]
|
|
(if (or (mth/almost-zero? length-p1)
|
|
(mth/almost-zero? length-p2))
|
|
0
|
|
(let [a (/ (+ (* (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(* (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y)))
|
|
(* length-p1 length-p2))
|
|
a (mth/acos (if (< a -1) -1 (if (> a 1) 1 a)))
|
|
d (mth/degrees a)]
|
|
(if (mth/nan? d) 0 d)))))
|
|
|
|
(defn angle-sign
|
|
[p1 p2]
|
|
(if (> (* (dm/get-prop p1 :y) (dm/get-prop p2 :x))
|
|
(* (dm/get-prop p1 :x) (dm/get-prop p2 :y)))
|
|
-1
|
|
1))
|
|
|
|
(defn signed-angle-with-other
|
|
[v1 v2]
|
|
(* (angle-sign v1 v2) (angle-with-other v1 v2)))
|
|
|
|
(defn update-angle
|
|
"Update the angle of the point."
|
|
[p angle]
|
|
(assert (number? angle) "expected number")
|
|
(let [len (length p)
|
|
angle (mth/radians angle)]
|
|
(pos->Point (* (mth/cos angle) len)
|
|
(* (mth/sin angle) len))))
|
|
|
|
(defn quadrant
|
|
"Return the quadrant of the angle of the point."
|
|
[p]
|
|
(assert (point? p) "expected point instance")
|
|
(let [x (dm/get-prop p :x)
|
|
y (dm/get-prop p :y)]
|
|
(if (>= x 0)
|
|
(if (>= y 0) 1 4)
|
|
(if (>= y 0) 2 3))))
|
|
|
|
(defn round
|
|
"Round the coordinates of the point to a precision"
|
|
([point]
|
|
(round point 0))
|
|
|
|
([pt decimals]
|
|
(assert (point? pt) "expected point instance")
|
|
(assert (number? decimals) "expected number instance")
|
|
(pos->Point (mth/precision (dm/get-prop pt :x) decimals)
|
|
(mth/precision (dm/get-prop pt :y) decimals))))
|
|
|
|
(defn round-step
|
|
"Round the coordinates to the closest half-point"
|
|
[pt step]
|
|
(assert (point? pt) "expected point instance")
|
|
(pos->Point (mth/round (dm/get-prop pt :x) step)
|
|
(mth/round (dm/get-prop pt :y) step)))
|
|
|
|
(defn transform
|
|
"Transform a point applying a matrix transformation."
|
|
[p m]
|
|
(when (point? p)
|
|
(if (some? m)
|
|
(let [x (dm/get-prop p :x)
|
|
y (dm/get-prop p :y)
|
|
a (dm/get-prop m :a)
|
|
b (dm/get-prop m :b)
|
|
c (dm/get-prop m :c)
|
|
d (dm/get-prop m :d)
|
|
e (dm/get-prop m :e)
|
|
f (dm/get-prop m :f)]
|
|
(pos->Point (+ (* x a) (* y c) e)
|
|
(+ (* x b) (* y d) f)))
|
|
p)))
|
|
|
|
|
|
(defn transform!
|
|
[p m]
|
|
|
|
(dm/assert!
|
|
"expected valid rect and matrix instances"
|
|
(and (some? p) (some? m)))
|
|
|
|
(let [x (dm/get-prop p :x)
|
|
y (dm/get-prop p :y)
|
|
a (dm/get-prop m :a)
|
|
b (dm/get-prop m :b)
|
|
c (dm/get-prop m :c)
|
|
d (dm/get-prop m :d)
|
|
e (dm/get-prop m :e)
|
|
f (dm/get-prop m :f)]
|
|
#?(:clj
|
|
(pos->Point (+ (* x a) (* y c) e)
|
|
(+ (* x b) (* y d) f))
|
|
:cljs
|
|
(do
|
|
(set! (.-x p) (+ (* x a) (* y c) e))
|
|
(set! (.-y p) (+ (* x b) (* y d) f))
|
|
p))))
|
|
|
|
(defn matrix->point
|
|
"Returns a result of transform an identity point with the provided
|
|
matrix instance"
|
|
[m]
|
|
(let [e (dm/get-prop m :e)
|
|
f (dm/get-prop m :f)]
|
|
(pos->Point e f)))
|
|
|
|
;; Vector functions
|
|
(defn to-vec [p1 p2]
|
|
(subtract p2 p1))
|
|
|
|
(defn scale
|
|
[p scalar]
|
|
(pos->Point (* (dm/get-prop p :x) scalar)
|
|
(* (dm/get-prop p :y) scalar)))
|
|
|
|
(defn dot
|
|
[p1 p2]
|
|
(+ (* (dm/get-prop p1 :x)
|
|
(dm/get-prop p2 :x))
|
|
(* (dm/get-prop p1 :y)
|
|
(dm/get-prop p2 :y))))
|
|
|
|
(defn unit
|
|
[p1]
|
|
(let [p-length (length p1)]
|
|
(if (mth/almost-zero? p-length)
|
|
(pos->Point 0 0)
|
|
(pos->Point (/ (dm/get-prop p1 :x) p-length)
|
|
(/ (dm/get-prop p1 :y) p-length)))))
|
|
|
|
(defn perpendicular
|
|
[pt]
|
|
(pos->Point (- (dm/get-prop pt :y))
|
|
(dm/get-prop pt :x)))
|
|
|
|
(defn project
|
|
"V1 perpendicular projection on vector V2"
|
|
[v1 v2]
|
|
(let [v2-unit (unit v2)
|
|
scalar-proj (dot v1 v2-unit)]
|
|
(scale v2-unit scalar-proj)))
|
|
|
|
(defn center-points
|
|
"Centroid of a group of points"
|
|
[points]
|
|
(let [k (point (count points))]
|
|
(reduce #(add %1 (divide %2 k)) (point) points)))
|
|
|
|
(defn normal-left
|
|
"Returns the normal unit vector on the left side"
|
|
[{:keys [x y]}]
|
|
(unit (point (- y) x)))
|
|
|
|
(defn normal-right
|
|
"Returns the normal unit vector on the right side"
|
|
[{:keys [x y]}]
|
|
(unit (point y (- x))))
|
|
|
|
(defn point-line-distance
|
|
"Returns the distance from a point to a line defined by two points"
|
|
[point line-point1 line-point2]
|
|
(let [x0 (dm/get-prop point :x)
|
|
y0 (dm/get-prop point :y)
|
|
x1 (dm/get-prop line-point1 :x)
|
|
y1 (dm/get-prop line-point1 :y)
|
|
x2 (dm/get-prop line-point2 :x)
|
|
y2 (dm/get-prop line-point2 :y)]
|
|
(/ (mth/abs (+ (* x0 (- y2 y1))
|
|
(- (* y0 (- x2 x1)))
|
|
(* x2 y1)
|
|
(- (* y2 x1))))
|
|
(distance line-point2 line-point1))))
|
|
|
|
(defn almost-zero?
|
|
[p]
|
|
(assert (point? p) "point instance expected")
|
|
(and ^boolean (mth/almost-zero? (dm/get-prop p :x))
|
|
^boolean (mth/almost-zero? (dm/get-prop p :y))))
|
|
|
|
(defn lerp
|
|
"Calculates a linear interpolation between two points given a tvalue"
|
|
[p1 p2 t]
|
|
(let [x (mth/lerp (dm/get-prop p1 :x) (dm/get-prop p2 :x) t)
|
|
y (mth/lerp (dm/get-prop p1 :y) (dm/get-prop p2 :y) t)]
|
|
(pos->Point x y)))
|
|
|
|
(defn rotate
|
|
"Rotates the point around center with an angle"
|
|
[p c angle]
|
|
(assert (point? p) "point instance expected")
|
|
(assert (point? c) "point instance expected")
|
|
(let [angle (mth/radians angle)
|
|
px (dm/get-prop p :x)
|
|
py (dm/get-prop p :y)
|
|
cx (dm/get-prop c :x)
|
|
cy (dm/get-prop c :y)
|
|
|
|
sa (mth/sin angle)
|
|
ca (mth/cos angle)
|
|
|
|
x (+ (* ca (- px cx))
|
|
(* sa (- py cy) -1)
|
|
cx)
|
|
y (+ (* sa (- px cx))
|
|
(* ca (- py cy))
|
|
cy)]
|
|
(pos->Point x y)))
|
|
|
|
(defn scale-from
|
|
"Moves a point in the vector that creates with center with a scale
|
|
value"
|
|
[point center value]
|
|
(add point
|
|
(-> (to-vec center point)
|
|
(unit)
|
|
(scale value))))
|
|
|
|
(defn no-zeros
|
|
"Remove zero values from either coordinate"
|
|
[p]
|
|
(let [x (dm/get-prop p :x)
|
|
y (dm/get-prop p :y)]
|
|
(pos->Point (if (mth/almost-zero? x) 0.001 x)
|
|
(if (mth/almost-zero? y) 0.001 y))))
|
|
|
|
(defn resize
|
|
"Creates a new vector with the same direction but different length"
|
|
[vector new-length]
|
|
(let [old-length (length vector)]
|
|
(scale vector (/ new-length old-length))))
|
|
|
|
;; FIXME: perfromance
|
|
(defn abs
|
|
[point]
|
|
(-> point
|
|
(update :x mth/abs)
|
|
(update :y mth/abs)))
|
|
|
|
;; --- Debug
|
|
|
|
(defmethod pp/simple-dispatch Point [obj] (pr obj))
|
|
|
|
#?(:clj
|
|
(fres/add-handlers!
|
|
{:name "penpot/point"
|
|
:class Point
|
|
:wfn (fn [n w ^Point o]
|
|
(fres/write-tag! w n 1)
|
|
(fres/write-list! w (List/of (.-x o) (.-y o))))
|
|
:rfn (fn [rdr]
|
|
(let [^List x (fres/read-object! rdr)]
|
|
(pos->Point (.get x 0) (.get x 1))))}))
|
|
|
|
(t/add-handlers!
|
|
{:id "point"
|
|
:class Point
|
|
:wfn #(into {} %)
|
|
:rfn map->Point})
|
|
|