diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index c550c150a5..63bd8350ad 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -379,6 +379,23 @@ (->> (rx/from added) (rx/map process-wasm-object))))))) + (when render-wasm? + (->> stream + (rx/filter (ptk/type? :wasm/position-data)) + (rx/map deref) + (rx/filter + (fn [{:keys [position-data]}] + (some? position-data))) + (rx/map + (fn [{:keys [id position-data]}] + (prn "???" id position-data) + (dwsh/update-shapes + [id] + (fn [shape] + (.log js/console (clj->js shape)) + (assoc shape :position-data position-data)) + {:ignore-wasm? true}))))) + (->> stream (rx/filter dch/commit?) (rx/map deref) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 3df0560a32..6d7571daab 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -50,7 +50,8 @@ ([ids update-fn] (update-shapes ids update-fn nil)) ([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id - ignore-touched undo-group with-objects? changed-sub-attr] + ignore-touched undo-group with-objects? changed-sub-attr + ignore-wasm?] :or {reg-objects? false save-undo? true stack-undo? false @@ -89,6 +90,7 @@ :ignore-tree ignore-tree :ignore-touched ignore-touched :with-objects? with-objects?}) + (assoc :ignore-wasm? ignore-wasm?) (cond-> undo-group (pcb/set-undo-group undo-group))) diff --git a/frontend/src/app/main/ui/workspace/viewport/debug.cljs b/frontend/src/app/main/ui/workspace/viewport/debug.cljs index c14ad650bf..17ec8a86bd 100644 --- a/frontend/src/app/main/ui/workspace/viewport/debug.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/debug.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.viewport.debug (:require + [app.render-wasm.api :as wasm.api] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] @@ -275,3 +276,29 @@ :y2 (:y end-p) :style {:stroke "red" :stroke-width (/ 1 zoom)}}]))])))) + +(mf/defc debug-text-position-data + {::mf/wrap-props false} + [props] + (let [objects (unchecked-get props "objects") + zoom (unchecked-get props "zoom") + selected-shapes (unchecked-get props "selected-shapes") + + selected-text + (when (and (= (count selected-shapes) 1) (= :text (-> selected-shapes first :type))) + (first selected-shapes)) + + position-data + (when selected-text + (wasm.api/calculate-position-data selected-text))] + + (for [{:keys [x y width height]} position-data] + [:rect {:x x + :y y + :width width + :height height + :fill "none" + :strokeWidth 1 + :stroke "red"}] + + ))) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index a7b42452cf..193c9e98a4 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -635,6 +635,10 @@ :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) + [:& wvd/debug-text-position-data {:selected-shapes selected-shapes + :objects base-objects + :zoom zoom}] + (when show-selection-handlers? [:g.selection-handlers {:clipPath "url(#clip-handlers)"} (when-not text-editing? diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 0eb0367e1e..673e1b937f 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -7,6 +7,9 @@ (ns app.render-wasm.api "A WASM based render API" (:require + [potok.v2.core :as ptk] + [app.main.data.helpers :as dsh] + [app.main.ui.shapes.text] ["react-dom/server" :as rds] [app.common.data :as d] [app.common.data.macros :as dm] @@ -127,10 +130,18 @@ (render ts))))) (declare get-text-dimensions) +(declare calculate-position-data) (defn update-text-rect! [id] (when wasm/context-initialized? + (let [objects (dsh/lookup-page-objects @st/state) + shape (get objects id) + position-data (calculate-position-data shape)] + (.log js/console (:name shape) (clj->js position-data)) + (st/emit! + (ptk/data-event :wasm/position-data {:id id :position-data position-data}))) + (mw/emit! {:cmd :index/update-text-rect :page-id (:current-page-id @st/state) @@ -988,10 +999,7 @@ (run! (fn [id] (f/update-text-layout id) - (mw/emit! {:cmd :index/update-text-rect - :page-id (:current-page-id @st/state) - :shape-id id - :dimensions (get-text-dimensions id)}))))) + (update-text-rect! id))))) (defn process-pending ([shapes thumbnails full on-complete] @@ -1347,6 +1355,58 @@ (h/call wasm/internal-module "_end_temp_objects") content))) +(def POSITION-DATA-U8-SIZE 36) +(def POSITION-DATA-U32-SIZE (/ POSITION-DATA-U8-SIZE 4)) + +(defn calculate-position-data + [shape] + (use-shape (:id shape)) + (let [heapf32 (mem/get-heap-f32) + heapu32 (mem/get-heap-u32) + offset (-> (h/call wasm/internal-module "_calc_position_data") + (mem/->offset-32)) + length (aget heapu32 offset) + + max-offset (+ offset 1 (* length POSITION-DATA-U32-SIZE)) + + result + (loop [result (transient []) + offset (inc offset)] + (if (< offset max-offset) + (let [entry (dr/read-position-data-entry heapu32 heapf32 offset)] + (recur (conj! result entry) + (+ offset POSITION-DATA-U32-SIZE))) + (persistent! result))) + + result + (->> result + (mapv + (fn [{:keys [paragraph span start-pos end-pos direction x y width height]}] + (let [content (:content shape) + element (-> content :children + (get 0) :children ;; paragraph-set + (get paragraph) :children ;; paragraph + (get span)) + text (subs (:text element) start-pos end-pos)] + + {:x x + :y y + :width width + :height height + :direction direction + :font-family (get element :font-family) + :font-size (get element :font-size) + :font-weight (get element :font-weight) + :text-transform (get element :text-transform) + :text-decoration (get element :text-decoration) + :letter-spacing (get element :letter-spacing) + :font-style (get element :font-style) + :fills (get element :fills) + :text text}))))] + (mem/free) + + result)) + (defn init-wasm-module [module] (let [default-fn (unchecked-get module "default") diff --git a/frontend/src/app/render_wasm/deserializers.cljs b/frontend/src/app/render_wasm/deserializers.cljs index dd718d82c4..dbae51fa97 100644 --- a/frontend/src/app/render_wasm/deserializers.cljs +++ b/frontend/src/app/render_wasm/deserializers.cljs @@ -45,4 +45,23 @@ :center (gpt/point cx cy) :transform (gmt/matrix a b c d e f)})) - +(defn read-position-data-entry + [heapu32 heapf32 offset] + (let [paragraph (aget heapu32 (+ offset 0)) + span (aget heapu32 (+ offset 1)) + start-pos (aget heapu32 (+ offset 2)) + end-pos (aget heapu32 (+ offset 3)) + x (aget heapf32 (+ offset 4)) + y (aget heapf32 (+ offset 5)) + width (aget heapf32 (+ offset 6)) + height (aget heapf32 (+ offset 7)) + direction (aget heapu32 (+ offset 8))] + {:paragraph paragraph + :span span + :start-pos start-pos + :end-pos end-pos + :x x + :y y + :width width + :height height + :direction direction})) diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index 7f55f10a46..35d47699b7 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -6,6 +6,7 @@ (ns debug (:require + [app.render-wasm.api :as wasm.api] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.repair :as cfr] @@ -456,3 +457,10 @@ (defn ^:export network-averages [] (.log js/console (clj->js @http/network-averages))) + +(defn ^:export tmp + [] + (let [objects (dsh/lookup-page-objects @st/state) + shape (->> (get-selected @st/state) (first) (get objects))] + (wasm.api/calculate-position-data shape)) + ) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 2b3038999d..77cefef2c5 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -847,6 +847,8 @@ impl RenderState { ); } } + + // text::render_position_data(self, fills_surface_id, &shape, &text_content); } } _ => { diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 5c64ed18ab..03140844cc 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -4,6 +4,7 @@ use crate::{ shapes::{ merge_fills, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind, TextContent, VerticalAlign, + calc_position_data }, utils::{get_fallback_fonts, get_font_collection}, }; @@ -504,6 +505,29 @@ pub fn render_as_path( } } +#[allow(dead_code)] +pub fn render_position_data( + render_state: &mut RenderState, + surface_id: SurfaceId, + shape: &Shape, + text_content: &TextContent +) { + let position_data = calc_position_data(shape, text_content); + + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_argb(255, 255, 0, 0)); + paint.set_stroke_width(2.); + + for pd in position_data { + let rect = Rect::from_xywh(pd.x, pd.y, pd.width, pd.height); + render_state.surfaces + .canvas(surface_id) + .draw_rect(rect, &paint); + } +} + + // How to use it? // Type::Text(text_content) => { // self.surfaces diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index ecace5d187..106b52d2a7 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -204,6 +204,49 @@ fn intersects(paragraph: &skia_safe::textlayout::Paragraph, x: f32, y: f32) -> b rects.iter().any(|r| r.rect.contains(&Point::new(x, y))) } + +/// Performs a text auto layout without width limits. +/// This should be the same as text_auto_layout. +pub fn build_paragraphs_from_paragraph_builders( + paragraph_builders: &mut [ParagraphBuilderGroup], + width: f32, +) -> Vec> { + let paragraphs = paragraph_builders + .iter_mut() + .map(|builders| { + builders + .iter_mut() + .map(|builder| { + let mut paragraph = builder.build(); + // For auto-width, always layout with infinite width first to get intrinsic width + paragraph.layout(width); + paragraph + }) + .collect() + }) + .collect(); + paragraphs +} + +/// Calculate the normalized line height from paragraph builders +pub fn calculate_normalized_line_height( + paragraph_builders: &mut [ParagraphBuilderGroup], + width: f32, +) -> f32 { + let mut normalized_line_height = 0.0; + for paragraph_builder_group in paragraph_builders.iter_mut() { + for paragraph_builder in paragraph_builder_group.iter_mut() { + let mut paragraph = paragraph_builder.build(); + paragraph.layout(width); + let baseline = paragraph.ideographic_baseline(); + if baseline > normalized_line_height { + normalized_line_height = baseline; + } + } + } + normalized_line_height +} + #[derive(Debug, PartialEq, Clone)] pub struct TextContent { pub paragraphs: Vec, @@ -440,59 +483,15 @@ impl TextContent { paragraph_group } - /// Performs a text auto layout without width limits. - /// This should be the same as text_auto_layout. - fn build_paragraphs_from_paragraph_builders( - &self, - paragraph_builders: &mut [ParagraphBuilderGroup], - width: f32, - ) -> Vec> { - let paragraphs = paragraph_builders - .iter_mut() - .map(|builders| { - builders - .iter_mut() - .map(|builder| { - let mut paragraph = builder.build(); - // For auto-width, always layout with infinite width first to get intrinsic width - paragraph.layout(width); - paragraph - }) - .collect() - }) - .collect(); - paragraphs - } - - /// Calculate the normalized line height from paragraph builders - fn calculate_normalized_line_height( - &self, - paragraph_builders: &mut [ParagraphBuilderGroup], - width: f32, - ) -> f32 { - let mut normalized_line_height = 0.0; - for paragraph_builder_group in paragraph_builders.iter_mut() { - for paragraph_builder in paragraph_builder_group.iter_mut() { - let mut paragraph = paragraph_builder.build(); - paragraph.layout(width); - let baseline = paragraph.ideographic_baseline(); - if baseline > normalized_line_height { - normalized_line_height = baseline; - } - } - } - normalized_line_height - } - /// Performs an Auto Width text layout. fn text_layout_auto_width(&self) -> TextContentLayoutResult { let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let normalized_line_height = - self.calculate_normalized_line_height(&mut paragraph_builders, f32::MAX); + calculate_normalized_line_height(&mut paragraph_builders, f32::MAX); let paragraphs = - self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::MAX); + build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::MAX); let (width, height) = paragraphs @@ -521,10 +520,10 @@ impl TextContent { let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let normalized_line_height = - self.calculate_normalized_line_height(&mut paragraph_builders, width); + calculate_normalized_line_height(&mut paragraph_builders, width); let paragraphs = - self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); + build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); let height = paragraphs .iter() .flatten() @@ -546,10 +545,10 @@ impl TextContent { let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let normalized_line_height = - self.calculate_normalized_line_height(&mut paragraph_builders, width); + calculate_normalized_line_height(&mut paragraph_builders, width); let paragraphs = - self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); + build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); let paragraph_height = paragraphs .iter() .flatten() @@ -577,7 +576,7 @@ impl TextContent { pub fn get_height(&self, width: f32) -> f32 { let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let paragraphs = - self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); + build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); let paragraph_height = paragraphs .iter() .flatten() @@ -734,7 +733,7 @@ impl TextContent { let width = self.width(); let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let paragraphs = - self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); + build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); paragraphs .iter() @@ -1045,3 +1044,121 @@ impl TextSpan { }) } } + +#[allow(dead_code)] +#[derive(Debug)] +pub struct PositionData { + pub paragraph: u32, // 4 + pub span: u32, // 4 + pub start_pos: u32, // 4 + pub end_pos: u32, // 4 + pub x: f32, // 4 + pub y: f32, // 4 + pub width: f32, // 4 + pub height: f32, // 4 + pub direction: u32 // 4, u32 to align with 32 bytes +} + +fn direction_to_int(direction: TextDirection) -> u32 { + match direction { + TextDirection::RTL => 0, + TextDirection::LTR => 1 + } +} + +//fn get_unicode_substring(full_text: &str, start: usize, end: usize) -> String { +// let chars: Vec = full_text.chars().collect(); +// chars[start..end].iter().collect() +//} + +pub fn calc_position_data( + shape: &Shape, + text_content: &TextContent +) -> Vec { + let mut result: Vec = Vec::default(); + let mut text_content = text_content.clone(); + text_content.update_layout(shape.selrect); + let rect = text_content.content_rect(&shape.selrect, shape.vertical_align); + + let x = rect.x(); + let mut y = rect.y(); + + let fonts = get_font_collection(); + let fallback_fonts = get_fallback_fonts(); + + for (paragraph_index, paragraph) in text_content.paragraphs().iter().enumerate() { + let mut paragraph_text = String::default(); + let paragraph_style = paragraph.paragraph_to_style(); + let mut builder = ParagraphBuilder::new(¶graph_style, fonts); + + let mut span_ranges: Vec<(usize, usize, usize)> = vec![]; + let mut cur = 0; + + for (span_index, span) in paragraph.children().iter().enumerate() { + let text_style = span.to_style( + &text_content.bounds(), + fallback_fonts, + false, + paragraph.line_height(), + ); + let text: String = span.apply_text_transform(); + builder.push_style(&text_style); + builder.add_text(&text); + + span_ranges.push((cur, cur + text.len(), span_index)); + cur += text.len(); + + paragraph_text += &text; + } + + let mut p = builder.build(); + p.layout(shape.selrect.width()); + + for (start, end, span_index) in span_ranges { + let rects = p.get_rects_for_range( + start .. end, + RectHeightStyle::Tight, + RectWidthStyle::Tight, + ); + + for textbox in rects { + let direction = textbox.direct; + let mut rect = textbox.rect; + let cy = rect.top + rect.height() / 2.0; + + let start_pos = p + .get_glyph_position_at_coordinate((rect.left + 0.1, cy)) + .position as usize; + + let end_pos = p + .get_glyph_position_at_coordinate((rect.right - 0.1, cy)) + .position as usize; + + // start_pos and end_pos are relative to the paragraph but we + // want it relative to the span + + let start_pos = start_pos - start; + let end_pos = end_pos - start; + + rect.offset((x, y)); + + result.push(PositionData { + paragraph: paragraph_index as u32, + span: span_index as u32, + start_pos: start_pos as u32, + end_pos: end_pos as u32, + x: rect.x(), + y: rect.y(), + width: rect.width(), + height: rect.height(), + direction: direction_to_int(direction) + }); + } + + } + + y += p.height(); + } + + return result; +} diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 1ae81d06b9..596d3d6a4d 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -2,13 +2,13 @@ use macros::ToJs; use super::{fills::RawFillData, fonts::RawFontStyle}; use crate::math::{Matrix, Point}; -use crate::mem; +use crate::mem::{self, SerializableResult}; use crate::shapes::{ self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type, }; use crate::utils::{uuid_from_u32, uuid_from_u32_quartet}; use crate::{ - with_current_shape_mut, with_state, with_state_mut, with_state_mut_current_shape, STATE, + with_current_shape, with_current_shape_mut, with_state, with_state_mut, with_state_mut_current_shape, STATE, }; const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::(); @@ -411,3 +411,37 @@ pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 { }); -1 } + +const RAW_POSITION_DATA_SIZE: usize = size_of::(); + +impl SerializableResult for shapes::PositionData { + type BytesType = [u8; RAW_POSITION_DATA_SIZE]; + + fn from_bytes(bytes: Self::BytesType) -> Self { + unsafe { std::mem::transmute(bytes) } + } + fn as_bytes(&self) -> Self::BytesType { + let ptr = self as *const shapes::PositionData as *const u8; + let bytes: &[u8] = unsafe { std::slice::from_raw_parts(ptr, RAW_POSITION_DATA_SIZE) }; + let mut result = [0; RAW_POSITION_DATA_SIZE]; + result.copy_from_slice(bytes); + result + } + + // The generic trait doesn't know the size of the array. This is why the + // clone needs to be here even if it could be generic. + fn clone_to_slice(&self, slice: &mut [u8]) { + slice.clone_from_slice(&self.as_bytes()); + } +} + +#[no_mangle] +pub extern "C" fn calc_position_data() -> *mut u8 { + let mut result = Vec::::default(); + with_current_shape!(state, |shape: &Shape| { + if let Type::Text(text_content) = &shape.shape_type { + result = shapes::calc_position_data(shape, &text_content); + } + }); + mem::write_vec(result) +}