From d9d2cc7b4e3ae6419a7b04da807b45855cb40f66 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 May 2025 13:52:51 +0200 Subject: [PATCH] :sparkles: Split byte buffer helpers from types path impl --- .gitignore | 1 + common/src/app/common/buffer.cljc | 141 +++++++++++++ common/src/app/common/types/path/impl.cljc | 221 ++++++++------------- common/test/common_tests/buffer_test.cljc | 50 +++++ 4 files changed, 276 insertions(+), 137 deletions(-) create mode 100644 common/src/app/common/buffer.cljc create mode 100644 common/test/common_tests/buffer_test.cljc diff --git a/.gitignore b/.gitignore index 416950e134..7e3654e7d8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ /*.zip /.clj-kondo/.cache /_dump +/notes /backend/*.md /backend/*.sql /backend/*.txt diff --git a/common/src/app/common/buffer.cljc b/common/src/app/common/buffer.cljc new file mode 100644 index 0000000000..c508f8dbb4 --- /dev/null +++ b/common/src/app/common/buffer.cljc @@ -0,0 +1,141 @@ +;; 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.buffer + "A collection of helpers and macros for work with byte buffers" + (:refer-clojure :exclude [clone]) + (:require + [app.common.uuid :as uuid]) + #?(:cljs + (:require-macros [app.common.buffer]) + :clj + (:import [java.nio ByteBuffer ByteOrder]))) + +(defmacro read-byte + [target offset] + (if (:ns &env) + `(.getInt8 ~target ~offset true) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(long (.get ~target ~offset))))) + +(defmacro read-bool + [target offset] + (if (:ns &env) + `(== 1 (.getInt8 ~target ~offset true)) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(== 1 (.get ~target ~offset))))) + +(defmacro read-short + [target offset] + (if (:ns &env) + `(.getInt16 ~target ~offset true) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(.getShort ~target ~offset)))) + +(defmacro read-int + [target offset] + (if (:ns &env) + `(.getInt32 ~target ~offset true) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(long (.getInt ~target ~offset))))) + +(defmacro read-float + [target offset] + (if (:ns &env) + `(.getFloat32 ~target ~offset true) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(double (.getFloat ~target ~offset))))) + +(defmacro read-uuid + [target offset] + (if (:ns &env) + `(let [a# (.getUint32 ~target (+ ~offset 0) true) + b# (.getUint32 ~target (+ ~offset 4) true) + c# (.getUint32 ~target (+ ~offset 8) true) + d# (.getUint32 ~target (+ ~offset 12) true)] + (uuid/from-unsigned-parts a# b# c# d#)) + + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(try + (.order ~target ByteOrder/BIG_ENDIAN) + (let [msb# (.getLong ~target (+ ~offset 0)) + lsb# (.getLong ~target (+ ~offset 8))] + (java.util.UUID. (long msb#) (long lsb#))) + (finally + (.order ~target ByteOrder/LITTLE_ENDIAN)))))) + +(defmacro write-byte + [target offset value] + (if (:ns &env) + `(.setInt8 ~target ~offset ~value true) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(.put ~target ~offset (unchecked-byte ~value))))) + +(defmacro write-short + [target offset value] + (if (:ns &env) + `(.setInt16 ~target ~offset ~value true) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(.putShort ~target ~offset (unchecked-short ~value))))) + +(defmacro write-int + [target offset value] + (if (:ns &env) + `(.setInt32 ~target ~offset ~value true) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(.putInt ~target ~offset (unchecked-int ~value))))) + +(defmacro write-float + [target offset value] + (if (:ns &env) + `(.setFloat32 ~target ~offset ~value true) + (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] + `(.putFloat ~target ~offset (unchecked-float ~value))))) + +(defmacro write-uuid + [target offset value] + (if (:ns &env) + `(let [barray# (uuid/get-u32 ~value)] + (.setUint32 ~target (+ ~offset 0) (aget barray# 0) true) + (.setUint32 ~target (+ ~offset 4) (aget barray# 1) true) + (.setUint32 ~target (+ ~offset 8) (aget barray# 2) true) + (.setUint32 ~target (+ ~offset 12) (aget barray# 3) true)) + + (let [target (with-meta target {:tag 'java.nio.ByteBuffer}) + value (with-meta value {:tag 'java.util.UUID})] + `(try + (.order ~target ByteOrder/BIG_ENDIAN) + (.putLong ~target (+ ~offset 0) (.getMostSignificantBits ~value)) + (.putLong ~target (+ ~offset 8) (.getLeastSignificantBits ~value)) + (finally + (.order ~target ByteOrder/LITTLE_ENDIAN)))))) + +(defn allocate + [size] + #?(:clj (let [buffer (ByteBuffer/allocate (int size))] + (.order buffer ByteOrder/LITTLE_ENDIAN)) + :cljs (new js/DataView (new js/ArrayBuffer size)))) + +(defn clone + [buffer] + #?(:clj + (let [src (.array ^ByteBuffer buffer) + len (alength ^bytes src) + dst (byte-array len)] + (System/arraycopy src 0 dst 0 len) + (let [buffer (ByteBuffer/wrap dst)] + (.order buffer ByteOrder/LITTLE_ENDIAN))) + :cljs + (let [src-view (js/Uint32Array. buffer) + dst-buff (js/ArrayBuffer. (.-byteLength buffer)) + dst-view (js/Uint32Array. dst-buff)] + (.set dst-view src-view) + dst-buff))) + +(defn buffer? + [o] + #?(:clj (instance? ByteBuffer o) + :cljs (instance? js/DataView o))) diff --git a/common/src/app/common/types/path/impl.cljc b/common/src/app/common/types/path/impl.cljc index ecafa45a7b..a98e46468c 100644 --- a/common/src/app/common/types/path/impl.cljc +++ b/common/src/app/common/types/path/impl.cljc @@ -7,13 +7,13 @@ (ns app.common.types.path.impl "Contains schemas and data type implementation for PathData binary and plain formats" - #?(:cljs - (:require-macros [app.common.types.path.impl :refer [read-float read-short write-float write-short]])) (:refer-clojure :exclude [-lookup -reduce]) + #?(:cljs (:require-macros [app.common.types.path.impl])) (:require #?(:clj [app.common.fressian :as fres]) #?(:clj [clojure.data.json :as json]) #?(:cljs [app.common.weak-map :as weak-map]) + [app.common.buffer :as buf] [app.common.data.macros :as dm] [app.common.schema :as sm] [app.common.schema.generators :as sg] @@ -42,93 +42,42 @@ ;; IMPL HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defmacro read-short - [target offset] - (if (:ns &env) - `(.getInt16 ~target ~offset true) - (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] - `(.getShort ~target ~offset)))) - -(defmacro read-float - [target offset] - (if (:ns &env) - `(.getFloat32 ~target ~offset true) - (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] - `(double (.getFloat ~target ~offset))))) - -(defmacro write-float - [target offset value] - (if (:ns &env) - `(.setFloat32 ~target ~offset ~value true) - (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] - `(.putFloat ~target ~offset ~value)))) - -(defmacro write-short - [target offset value] - (if (:ns &env) - `(.setInt16 ~target ~offset ~value true) - (let [target (with-meta target {:tag 'java.nio.ByteBuffer})] - `(.putShort ~target ~offset ~value)))) - (defmacro with-cache "A helper macro that facilitates cache handling for content instance, only relevant on CLJS" [target key & expr] (if (:ns &env) - (let [cache (gensym "cache-") - target (with-meta target {:tag 'js})] - `(let [~cache (.-cache ~target) - ~'result (.get ~cache ~key)] + (let [target (with-meta target {:tag 'js})] + `(let [~'cache (.-cache ~target) + ~'result (.get ~'cache ~key)] (if ~'result (do ~'result) (let [~'result (do ~@expr)] - (.set ~cache ~key ~'result) + (.set ~'cache ~key ~'result) ~'result)))) `(do ~@expr))) -(defn- allocate - [n-segments] - #?(:clj (let [buffer (ByteBuffer/allocate (* n-segments SEGMENT-BYTE-SIZE))] - (.order buffer ByteOrder/LITTLE_ENDIAN)) - :cljs (new js/ArrayBuffer (* n-segments SEGMENT-BYTE-SIZE)))) - -(defn- clone-buffer - [buffer] - #?(:clj - (let [src (.array ^ByteBuffer buffer) - len (alength ^bytes src) - dst (byte-array len)] - (System/arraycopy src 0 dst 0 len) - (let [buffer (ByteBuffer/wrap dst)] - (.order buffer ByteOrder/LITTLE_ENDIAN))) - :cljs - (let [src-view (js/Uint32Array. buffer) - dst-buff (js/ArrayBuffer. (.-byteLength buffer)) - dst-view (js/Uint32Array. dst-buff)] - (.set dst-view src-view) - dst-buff))) - (defn- impl-transform-segment "Apply a transformation to a segment located under specified offset" [buffer offset a b c d e f] - (let [t (read-short buffer offset)] + (let [t (buf/read-short buffer offset)] (case t (1 2) - (let [x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24)) + (let [x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24)) x (+ (* x a) (* y c) e) y (+ (* x b) (* y d) f)] - (write-float buffer (+ offset 20) x) - (write-float buffer (+ offset 24) y)) + (buf/write-float buffer (+ offset 20) x) + (buf/write-float buffer (+ offset 24) y)) 3 - (let [c1x (read-float buffer (+ offset 4)) - c1y (read-float buffer (+ offset 8)) - c2x (read-float buffer (+ offset 12)) - c2y (read-float buffer (+ offset 16)) - x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24)) + (let [c1x (buf/read-float buffer (+ offset 4)) + c1y (buf/read-float buffer (+ offset 8)) + c2x (buf/read-float buffer (+ offset 12)) + c2y (buf/read-float buffer (+ offset 16)) + x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24)) c1x (+ (* c1x a) (* c1y c) e) c1y (+ (* c1x b) (* c1y d) f) @@ -137,12 +86,12 @@ x (+ (* x a) (* y c) e) y (+ (* x b) (* y d) f)] - (write-float buffer (+ offset 4) c1x) - (write-float buffer (+ offset 8) c1y) - (write-float buffer (+ offset 12) c2x) - (write-float buffer (+ offset 16) c2y) - (write-float buffer (+ offset 20) x) - (write-float buffer (+ offset 24) y)) + (buf/write-float buffer (+ offset 4) c1x) + (buf/write-float buffer (+ offset 8) c1y) + (buf/write-float buffer (+ offset 12) c2x) + (buf/write-float buffer (+ offset 16) c2y) + (buf/write-float buffer (+ offset 20) x) + (buf/write-float buffer (+ offset 24) y)) nil))) @@ -166,13 +115,13 @@ result (transient initial)] (if (< index size) (let [offset (* index SEGMENT-BYTE-SIZE) - type (read-short buffer offset) - c1x (read-float buffer (+ offset 4)) - c1y (read-float buffer (+ offset 8)) - c2x (read-float buffer (+ offset 12)) - c2y (read-float buffer (+ offset 16)) - x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24)) + type (buf/read-short buffer offset) + c1x (buf/read-float buffer (+ offset 4)) + c1y (buf/read-float buffer (+ offset 8)) + c2x (buf/read-float buffer (+ offset 12)) + c2y (buf/read-float buffer (+ offset 16)) + x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24)) type (case type 1 :line-to 2 :move-to @@ -191,13 +140,13 @@ result initial] (if (< index size) (let [offset (* index SEGMENT-BYTE-SIZE) - type (read-short buffer offset) - c1x (read-float buffer (+ offset 4)) - c1y (read-float buffer (+ offset 8)) - c2x (read-float buffer (+ offset 12)) - c2y (read-float buffer (+ offset 16)) - x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24)) + type (buf/read-short buffer offset) + c1x (buf/read-float buffer (+ offset 4)) + c1y (buf/read-float buffer (+ offset 8)) + c2x (buf/read-float buffer (+ offset 12)) + c2y (buf/read-float buffer (+ offset 16)) + x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24)) type (case type 1 :line-to 2 :move-to @@ -212,13 +161,13 @@ (defn impl-lookup [buffer index f] (let [offset (* index SEGMENT-BYTE-SIZE) - type (read-short buffer offset) - c1x (read-float buffer (+ offset 4)) - c1y (read-float buffer (+ offset 8)) - c2x (read-float buffer (+ offset 12)) - c2y (read-float buffer (+ offset 16)) - x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24)) + type (buf/read-short buffer offset) + c1x (buf/read-float buffer (+ offset 4)) + c1y (buf/read-float buffer (+ offset 8)) + c2x (buf/read-float buffer (+ offset 12)) + c2y (buf/read-float buffer (+ offset 16)) + x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24)) type (case type 1 :line-to 2 :move-to @@ -230,27 +179,27 @@ (defn- to-string-segment* [buffer offset type ^StringBuilder builder] (case (long type) - 1 (let [x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24))] + 1 (let [x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24))] (doto builder (.append "M") (.append x) (.append ",") (.append y))) - 2 (let [x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24))] + 2 (let [x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24))] (doto builder (.append "L") (.append x) (.append ",") (.append y))) - 3 (let [c1x (read-float buffer (+ offset 4)) - c1y (read-float buffer (+ offset 8)) - c2x (read-float buffer (+ offset 12)) - c2y (read-float buffer (+ offset 16)) - x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24))] + 3 (let [c1x (buf/read-float buffer (+ offset 4)) + c1y (buf/read-float buffer (+ offset 8)) + c2x (buf/read-float buffer (+ offset 12)) + c2y (buf/read-float buffer (+ offset 16)) + x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24))] (doto builder (.append "C") (.append c1x) @@ -275,7 +224,7 @@ (loop [index 0] (when (< index size) (let [offset (* index SEGMENT-BYTE-SIZE) - type (read-short buffer offset)] + type (buf/read-short buffer offset)] (to-string-segment* buffer offset type builder) (recur (inc index))))) @@ -285,26 +234,26 @@ "Read segment from binary buffer at specified index" [buffer index] (let [offset (* index SEGMENT-BYTE-SIZE) - type (read-short buffer offset)] + type (buf/read-short buffer offset)] (case (long type) - 1 (let [x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24))] + 1 (let [x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24))] {:command :move-to :params {:x (double x) :y (double y)}}) - 2 (let [x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24))] + 2 (let [x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24))] {:command :line-to :params {:x (double x) :y (double y)}}) - 3 (let [c1x (read-float buffer (+ offset 4)) - c1y (read-float buffer (+ offset 8)) - c2x (read-float buffer (+ offset 12)) - c2y (read-float buffer (+ offset 16)) - x (read-float buffer (+ offset 20)) - y (read-float buffer (+ offset 24))] + 3 (let [c1x (buf/read-float buffer (+ offset 4)) + c1y (buf/read-float buffer (+ offset 8)) + c2x (buf/read-float buffer (+ offset 12)) + c2y (buf/read-float buffer (+ offset 16)) + x (buf/read-float buffer (+ offset 20)) + y (buf/read-float buffer (+ offset 24))] {:command :curve-to :params {:x (double x) :y (double y) @@ -339,7 +288,7 @@ ITransformable (-transform [_ m] - (let [buffer (clone-buffer buffer)] + (let [buffer (buf/clone buffer)] (impl-transform buffer m size) (PathData. size buffer nil))) @@ -427,7 +376,7 @@ ITransformable (-transform [this m] - (let [buffer (clone-buffer buffer) + (let [buffer (buf/clone buffer) dview (js/DataView. buffer)] (impl-transform dview m size) (PathData. size buffer dview (weak-map/create) nil))) @@ -688,10 +637,8 @@ [segments] (assert (check-segments segments)) - (let [total (count segments) - #?@(:cljs [buffer' (allocate total) - buffer (new js/DataView buffer')] - :clj [buffer (allocate total)])] + (let [total (count segments) + buffer (buf/allocate (* total SEGMENT-BYTE-SIZE))] (loop [index 0] (when (< index total) (let [segment (nth segments index) @@ -701,18 +648,18 @@ (let [params (get segment :params) x (float (get params :x)) y (float (get params :y))] - (write-short buffer offset 1) - (write-float buffer (+ offset 20) x) - (write-float buffer (+ offset 24) y)) + (buf/write-short buffer offset 1) + (buf/write-float buffer (+ offset 20) x) + (buf/write-float buffer (+ offset 24) y)) :line-to (let [params (get segment :params) x (float (get params :x)) y (float (get params :y))] - (write-short buffer offset 2) - (write-float buffer (+ offset 20) x) - (write-float buffer (+ offset 24) y)) + (buf/write-short buffer offset 2) + (buf/write-float buffer (+ offset 20) x) + (buf/write-float buffer (+ offset 24) y)) :curve-to (let [params (get segment :params) @@ -723,16 +670,16 @@ c2x (float (get params :c2x x)) c2y (float (get params :c2y y))] - (write-short buffer offset 3) - (write-float buffer (+ offset 4) c1x) - (write-float buffer (+ offset 8) c1y) - (write-float buffer (+ offset 12) c2x) - (write-float buffer (+ offset 16) c2y) - (write-float buffer (+ offset 20) x) - (write-float buffer (+ offset 24) y)) + (buf/write-short buffer offset 3) + (buf/write-float buffer (+ offset 4) c1x) + (buf/write-float buffer (+ offset 8) c1y) + (buf/write-float buffer (+ offset 12) c2x) + (buf/write-float buffer (+ offset 16) c2y) + (buf/write-float buffer (+ offset 20) x) + (buf/write-float buffer (+ offset 24) y)) :close-path - (write-short buffer offset 4)) + (buf/write-short buffer offset 4)) (recur (inc index))))) (from-bytes buffer))) diff --git a/common/test/common_tests/buffer_test.cljc b/common/test/common_tests/buffer_test.cljc new file mode 100644 index 0000000000..b4f822e526 --- /dev/null +++ b/common/test/common_tests/buffer_test.cljc @@ -0,0 +1,50 @@ +;; 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 common-tests.buffer-test + (:require + [app.common.buffer :as buf] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(t/deftest allocate + (let [b (buf/allocate 1)] + (t/is (buf/buffer? b)))) + +(t/deftest rw-byte + (let [b (buf/allocate 1)] + (buf/write-byte b 0 123) + (let [res (buf/read-byte b 0)] + (t/is (= 123 res))) + + (buf/write-byte b 0 252) + (let [res (buf/read-byte b 0)] + (t/is (= -4 res))))) + +(t/deftest rw-int + (let [b (buf/allocate 4)] + (buf/write-int b 0 123) + (let [res (buf/read-int b 0)] + (t/is (= 123 res))))) + +(t/deftest rw-float + (let [b (buf/allocate 4)] + (buf/write-float b 0 123) + (let [res (buf/read-float b 0)] + (t/is (= 123.0 res))))) + +(t/deftest rw-short + (let [b (buf/allocate 2)] + (buf/write-short b 0 123) + (let [res (buf/read-short b 0)] + (t/is (= 123 res))))) + +(t/deftest rw-uuid + (let [b (buf/allocate 16) + id (uuid/next)] + (buf/write-uuid b 0 id) + (let [res (buf/read-uuid b 0)] + (t/is (= id res)))))