mirror of https://github.com/logseq/logseq
Merge branch 'whiteboards' into enhance/whiteboards-ux
commit
50b67ffcea
|
@ -1,7 +1,8 @@
|
|||
(ns logseq.graph-parser.config
|
||||
"App config that is shared between graph-parser and rest of app"
|
||||
(:require [clojure.set :as set]
|
||||
[clojure.string :as string]))
|
||||
[clojure.string :as string]
|
||||
[goog.object :as gobj]))
|
||||
|
||||
(def app-name
|
||||
"Copy of frontend.config/app-name. Too small to couple to main app"
|
||||
|
@ -9,7 +10,9 @@
|
|||
|
||||
(defonce asset-protocol "assets://")
|
||||
(defonce capacitor-protocol "capacitor://")
|
||||
(defonce capacitor-protocol-with-prefix (str capacitor-protocol "localhost/_capacitor_file_"))
|
||||
(defonce capacitor-prefix "_capacitor_file_")
|
||||
(defonce capacitor-protocol-with-prefix (str capacitor-protocol "localhost/" capacitor-prefix))
|
||||
(defonce capacitor-x-protocol-with-prefix (str (gobj/getValueByKeys js/globalThis "location" "href") capacitor-prefix))
|
||||
|
||||
(defonce local-assets-dir "assets")
|
||||
|
||||
|
@ -22,14 +25,16 @@
|
|||
[s]
|
||||
(when (string? s)
|
||||
(or (string/starts-with? s asset-protocol)
|
||||
(string/starts-with? s capacitor-protocol))))
|
||||
(string/starts-with? s capacitor-protocol)
|
||||
(string/starts-with? s capacitor-x-protocol-with-prefix))))
|
||||
|
||||
(defn remove-asset-protocol
|
||||
[s]
|
||||
(if (local-protocol-asset? s)
|
||||
(-> s
|
||||
(string/replace-first asset-protocol "")
|
||||
(string/replace-first capacitor-protocol-with-prefix "file://"))
|
||||
(string/replace-first capacitor-protocol-with-prefix "file://")
|
||||
(string/replace-first capacitor-x-protocol-with-prefix "file://"))
|
||||
s))
|
||||
|
||||
(defonce default-draw-directory "draws")
|
||||
|
|
|
@ -204,7 +204,7 @@
|
|||
(defn extract-whiteboard-edn
|
||||
"Extracts whiteboard page from given edn file
|
||||
Whiteboard page edn is a subset of page schema
|
||||
- it will only contain a single page (for now). The page properties contains 'bindings' etc
|
||||
- it will only contain a single page (for now). The page properties are stored under :logseq.tldraw.* properties and contain 'bindings' etc
|
||||
- blocks will be adapted to tldraw shapes. All blocks's parent is the given page."
|
||||
[file content {:keys [verbose] :or {verbose true}}]
|
||||
(let [_ (when verbose (println "Parsing start: " file))
|
||||
|
|
|
@ -60,7 +60,8 @@
|
|||
#{:id :custom-id :background-color :background_color :heading :collapsed
|
||||
:created-at :updated-at :last-modified-at :created_at :last_modified_at
|
||||
:query-table :query-properties :query-sort-by :query-sort-desc :ls-type
|
||||
:hl-type :hl-page :hl-stamp :logseq.macro-name :logseq.macro-arguments}
|
||||
:hl-type :hl-page :hl-stamp :logseq.macro-name :logseq.macro-arguments
|
||||
:logseq.tldraw.page :logseq.tldraw.shape}
|
||||
(set (map keyword markers))
|
||||
@built-in-extended-properties))
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
"@capacitor/status-bar": "^4.0.0",
|
||||
"@excalidraw/excalidraw": "0.10.0",
|
||||
"@kanru/rage-wasm": "^0.3.0",
|
||||
"@logseq/capacitor-file-sync": "0.0.10",
|
||||
"@logseq/capacitor-file-sync": "0.0.11",
|
||||
"@logseq/react-tweet-embed": "1.3.1-1",
|
||||
"@sentry/react": "^6.18.2",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"https-proxy-agent": "5.0.0",
|
||||
"@sentry/electron": "2.5.1",
|
||||
"posthog-js": "1.10.2",
|
||||
"@logseq/rsapi": "0.0.46",
|
||||
"@logseq/rsapi": "0.0.48",
|
||||
"electron-deeplink": "1.0.10",
|
||||
"abort-controller": "3.0.0"
|
||||
},
|
||||
|
|
|
@ -47,6 +47,9 @@
|
|||
(defn decrypt-with-passphrase [passphrase data]
|
||||
(rsapi/ageDecryptWithPassphrase passphrase data))
|
||||
|
||||
(defn cancel-all-requests []
|
||||
(rsapi/cancelAllRequests))
|
||||
|
||||
(defonce progress-notify-chan "file-sync-progress")
|
||||
(set-progress-callback (fn [error progress-info]
|
||||
(when-not error
|
||||
|
|
|
@ -579,6 +579,9 @@
|
|||
(defmethod handle :decrypt-with-passphrase [_ args]
|
||||
(apply rsapi/decrypt-with-passphrase (rest args)))
|
||||
|
||||
(defmethod handle :cancel-all-requests [_ args]
|
||||
(apply rsapi/cancel-all-requests (rest args)))
|
||||
|
||||
(defmethod handle :default [args]
|
||||
(logger/error "Error: no ipc handler for:" args))
|
||||
|
||||
|
|
|
@ -1895,7 +1895,14 @@
|
|||
priority (priority-cp t)
|
||||
tags (block-tags-cp t)
|
||||
bg-color (:background-color properties)
|
||||
heading (:heading properties)
|
||||
;; `heading-level` is for backward compatiblity, will remove it in later releases
|
||||
heading-level (:block/heading-level t)
|
||||
heading (or
|
||||
(and heading-level
|
||||
(<= heading-level 6)
|
||||
heading-level)
|
||||
(:heading properties))
|
||||
heading (if (true? heading) 2 heading)
|
||||
elem (if heading
|
||||
(keyword (str "h" heading
|
||||
(when block-ref? ".inline")))
|
||||
|
|
|
@ -306,11 +306,8 @@
|
|||
(ui/icon "chevron-left" {:style {:font-size 24}}))])]]))
|
||||
|
||||
(defn- sort-files
|
||||
[progress files]
|
||||
(sort-by (fn [f]
|
||||
(let [percent (or (:percent (get progress f)) 0)]
|
||||
(if (= percent 100) -1 percent)))
|
||||
> files))
|
||||
[files]
|
||||
(sort-by (fn [f] (or (:size f) 0)) > files))
|
||||
|
||||
(rum/defcs ^:large-vars/cleanup-todo indicator <
|
||||
rum/reactive
|
||||
|
@ -331,8 +328,8 @@
|
|||
sync-progress (state/sub [:file-sync/progress (second @fs-sync/graphs-txid)])
|
||||
_ (rum/react file-sync-handler/refresh-file-sync-component)
|
||||
synced-file-graph? (file-sync-handler/synced-file-graph? current-repo)
|
||||
uploading-files (sort-files sync-progress (:current-local->remote-files sync-state))
|
||||
downloading-files (sort-files sync-progress (:current-remote->local-files sync-state))
|
||||
uploading-files (sort-files (:current-local->remote-files sync-state))
|
||||
downloading-files (sort-files (:current-remote->local-files sync-state))
|
||||
queuing-files (:queued-local->remote-files sync-state)
|
||||
history-files (:history sync-state)
|
||||
status (:state sync-state)
|
||||
|
@ -525,56 +522,49 @@
|
|||
[:div.cp__file-sync-related-normal-modal
|
||||
[:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-download")]]
|
||||
|
||||
[:h1.mb-5.text-2xl.text-center.font-bold "Sync a remote graph to local"]
|
||||
[:h1.mb-5.text-2xl.text-center.font-bold (util/format "Sync graph \"%s\" to local"
|
||||
(:GraphName graph))]
|
||||
|
||||
[:div.folder-tip.flex.flex-col.items-center
|
||||
{:style {:border-bottom-right-radius 0 :border-bottom-left-radius 0}}
|
||||
[:h3
|
||||
[:span.flex.space-x-2.leading-none.pb-1
|
||||
(ui/icon "cloud-lock")
|
||||
[:span (:GraphName graph)]
|
||||
[:span.scale-75 (ui/icon "arrow-right")]
|
||||
[:span (ui/icon "folder")]]]
|
||||
[:h4.px-2.-mb-1.5 [:strong "UUID: "] (:GraphUUID graph)]]
|
||||
(ui/button
|
||||
"Open a local directory"
|
||||
:class "block w-full py-4 mt-4"
|
||||
:on-click #(do
|
||||
(state/close-modal!)
|
||||
(fs-sync/<sync-stop)
|
||||
(->
|
||||
(page-handler/ls-dir-files!
|
||||
(fn [{:keys [url]}]
|
||||
(file-sync-handler/init-remote-graph url graph)
|
||||
(js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))
|
||||
|
||||
[:div.-mt-1
|
||||
(ui/button
|
||||
"Open a local directory"
|
||||
:class "w-full rounded-t-none py-4"
|
||||
:on-click #(do
|
||||
(state/close-modal!)
|
||||
(fs-sync/<sync-stop)
|
||||
(->
|
||||
(page-handler/ls-dir-files!
|
||||
(fn [{:keys [url]}]
|
||||
(file-sync-handler/init-remote-graph url graph)
|
||||
(js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))
|
||||
{:empty-dir?-or-pred
|
||||
(fn [ret]
|
||||
(let [empty-dir? (nil? (second ret))]
|
||||
(if-let [root (first ret)]
|
||||
|
||||
{:empty-dir?-or-pred
|
||||
(fn [ret]
|
||||
(let [empty-dir? (nil? (second ret))]
|
||||
(if-let [root (first ret)]
|
||||
;; verify directory
|
||||
(-> (if empty-dir?
|
||||
(p/resolved nil)
|
||||
(if (util/electron?)
|
||||
(ipc/ipc :readGraphTxIdInfo root)
|
||||
(fs-util/read-graphs-txid-info root)))
|
||||
|
||||
;; verify directory
|
||||
(-> (if empty-dir?
|
||||
(p/resolved nil)
|
||||
(if (util/electron?)
|
||||
(ipc/ipc :readGraphTxIdInfo root)
|
||||
(fs-util/read-graphs-txid-info root)))
|
||||
(p/then (fn [^js info]
|
||||
(when (and (not empty-dir?)
|
||||
(or (nil? info)
|
||||
(nil? (second info))
|
||||
(not= (second info) (:GraphUUID graph))))
|
||||
(if (js/confirm "This directory is not empty, are you sure to sync the remote graph to it? Make sure to back up the directory first.")
|
||||
(p/resolved nil)
|
||||
(throw (js/Error. nil)))))))
|
||||
|
||||
(p/then (fn [^js info]
|
||||
(when (and (not empty-dir?)
|
||||
(or (nil? info)
|
||||
(nil? (second info))
|
||||
(not= (second info) (:GraphUUID graph))))
|
||||
(if (js/confirm "This directory is not empty, are you sure to sync the remote graph to it? Make sure to back up the directory first.")
|
||||
(p/resolved nil)
|
||||
(throw (js/Error. nil)))))))
|
||||
;; cancel pick a directory
|
||||
(throw (js/Error. nil)))))})
|
||||
(p/catch (fn [])))))
|
||||
|
||||
;; cancel pick a directory
|
||||
(throw (js/Error. nil)))))})
|
||||
(p/catch (fn [])))))
|
||||
[:p.text-xs.opacity-50.px-1 (ui/icon "alert-circle") " An empty directory or an existing remote graph!"]]])
|
||||
[:div.text-xs.opacity-50.px-1.flex-row.flex.items-center.p-2
|
||||
(ui/icon "alert-circle")
|
||||
[:span.ml-1 " An empty directory or an existing remote graph!"]]])
|
||||
|
||||
(defn pick-dest-to-sync-panel [graph]
|
||||
(fn []
|
||||
|
|
|
@ -54,6 +54,8 @@
|
|||
:block/repeated?
|
||||
:block/created-at
|
||||
:block/updated-at
|
||||
;; TODO: remove this in later releases
|
||||
:block/heading-level
|
||||
:block/file
|
||||
{:block/page [:db/id :block/name :block/original-name :block/journal-day]}
|
||||
{:block/_parent ...}])
|
||||
|
|
|
@ -657,7 +657,8 @@
|
|||
(<update-remote-files [this graph-uuid base-path filepaths local-txid] "local -> remote, return err or txid")
|
||||
(<delete-remote-files [this graph-uuid base-path filepaths local-txid] "return err or txid")
|
||||
(<encrypt-fnames [this graph-uuid fnames])
|
||||
(<decrypt-fnames [this graph-uuid fnames]))
|
||||
(<decrypt-fnames [this graph-uuid fnames])
|
||||
(<cancel-all-requests [this]))
|
||||
|
||||
(defprotocol IRemoteAPI
|
||||
(<user-info [this] "user info")
|
||||
|
@ -718,6 +719,8 @@
|
|||
(recur (dec n)))
|
||||
r))))
|
||||
|
||||
(declare <rsapi-cancel-all-requests)
|
||||
|
||||
(deftype RSAPI [^:mutable graph-uuid' ^:mutable private-key' ^:mutable public-key']
|
||||
IToken
|
||||
(<get-token [this]
|
||||
|
@ -765,6 +768,7 @@
|
|||
(<update-local-files [this graph-uuid base-path filepaths]
|
||||
(println "update-local-files" graph-uuid base-path filepaths)
|
||||
(go
|
||||
(<! (<rsapi-cancel-all-requests))
|
||||
(let [token (<! (<get-token this))]
|
||||
(<! (p->c (ipc/ipc "update-local-files" graph-uuid base-path filepaths token))))))
|
||||
(<download-version-files [this graph-uuid base-path filepaths]
|
||||
|
@ -782,6 +786,7 @@
|
|||
|
||||
(<update-remote-files [this graph-uuid base-path filepaths local-txid]
|
||||
(go
|
||||
(<! (<rsapi-cancel-all-requests))
|
||||
(let [token (<! (<get-token this))]
|
||||
(<! (<retry-rsapi
|
||||
#(p->c (ipc/ipc "update-remote-files" graph-uuid base-path filepaths local-txid token)))))))
|
||||
|
@ -794,10 +799,12 @@
|
|||
#(p->c (ipc/ipc "delete-remote-files" graph-uuid base-path filepaths local-txid token)))))))
|
||||
(<encrypt-fnames [_ graph-uuid fnames] (go (js->clj (<! (p->c (ipc/ipc "encrypt-fnames" graph-uuid fnames))))))
|
||||
(<decrypt-fnames [_ graph-uuid fnames] (go
|
||||
(let [r (<! (p->c (ipc/ipc "decrypt-fnames" graph-uuid fnames)))]
|
||||
(if (instance? ExceptionInfo r)
|
||||
(ex-info "decrypt-failed" {:fnames fnames} (ex-cause r))
|
||||
(js->clj r))))))
|
||||
(let [r (<! (p->c (ipc/ipc "decrypt-fnames" graph-uuid fnames)))]
|
||||
(if (instance? ExceptionInfo r)
|
||||
(ex-info "decrypt-failed" {:fnames fnames} (ex-cause r))
|
||||
(js->clj r)))))
|
||||
(<cancel-all-requests [_]
|
||||
(p->c (ipc/ipc "cancel-all-requests"))))
|
||||
|
||||
|
||||
(deftype ^:large-vars/cleanup-todo CapacitorAPI [^:mutable graph-uuid' ^:mutable private-key ^:mutable public-key']
|
||||
|
@ -873,10 +880,10 @@
|
|||
(let [token (<! (<get-token this))
|
||||
r (<! (<retry-rsapi
|
||||
#(p->c (.updateLocalVersionFiles mobile-util/file-sync
|
||||
(clj->js {:graphUUID graph-uuid
|
||||
:basePath base-path
|
||||
:filePaths filepaths
|
||||
:token token})))))]
|
||||
(clj->js {:graphUUID graph-uuid
|
||||
:basePath base-path
|
||||
:filePaths filepaths
|
||||
:token token})))))]
|
||||
r)))
|
||||
|
||||
(<delete-local-files [_ graph-uuid base-path filepaths]
|
||||
|
@ -903,15 +910,15 @@
|
|||
|
||||
(<delete-remote-files [this graph-uuid _base-path filepaths local-txid]
|
||||
(go
|
||||
(let [token (<! (<get-token this))
|
||||
r (<! (p->c (.deleteRemoteFiles mobile-util/file-sync
|
||||
(clj->js {:graphUUID graph-uuid
|
||||
:filePaths filepaths
|
||||
:txid local-txid
|
||||
:token token}))))]
|
||||
(if (instance? ExceptionInfo r)
|
||||
r
|
||||
(get (js->clj r) "txid")))))
|
||||
(let [token (<! (<get-token this))
|
||||
r (<! (p->c (.deleteRemoteFiles mobile-util/file-sync
|
||||
(clj->js {:graphUUID graph-uuid
|
||||
:filePaths filepaths
|
||||
:txid local-txid
|
||||
:token token}))))]
|
||||
(if (instance? ExceptionInfo r)
|
||||
r
|
||||
(get (js->clj r) "txid")))))
|
||||
|
||||
(<encrypt-fnames [_ graph-uuid fnames]
|
||||
(go
|
||||
|
@ -927,7 +934,9 @@
|
|||
:filePaths fnames}))))]
|
||||
(if (instance? ExceptionInfo r)
|
||||
(ex-info "decrypt-failed" {:fnames fnames} (ex-cause r))
|
||||
(get (js->clj r) "value"))))))
|
||||
(get (js->clj r) "value")))))
|
||||
(<cancel-all-requests [_]
|
||||
(p->c (.cancelAllRequests mobile-util/file-sync))))
|
||||
|
||||
(def rsapi (cond
|
||||
(util/electron?)
|
||||
|
@ -942,6 +951,11 @@
|
|||
:else
|
||||
nil))
|
||||
|
||||
(defn <rsapi-cancel-all-requests []
|
||||
(go
|
||||
(when rsapi
|
||||
(<! (<cancel-all-requests rsapi)))))
|
||||
|
||||
;;; ### remote & rs api exceptions
|
||||
(defn sync-stop-when-api-flying?
|
||||
[exp]
|
||||
|
@ -2803,6 +2817,7 @@
|
|||
(when ops-chan (async/close! ops-chan))
|
||||
(stop-local->remote! local->remote-syncer)
|
||||
(stop-remote->local! remote->local-syncer)
|
||||
(<! (<rsapi-cancel-all-requests))
|
||||
(debug/pprint ["stop sync-manager, graph-uuid" graph-uuid "base-path" base-path])
|
||||
(swap! *sync-state sync-state--update-state ::stop)
|
||||
(loop []
|
||||
|
|
|
@ -2795,7 +2795,8 @@
|
|||
value (gobj/get input "value")
|
||||
c (util/nth-safe value (dec current-pos))
|
||||
[key-code k code is-processed?]
|
||||
(if (and (mobile-util/native-android?)
|
||||
(if (and c
|
||||
(mobile-util/native-android?)
|
||||
(or (= key-code 229)
|
||||
(= key-code 0)))
|
||||
[(.charCodeAt value (dec current-pos))
|
||||
|
|
|
@ -163,12 +163,13 @@
|
|||
state/set-state! :sync-graph/init? false)))
|
||||
|
||||
(defmethod handle :graph/switch [[_ graph opts]]
|
||||
(if (or (not (false? (get @outliner-file/*writes-finished? graph)))
|
||||
(:sync-graph/init? @state/state))
|
||||
(graph-switch-on-persisted graph opts)
|
||||
(notification/show!
|
||||
"Please wait seconds until all changes are saved for the current graph."
|
||||
:warning)))
|
||||
(let [opts (if (false? (:persist? opts)) opts (assoc opts :persist? true))]
|
||||
(if (or (not (false? (get @outliner-file/*writes-finished? graph)))
|
||||
(:sync-graph/init? @state/state))
|
||||
(graph-switch-on-persisted graph opts)
|
||||
(notification/show!
|
||||
"Please wait seconds until all changes are saved for the current graph."
|
||||
:warning))))
|
||||
|
||||
(defmethod handle :graph/pick-dest-to-sync [[_ graph]]
|
||||
(state/set-modal!
|
||||
|
|
|
@ -233,24 +233,24 @@
|
|||
|
||||
;; file-sync
|
||||
:file-sync/jstour-inst nil
|
||||
:file-sync/remote-graphs {:loading false :graphs nil}
|
||||
:file-sync/sync-manager nil
|
||||
:file-sync/sync-state-manager nil
|
||||
:file-sync/sync-state nil
|
||||
:file-sync/sync-uploading-files nil
|
||||
:file-sync/sync-downloading-files nil
|
||||
:file-sync/onboarding-state (or (storage/get :file-sync/onboarding-state)
|
||||
{:welcome false})
|
||||
|
||||
:encryption/graph-parsing? false
|
||||
|
||||
:ui/loading? {}
|
||||
:file-sync/remote-graphs {:loading false :graphs nil}
|
||||
:file-sync/sync-manager nil
|
||||
:file-sync/sync-state nil
|
||||
:file-sync/sync-uploading-files nil
|
||||
:file-sync/sync-downloading-files nil
|
||||
:file-sync/set-remote-graph-password-result {}
|
||||
;; graph-uuid -> {file-path -> payload}
|
||||
:file-sync/progress {}
|
||||
:file-sync/start {}
|
||||
;; graph -> epoch
|
||||
:file-sync/last-synced-at {}
|
||||
|
||||
:encryption/graph-parsing? false
|
||||
|
||||
:ui/loading? {}
|
||||
:feature/enable-sync? (storage/get :logseq-sync-enabled)
|
||||
|
||||
:file/rename-event-chan (async/chan 100)
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { SvgPathUtils, TLDrawShape, TLDrawShapeProps } from '@tldraw/core'
|
||||
import { SVGContainer, TLComponentProps } from '@tldraw/react'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { computed, makeObservable } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import getStroke, { getStrokeOutlinePoints, getStrokePoints, StrokeOptions } from 'perfect-freehand'
|
||||
import getStroke, {
|
||||
getStrokeOutlinePoints,
|
||||
getStrokePoints,
|
||||
StrokeOptions,
|
||||
StrokePoint,
|
||||
} from 'perfect-freehand'
|
||||
import { CustomStyleProps, withClampedStyles } from './style-props'
|
||||
|
||||
export interface PencilShapeProps extends TLDrawShapeProps, CustomStyleProps {
|
||||
|
@ -53,6 +59,17 @@ function getDrawStrokePathTDSnapshot(shape: PencilShapeProps) {
|
|||
return path
|
||||
}
|
||||
|
||||
function getSolidStrokePathTDSnapshot(shape: PencilShapeProps) {
|
||||
const { points } = shape
|
||||
if (points.length < 2) return 'M 0 0 L 0 0'
|
||||
const options = getFreehandOptions(shape)
|
||||
const strokePoints = getDrawStrokePoints(shape, options)
|
||||
const last = points[points.length - 1]
|
||||
if (!Vec.isEqual(strokePoints[0].point, last)) strokePoints.push({ point: last } as StrokePoint)
|
||||
const path = SvgPathUtils.getSvgPathFromStrokePoints(strokePoints)
|
||||
return path
|
||||
}
|
||||
|
||||
export class PencilShape extends TLDrawShape<PencilShapeProps> {
|
||||
constructor(props = {} as Partial<PencilShapeProps>) {
|
||||
super(props)
|
||||
|
@ -109,11 +126,13 @@ export class PencilShape extends TLDrawShape<PencilShapeProps> {
|
|||
} = this
|
||||
return (
|
||||
<path
|
||||
pointerEvents="none"
|
||||
d={pointsPath}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeWidth={strokeWidth / 2}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
stroke={stroke}
|
||||
fill={stroke}
|
||||
pointerEvents="all"
|
||||
strokeDasharray={strokeType === 'dashed' ? '12 4' : undefined}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type { StrokePoint } from 'perfect-freehand'
|
||||
|
||||
export class SvgPathUtils {
|
||||
static getCurvedPathForPolygon(points: number[][]) {
|
||||
if (points.length < 3) {
|
||||
|
@ -69,6 +71,39 @@ export class SvgPathUtils {
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an array of stroke points into a path of quadradic curves.
|
||||
*
|
||||
* @param points - The stroke points returned from perfect-freehand
|
||||
*/
|
||||
static getSvgPathFromStrokePoints(points: StrokePoint[], closed = false): string {
|
||||
const len = points.length
|
||||
|
||||
if (len < 4) {
|
||||
return ``
|
||||
}
|
||||
|
||||
let a = points[0].point
|
||||
let b = points[1].point
|
||||
const c = points[2].point
|
||||
|
||||
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(2)},${b[1].toFixed(
|
||||
2
|
||||
)} ${average(b[0], c[0]).toFixed(2)},${average(b[1], c[1]).toFixed(2)} T`
|
||||
|
||||
for (let i = 2, max = len - 1; i < max; i++) {
|
||||
a = points[i].point
|
||||
b = points[i + 1].point
|
||||
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(2)} `
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
result += 'Z'
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
function average(a: number, b: number): number {
|
||||
|
|
|
@ -480,10 +480,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@kanru/rage-wasm/-/rage-wasm-0.3.0.tgz#de96b1fda1f781ff401d43b50d0f95b7338c4399"
|
||||
integrity sha512-2LMRS27nNJPqFNpRQL7kXG0kgBeIPo63KM6u0Xu6Es5XIS7LP4MFtdHkCg8Pt7IhMM7GuOa2YnzAZgKBxE1lcw==
|
||||
|
||||
"@logseq/capacitor-file-sync@0.0.10":
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@logseq/capacitor-file-sync/-/capacitor-file-sync-0.0.10.tgz#36575b369a4fff83e71b282c19cc948d60309809"
|
||||
integrity sha512-O4bCHLym7+DnqCoO57D1Ii6ec6tcZBjp8SzFEYCKE9mvHsX5aFmCA61XUHgNEFovocQ3gBMJvi6GKSJBcGwT3Q==
|
||||
"@logseq/capacitor-file-sync@0.0.11":
|
||||
version "0.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@logseq/capacitor-file-sync/-/capacitor-file-sync-0.0.11.tgz#aa84f320d73c292d8dd9e483e0ccf73baa2e021b"
|
||||
integrity sha512-ZCV/2fp8iPpShk+rk+6aH3aFZID7BKhSlyw/f4fcSCapbh6im638GR2fi9RPRc1eg+0vbtrx6UzG7Vbw+Kz2aQ==
|
||||
|
||||
"@logseq/react-tweet-embed@1.3.1-1":
|
||||
version "1.3.1-1"
|
||||
|
|
Loading…
Reference in New Issue