2025-06-20 07:41:06 +02:00

192 lines
7.0 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.main.data.workspace.viewport
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.align :as gal]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as gpr]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.main.data.helpers :as dsh]
[app.util.mouse :as mse]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn initialize-viewport
[{:keys [width height] :as size}]
(assert (gpr/rect? size)
"expected `size` to be a rect instance")
(letfn [(update* [{:keys [vport] :as local}]
(let [wprop (/ (:width vport) width)
hprop (/ (:height vport) height)]
(-> local
(assoc :vport size)
(update :vbox (fn [vbox]
(-> vbox
(update :width #(/ % wprop))
(update :height #(/ % hprop))
(gpr/update-rect :size)))))))
(initialize [state local]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
shapes (cfh/get-immediate-children objects)
srect (gsh/shapes->rect shapes)
local (assoc local :vport size :zoom 1 :zoom-inverse 1 :hide-toolbar false)]
(cond
(or (not (d/num? (:width srect)))
(not (d/num? (:height srect))))
(assoc local :vbox (assoc size :x 0 :y 0))
(or (> (:width srect) width)
(> (:height srect) height))
(let [srect (gal/adjust-to-viewport size srect {:padding 40})
zoom (/ (:width size) (:width srect))]
(-> local
(assoc :zoom zoom)
(assoc :zoom-inverse (/ 1 zoom))
(update :vbox (fn [vbox]
(-> (merge vbox srect)
(gpr/make-rect))))))
:else
(let [vx (+ (:x srect)
(/ (- (:width srect) width) 2))
vy (+ (:y srect)
(/ (- (:height srect) height) 2))
vbox (-> size
(assoc :x vx)
(assoc :y vy)
(gpr/update-rect :position))]
(assoc local :vbox vbox)))))
(setup [state local]
(if (and (:vbox local) (:vport local))
(update* local)
(initialize state local)))]
(ptk/reify ::initialize-viewport
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local
(fn [local]
(setup state local)))))))
(defn calculate-centered-viewbox
"Updates the viewbox coordinates for a given center position"
[local position]
(let [vbox (:vbox local)
nw (/ (:width vbox) 2)
nh (/ (:height vbox) 2)
nx (- (:x position) nw)
ny (- (:y position) nh)]
(update local :vbox assoc :x nx :y ny)))
(defn update-viewport-position-center
[position]
(assert (gpt/point? position) "expected a point instance for `position` param")
(ptk/reify ::update-viewport-position-center
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local calculate-centered-viewbox position))))
(defn update-viewport-position
[{:keys [x y] :or {x identity y identity}}]
(dm/assert!
"expected function for `x`"
(fn? x))
(dm/assert!
"expected function for `y`"
(fn? y))
(ptk/reify ::update-viewport-position
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :vbox]
(fn [vbox]
(-> vbox
(update :x x)
(update :y y)))))))
(defn update-viewport-size
[resize-type {:keys [width height] :as size}]
(ptk/reify ::update-viewport-size
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local
(fn [{:keys [vport] :as local}]
(if (or (nil? vport)
(mth/almost-zero? width)
(mth/almost-zero? height))
;; If we have a resize to zero just keep the old value
local
(let [wprop (/ (:width vport) width)
hprop (/ (:height vport) height)
vbox (:vbox local)
vbox-x (:x vbox)
vbox-y (:y vbox)
vbox-width (:width vbox)
vbox-height (:height vbox)
vbox-width' (/ vbox-width wprop)
vbox-height' (/ vbox-height hprop)
vbox-x'
(case resize-type
:left (+ vbox-x (- vbox-width vbox-width'))
:right vbox-x
(+ vbox-x (/ (- vbox-width vbox-width') 2)))
vbox-y'
(case resize-type
:top (+ vbox-y (- vbox-height vbox-height'))
:bottom vbox-y
(+ vbox-y (/ (- vbox-height vbox-height') 2)))]
(-> local
(assoc :vport size)
(assoc-in [:vbox :x] vbox-x')
(assoc-in [:vbox :y] vbox-y')
(assoc-in [:vbox :width] vbox-width')
(assoc-in [:vbox :height] vbox-height')))))))))
(defn start-panning []
(ptk/reify ::start-panning
ptk/WatchEvent
(watch [_ state stream]
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
zoom (get-in state [:workspace-local :zoom])]
(when-not (get-in state [:workspace-local :panning])
(rx/concat
(rx/of #(-> % (assoc-in [:workspace-local :panning] true)))
(->> stream
(rx/filter mse/pointer-event?)
(rx/filter #(= :delta (:source %)))
(rx/take-until stopper)
;; Some events are executed in synchronous way like panning with backspace pressed
(rx/observe-on :af)
(rx/map (fn [event]
(let [delta (dm/get-prop event :pt)]
(update-viewport-position {:x #(- % (/ (:x delta) zoom))
:y #(- % (/ (:y delta) zoom))})))))))))))
(defn finish-panning []
(ptk/reify ::finish-panning
ptk/UpdateEvent
(update [_ state]
(-> state
(update :workspace-local dissoc :panning)))))