profile
viewpoint

laurmaedje/rechner 1

calculator with electron and rust

laurmaedje/symflow 1

Data flow analysis for x86-64 ELF binaries based on symbolic execution. 🔎

laurmaedje/vscode-colorful 1

A theme with a dark & light variant for VS Code.

laurmaedje/arm-hello 0

hello world OS for aarch64 architecture

laurmaedje/Daydream 0

Pathfinding with A* in Unity

laurmaedje/harfbuzz 0

HarfBuzz text shaping engine

laurmaedje/kurbo 0

A Rust library for manipulating curves

laurmaedje/leval 0

rust expression evaluation

laurmaedje/rustybuzz 0

An incremental harfbuzz port to Rust

laurmaedje/sudoku 0

Tree-searching sudoku solver with forward checking.

create barnchlaurmaedje/rustybuzz

branch : ggg

created branch time in 11 hours

delete branch laurmaedje/rustybuzz

delete branch : ggg-dead

delete time in 3 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha af48a7d77189a0f95b1ecb937bef074735008a54

Remove dead glyph alternates code.

view details

push time in 5 days

create barnchlaurmaedje/rustybuzz

branch : ggg-dead

created branch time in 5 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha cf6ce5783d4d3d355928ff1a82a04648b02e368b

Port normalization.

view details

push time in 6 days

pull request commentRazrFalcon/rustybuzz

Port normalization.

That's neat and looks very helpful! I'm gonna study harfbuzz's GGG-Table implementation and the OpenType spec a bit more and then see whether I have the time and ability to actually implement this stuff in Rust. I'd love to do it but it's difficult and I obviously can't say for sure that I'll get it done.

laurmaedje

comment created time in 6 days

pull request commentRazrFalcon/rustybuzz

Port normalization.

Testing those proprietary fonts on macOS with a large corpus would at least make sure that rustybuzz matches harfbuzz's behaviour on those fonts. Even though these might not hit every edge case. With regards to the GSUB/GPOS complexity, that's true, although I still think that there's more to gain here short-term than with AAT.

Honestly, I have no idea what the right move is here. I love the idea of having all this in Rust, both for safety and ease of use in the Rust ecosystem, but it's obviously a massive effort and then there's also the fact that harfbuzz is actively developed and moving along in C++.

laurmaedje

comment created time in 6 days

delete branch laurmaedje/rustybuzz

delete branch : port-normalize

delete time in 6 days

pull request commentRazrFalcon/rustybuzz

Port normalization.

With the Apple testing, I have two thoughts:

  • First: Is it feasible to test Rustybuzz against Harfbuzz against all of Wikipedia in the same way that Harfbuzz originally did against Uniscribe (described here)?
  • Second: I do think there's value in Rustybuzz even if AAT shaping is still in C++ for some time. We could, for example, put it behind a default-enabled feature flag. Then you can opt-out for pure-rustness (e.g. on WASM where AAT shaping probably doesn't matter). I agree that it isn't great, but maybe better than nothing.

In general, I'll have to dig a bit deeper into the harfbuzz sources to find out how everything works together before even thinking about attempting any of this stuff that comes next.

laurmaedje

comment created time in 7 days

Pull request review commentRazrFalcon/rustybuzz

Port normalization.

+use crate::{ffi, ot, Font};+use crate::buffer::{Buffer, BufferScratchFlags, GlyphInfo};+use crate::unicode::{CharExt, GeneralCategory};++// HIGHLEVEL DESIGN:+//+// This file exports one main function: _rb_ot_shape_normalize().+//+// This function closely reflects the Unicode Normalization Algorithm,+// yet it's different.+//+// Each shaper specifies whether it prefers decomposed (NFD) or composed (NFC).+// The logic however tries to use whatever the font can support.+//+// In general what happens is that: each grapheme is decomposed in a chain+// of 1:2 decompositions, marks reordered, and then recomposed if desired,+// so far it's like Unicode Normalization.  However, the decomposition and+// recomposition only happens if the font supports the resulting characters.+//+// The goals are:+//+//   - Try to render all canonically equivalent strings similarly.  To really+//     achieve this we have to always do the full decomposition and then+//     selectively recompose from there.  It's kinda too expensive though, so+//     we skip some cases.  For example, if composed is desired, we simply+//     don't touch 1-character clusters that are supported by the font, even+//     though their NFC may be different.+//+//   - When a font has a precomposed character for a sequence but the 'ccmp'+//     feature in the font is not adequate, use the precomposed character+//     which typically has better mark positioning.+//+//   - When a font does not support a combining mark, but supports it precomposed+//     with previous base, use that.  This needs the itemizer to have this+//     knowledge too.  We need to provide assistance to the itemizer.+//+//   - When a font does not support a character but supports its canonical+//     decomposition, well, use the decomposition.+//+//   - The complex shapers can customize the compose and decompose functions to+//     offload some of their requirements to the normalizer.  For example, the+//     Indic shaper may want to disallow recomposing of two matras.++pub struct ShapeNormalizeContext {+    pub plan: ot::ShapePlan,+    pub(crate) buffer: &'static mut Buffer,+    pub font: &'static Font<'static>,+    pub decompose: ffi::rb_ot_decompose_func_t,+    pub compose: ffi::rb_ot_compose_func_t,+}++impl ShapeNormalizeContext {+    #[inline]+    pub fn from_ptr(ctx: *const ffi::rb_ot_shape_normalize_context_t) -> &'static Self {+        unsafe { &*(ctx as *const Self) }+    }++    #[inline]+    pub fn as_ptr(&self) -> *const ffi::rb_ot_shape_normalize_context_t {+        self as *const _ as *const ffi::rb_ot_shape_normalize_context_t+    }++    #[inline]+    pub fn decompose(&self, ab: u32) -> Option<(u32, u32)> {+        let mut a = 0;+        let mut b = 0;++        unsafe {+            if (self.decompose)(self.as_ptr(), ab, &mut a, &mut b) != 0 {+                return Some((a, b));+            }+        }++        None+    }++    #[inline]+    pub fn compose(&self, a: u32, b: u32) -> Option<u32> {+        let mut ab = 0;++        unsafe {+            if (self.compose)(self.as_ptr(), a, b, &mut ab) != 0 {+                return Some(ab);+            }+        }++        None+    }+}++#[derive(Clone, Copy, Debug, Eq, PartialEq)]+#[allow(dead_code)]+pub enum ShapeNormalizationMode {+    None = 0,+    Decomposed,+    /// Never composes base-to-base.+    ComposedDiacritics,+    /// Always fully decomposes and then recompose back.+    ComposedDiacriticsNoShortCircuit,+    Auto,+}++impl Default for ShapeNormalizationMode {+    fn default() -> Self {+        Self::Auto+    }+}++#[no_mangle]+pub extern "C" fn _rb_ot_shape_normalize(+    plan: *const ffi::rb_ot_shape_plan_t,+    buffer: *mut ffi::rb_buffer_t,+    font: *mut ffi::rb_font_t,+) {+    let plan = ot::ShapePlan::from_ptr(plan);+    let buffer = Buffer::from_ptr_mut(buffer);+    let font = Font::from_ptr(font);++    if buffer.is_empty() {+        return;+    }++    let mut mode = plan.ot_shaper.normalization_preference();+    if mode == ShapeNormalizationMode::Auto {+        // https://github.com/harfbuzz/harfbuzz/issues/653#issuecomment-423905920+        // if plan.has_gpos_mark() {+        //     mode = ShapeNormalizationMode::Decomposed;+        // }+        mode = ShapeNormalizationMode::ComposedDiacritics;+    }++    let decompose = plan.ot_shaper.get_decompose().unwrap_or(decompose_unicode);+    let compose = plan.ot_shaper.get_compose().unwrap_or(compose_unicode);+    let mut ctx = ShapeNormalizeContext { plan, buffer, font, decompose, compose };+    let mut buffer = &mut ctx.buffer;++    let always_short_circuit = mode == ShapeNormalizationMode::None;+    let might_short_circuit = always_short_circuit || !matches!(+        mode,+        ShapeNormalizationMode::Decomposed |+        ShapeNormalizationMode::ComposedDiacriticsNoShortCircuit+    );++    // We do a fairly straightforward yet custom normalization process in three+    // separate rounds: decompose, reorder, recompose (if desired).  Currently+    // this makes two buffer swaps.  We can make it faster by moving the last+    // two rounds into the inner loop for the first round, but it's more readable+    // this way.++    // First round, decompose+    let mut all_simple = true;+    {+        let count = buffer.len;+        buffer.idx = 0;+        buffer.clear_output();+        loop {+            let mut end = buffer.idx + 1;+            while end < count && !buffer.info[end].is_unicode_mark() {+                end += 1;+            }++            if end < count {+                // Leave one base for the marks to cluster with.+                end -= 1;+            }++            // From idx to end are simple clusters.+            if might_short_circuit {+                let len = end - buffer.idx;+                let mut done = 0;+                while done < len {+                    let cur = buffer.cur_mut(done);+                    *cur.glyph_index() = match font.glyph_index(cur.codepoint) {+                        Some(glyph_id) => glyph_id.0 as u32,+                        None => break,+                    };+                    done += 1;+                }+                buffer.next_glyphs(done);+            }++            while buffer.idx < end && buffer.successful {+                decompose_current_character(&mut ctx, might_short_circuit);+                buffer = &mut ctx.buffer;+            }++            if buffer.idx == count || !buffer.successful {+                break;+            }++            all_simple = false;++            // Find all the marks now.+            end = buffer.idx + 1;+            while end < count && buffer.info[end].is_unicode_mark() {+                end += 1;+            }++            // idx to end is one non-simple cluster.+            decompose_multi_char_cluster(&mut ctx, end, always_short_circuit);+            buffer = &mut ctx.buffer;++            if buffer.idx >= count || !buffer.successful {+                break;+            }+        }++        buffer.swap_buffers();+    }++    // Second round, reorder (inplace)+    if !all_simple {+        let count = buffer.len;+        let mut i = 0;+        while i < count {+            if buffer.info[i].modified_combining_class() == 0 {+                i += 1;+                continue;+            }++            let mut end = i + 1;+            while end < count && buffer.info[end].modified_combining_class() != 0 {+                end += 1;+            }++            // We are going to do a O(n^2).  Only do this if the sequence is short.+            if end - i <= ot::MAX_COMBINING_MARKS {+                buffer.sort(i, end, compare_combining_class);++                if let Some(reorder_marks) = ctx.plan.ot_shaper.get_reorder_marks() {+                    unsafe {+                        reorder_marks(ctx.plan.as_ptr(), buffer.as_ptr(), i as u32, end as u32);+                    }+                }+            }++            i = end + 1;+        }+    }+    if buffer.scratch_flags.contains(BufferScratchFlags::HAS_CGJ) {+        // For all CGJ, check if it prevented any reordering at all.+        // If it did NOT, then make it skippable.+        // https://github.com/harfbuzz/harfbuzz/issues/554+        for i in 1..buffer.len.saturating_sub(1) {+            if buffer.info[i].codepoint == 0x034F /* CGJ */ {+                let last = buffer.info[i - 1].modified_combining_class();+                let next = buffer.info[i + 1].modified_combining_class();+                if next == 0 || last <= next {+                    buffer.info[i].unhide();+                }+            }+        }+    }++    // Third round, recompose+    if !all_simple && matches!(+        mode,+        ShapeNormalizationMode::ComposedDiacritics |+        ShapeNormalizationMode::ComposedDiacriticsNoShortCircuit+    ) {+        // As noted in the comment earlier, we don't try to combine+        // ccc=0 chars with their previous Starter.++        let count = buffer.len;+        let mut starter = 0;+        buffer.clear_output();+        buffer.next_glyph();+        while buffer.idx < count && buffer.successful {+            // We don't try to compose a non-mark character with it's preceding starter.+            // This is both an optimization to avoid trying to compose every two neighboring+            // glyphs in most scripts AND a desired feature for Hangul.  Apparently Hangul+            // fonts are not designed to mix-and-match pre-composed syllables and Jamo.+            let cur = buffer.cur(0);+            if cur.is_unicode_mark() &&+                // If there's anything between the starter and this char, they should have CCC+                // smaller than this character's.+                (starter == buffer.out_len - 1+                    || buffer.prev().modified_combining_class() < cur.modified_combining_class())+            {+                let a = buffer.out_info()[starter].codepoint;+                let b = cur.codepoint;+                if let Some(composed) = ctx.compose(a, b) {+                    if let Some(glyph_id) = font.glyph_index(composed) {+                        // Copy to out-buffer.+                        buffer = &mut ctx.buffer;+                        buffer.next_glyph();+                        if !buffer.successful {+                            return;+                        }++                        // Merge and remove the second composable.+                        buffer.merge_out_clusters(starter, buffer.out_len);+                        buffer.out_len -= 1;++                        // Modify starter and carry on.+                        let mut flags = buffer.scratch_flags;+                        let mut info = &mut buffer.out_info_mut()[starter];+                        info.codepoint = composed;+                        *info.glyph_index() = glyph_id.0 as u32;+                        info.init_unicode_props(&mut flags);+                        buffer.scratch_flags = flags;++                        continue;+                    }+                }+            }++            // Blocked, or doesn't compose.+            buffer = &mut ctx.buffer;+            buffer.next_glyph();++            if buffer.prev().modified_combining_class() == 0 {+                starter = buffer.out_len - 1;+            }+        }++        buffer.swap_buffers();+    }+}++fn decompose_multi_char_cluster(ctx: &mut ShapeNormalizeContext, end: usize, short_circuit: bool) {+    let mut i = ctx.buffer.idx;+    while i < end && ctx.buffer.successful {+        if ctx.buffer.info[i].as_char().is_variation_selector() {+            handle_variation_selector_cluster(ctx, end, short_circuit);+            return;+        }+        i += 1;+    }++    while ctx.buffer.idx < end && ctx.buffer.successful {+        decompose_current_character(ctx, short_circuit);+    }+}++fn handle_variation_selector_cluster(ctx: &mut ShapeNormalizeContext, end: usize, _: bool) {+    // TODO: Currently if there's a variation-selector we give-up, it's just too hard.+    let buffer = &mut ctx.buffer;+    let font = ctx.font;+    while buffer.idx < end - 1 && buffer.successful {+        if buffer.cur(1).as_char().is_variation_selector() {+            if let Some(glyph_id) = font.glyph_variation_index(+                buffer.cur(0).as_char(),+                buffer.cur(1).as_char(),+            ) {+                *buffer.cur_mut(0).glyph_index() = glyph_id.0 as u32;+                let unicode = buffer.cur(0).codepoint;+                buffer.replace_glyphs(2, 1, &[unicode]);+            } else {+                // Just pass on the two characters separately, let GSUB do its magic.+                set_glyph(buffer.cur_mut(0), font);+                buffer.next_glyph();+                set_glyph(buffer.cur_mut(0), font);+                buffer.next_glyph();+            }++            // Skip any further variation selectors.+            while buffer.idx < end && buffer.cur(0).as_char().is_variation_selector() {+                set_glyph(buffer.cur_mut(0), font);+                buffer.next_glyph();+            }+        } else {+            set_glyph(buffer.cur_mut(0), font);+            buffer.next_glyph();+        }+    }++    if ctx.buffer.idx < end {+        set_glyph(ctx.buffer.cur_mut(0), font);+        ctx.buffer.next_glyph();+    }+}++fn decompose_current_character(ctx: &mut ShapeNormalizeContext, shortest: bool) {+    let u = ctx.buffer.cur(0).as_char();+    let glyph = ctx.font.glyph_index(u as u32);++    if !shortest || glyph.is_none() {+        if decompose(ctx, shortest, u as u32) > 0 {+            skip_char(ctx.buffer);+            return;+        }+    }++    if let Some(glyph) = glyph {+        next_char(ctx.buffer, glyph.0 as u32);+        return;+    }++    // Handle space characters.+    if ctx.buffer.cur(0).general_category() == GeneralCategory::SpaceSeparator {+        if let Some(space_type) = u.space_fallback() {+            if let Some(space_glyph) = ctx.font.glyph_index(' ' as u32) {+                ctx.buffer.cur_mut(0).set_space_fallback(space_type);+                next_char(ctx.buffer, space_glyph.0 as u32);+                ctx.buffer.scratch_flags |= BufferScratchFlags::HAS_SPACE_FALLBACK;+                return;+            }+        }+    }++    // U+2011 is the only sensible character that is a no-break version of another character+    // and not a space.  The space ones are handled already.  Handle this lone one.+    if u as u32 == 0x2011 {+        if let Some(other_glyph) = ctx.font.glyph_index(0x2010) {+            next_char(ctx.buffer, other_glyph.0 as u32);+            return;+        }+    }++    // Insert a .notdef glyph if decomposition failed.+    next_char(ctx.buffer, 0);+}++/// Returns 0 if didn't decompose, number of resulting characters otherwise.+fn decompose(ctx: &mut ShapeNormalizeContext, shortest: bool, ab: u32) -> u32 {+    let (a, b) = match ctx.decompose(ab) {+        Some(decomposed) => decomposed,+        _ => return 0,+    };++    let a_glyph = ctx.font.glyph_index(a);+    let b_glyph = if b != 0 {+        match ctx.font.glyph_index(b) {+            Some(glyph_id) => Some(glyph_id),+            None => return 0,+        }+    } else {+        None+    };++    if !shortest || a_glyph.is_none() {+        let ret = decompose(ctx, shortest, a);+        if ret != 0 {+            if let Some(b_glyph) = b_glyph {+                output_char(ctx.buffer, b, b_glyph.0 as u32);+                return ret + 1;+            }+            return ret;+        }+    }++    if let Some(a_glyph) = a_glyph {+        // Output a and b.+        output_char(ctx.buffer, a, a_glyph.0 as u32);+        if let Some(b_glyph) = b_glyph {+            output_char(ctx.buffer, b, b_glyph.0 as u32);+            return 2;+        }+        return 1;+    }++    0+}++extern "C" fn decompose_unicode(+    _: *const ffi::rb_ot_shape_normalize_context_t,+    ab: ffi::rb_codepoint_t,+    a: *mut ffi::rb_codepoint_t,+    b: *mut ffi::rb_codepoint_t,+) -> ffi::rb_bool_t {+    crate::unicode::rb_ucd_decompose(ab, a, b)+}++extern "C" fn compose_unicode(+    _: *const ffi::rb_ot_shape_normalize_context_t,+    a: ffi::rb_codepoint_t,+    b: ffi::rb_codepoint_t,+    ab: *mut ffi::rb_codepoint_t,+) -> ffi::rb_bool_t {+    crate::unicode::rb_ucd_compose(a, b, ab)+}++fn compare_combining_class(pa: &GlyphInfo, pb: &GlyphInfo) -> bool {+    pa.modified_combining_class() > pb.modified_combining_class()+}++fn set_glyph(info: &mut GlyphInfo, font: &Font) {+    if let Some(glyph_id) = font.glyph_index(info.codepoint) {+        *info.glyph_index() = glyph_id.0 as u32;+    }+}++fn output_char(buffer: &mut Buffer, unichar: u32, glyph: u32) {

Yes, I also thought about that, but went with the minimally invasive solution first.

laurmaedje

comment created time in 7 days

PullRequestReviewEvent

Pull request review commentRazrFalcon/rustybuzz

Port normalization.

+use crate::{ffi, ot, Font};+use crate::buffer::{Buffer, BufferScratchFlags, GlyphInfo};+use crate::unicode::{CharExt, GeneralCategory};++// HIGHLEVEL DESIGN:+//+// This file exports one main function: _rb_ot_shape_normalize().+//+// This function closely reflects the Unicode Normalization Algorithm,+// yet it's different.+//+// Each shaper specifies whether it prefers decomposed (NFD) or composed (NFC).+// The logic however tries to use whatever the font can support.+//+// In general what happens is that: each grapheme is decomposed in a chain+// of 1:2 decompositions, marks reordered, and then recomposed if desired,+// so far it's like Unicode Normalization.  However, the decomposition and+// recomposition only happens if the font supports the resulting characters.+//+// The goals are:+//+//   - Try to render all canonically equivalent strings similarly.  To really+//     achieve this we have to always do the full decomposition and then+//     selectively recompose from there.  It's kinda too expensive though, so+//     we skip some cases.  For example, if composed is desired, we simply+//     don't touch 1-character clusters that are supported by the font, even+//     though their NFC may be different.+//+//   - When a font has a precomposed character for a sequence but the 'ccmp'+//     feature in the font is not adequate, use the precomposed character+//     which typically has better mark positioning.+//+//   - When a font does not support a combining mark, but supports it precomposed+//     with previous base, use that.  This needs the itemizer to have this+//     knowledge too.  We need to provide assistance to the itemizer.+//+//   - When a font does not support a character but supports its canonical+//     decomposition, well, use the decomposition.+//+//   - The complex shapers can customize the compose and decompose functions to+//     offload some of their requirements to the normalizer.  For example, the+//     Indic shaper may want to disallow recomposing of two matras.++pub struct ShapeNormalizeContext {+    pub plan: ot::ShapePlan,+    pub(crate) buffer: &'static mut Buffer,+    pub font: &'static Font<'static>,+    pub decompose: ffi::rb_ot_decompose_func_t,+    pub compose: ffi::rb_ot_compose_func_t,+}++impl ShapeNormalizeContext {+    #[inline]+    pub fn from_ptr(ctx: *const ffi::rb_ot_shape_normalize_context_t) -> &'static Self {+        unsafe { &*(ctx as *const Self) }+    }++    #[inline]+    pub fn as_ptr(&self) -> *const ffi::rb_ot_shape_normalize_context_t {+        self as *const _ as *const ffi::rb_ot_shape_normalize_context_t+    }++    #[inline]+    pub fn decompose(&self, ab: u32) -> Option<(u32, u32)> {+        let mut a = 0;+        let mut b = 0;++        unsafe {+            if (self.decompose)(self.as_ptr(), ab, &mut a, &mut b) != 0 {+                return Some((a, b));+            }+        }++        None+    }++    #[inline]+    pub fn compose(&self, a: u32, b: u32) -> Option<u32> {+        let mut ab = 0;++        unsafe {+            if (self.compose)(self.as_ptr(), a, b, &mut ab) != 0 {+                return Some(ab);+            }+        }++        None+    }+}++#[derive(Clone, Copy, Debug, Eq, PartialEq)]+#[allow(dead_code)]+pub enum ShapeNormalizationMode {+    None = 0,+    Decomposed,+    /// Never composes base-to-base.+    ComposedDiacritics,+    /// Always fully decomposes and then recompose back.+    ComposedDiacriticsNoShortCircuit,+    Auto,+}++impl Default for ShapeNormalizationMode {+    fn default() -> Self {+        Self::Auto+    }+}++#[no_mangle]+pub extern "C" fn _rb_ot_shape_normalize(

Sure, I also did it for the fallback shaper functions.

laurmaedje

comment created time in 7 days

PullRequestReviewEvent

push eventlaurmaedje/rustybuzz

Laurenz

commit sha 69acc91f1f89d5b47900e800982b339bc78d9a68

Wrap extern functions, move buffer methods, update log

view details

push time in 7 days

PR opened RazrFalcon/rustybuzz

Port normalization.

Okay, I ported the normalization. This one was already a bit trickier because it was more intertwined, especially with the compose/decompose function pointers through FFI.

Some observations:

  • The existing complex shaper's compose/decompose functions used Rust's bool through FFI, which I think was incorrect. I changed it to rb_bool_t.
  • I had to rename _rb_glyph_info_set_unicode_props to init_unicode_props in Rust because it would have clashed with the existing set_unicode_props. Furthermore, the logic is duplicated in Rust and C because there wasn't any GlyphInfo FFI so far and I didn't think it was worth it.
  • I also had to do a mimimal port of rb_ot_complex_shaper_t to get some properties needed for normalization.

I think, maybe the main shaping logic could be ported next, but I have to look more closely whether there are still any obstacles that need to be taken care of before.

+809 -578

0 comment

18 changed files

pr created time in 8 days

push eventlaurmaedje/rustybuzz

Evgeniy Reizner

commit sha 3dcf7905948a16a8afaf0252eaa06526e582337b

Update readme.

view details

Laurenz

commit sha 4c72672f64191e1a1227c55967b5fc586a8329d0

Port normalization.

view details

push time in 8 days

push eventlaurmaedje/rustybuzz

Evgeniy Reizner

commit sha 3dcf7905948a16a8afaf0252eaa06526e582337b

Update readme.

view details

push time in 8 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha d44e047322d1e3e6163841a60d381cbbf7579c9b

Port normalization.

view details

push time in 8 days

delete branch laurmaedje/rustybuzz

delete branch : port-normalize-step-by-step

delete time in 8 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha d6222c33475a760c6bc8312e21518cfd72ee02da

Port normalization.

view details

push time in 8 days

create barnchlaurmaedje/rustybuzz

branch : port-normalize-step-by-step

created branch time in 8 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha 855bfc2ae4dd20af4666d5b5c3c411d7cc2d17e5

Fix unhide() bug

view details

Laurenz

commit sha ecaf2f5d4f2292658d1eb7b9e276ff05cc04a208

Remove macros

view details

push time in 8 days

create barnchlaurmaedje/rustybuzz

branch : port-normalize

created branch time in 8 days

delete branch laurmaedje/harfbuzz

delete branch : fix-signedness

delete time in 9 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha 9533c2759826adc492bc23f28d887f5c6330205b

Port fallback shaper.

view details

Evgeniy Reizner

commit sha e435610b007357c079d826de1c53e6556252f6ad

Update readme.

view details

push time in 9 days

delete branch laurmaedje/rustybuzz

delete branch : port-fallback-shaper-full

delete time in 9 days

delete branch laurmaedje/rustybuzz

delete branch : port-fallback-shaper

delete time in 9 days

PR opened harfbuzz/harfbuzz

[hb-shape-fallback] Use signed int for correction

This fixes #2719.

+1 -1

0 comment

1 changed file

pr created time in 9 days

push eventlaurmaedje/harfbuzz

Laurenz

commit sha 4f3e39c2a02887c39e4aed3a17ad7e07240c8204

[hb-shape-fallback] Use signed int for correction

view details

push time in 9 days

push eventlaurmaedje/harfbuzz

Laurenz

commit sha cfe4a7c6e6ebac56670becb51d4f869a8ed10a91

[hb-shape-fallback] Used signed int for correction

view details

push time in 9 days

create barnchlaurmaedje/harfbuzz

branch : fix-signedness

created branch time in 9 days

Pull request review commentRazrFalcon/rustybuzz

Port fallback shaper.

+use crate::{ffi, Direction, Font};+use crate::buffer::{Buffer, GlyphPosition};+use crate::unicode::{modified_combining_class, CanonicalCombiningClass, GeneralCategory, Space};+use crate::ot::*;++fn recategorize_combining_class(u: u32, mut class: u8) -> u8 {+    use CanonicalCombiningClass as Class;+    use modified_combining_class as mcc;++    if class >= 200 {+        return class;+    }++    // Thai / Lao need some per-character work.+    if u & !0xFF == 0x0E00 {+        // NOTE(laurmaedje): This branch is never tested.+        if class == 0 {+            match u {+                0x0E31 |+                0x0E34 |+                0x0E35 |+                0x0E36 |+                0x0E37 |+                0x0E47 |+                0x0E4C |+                0x0E4D |+                0x0E4E => class = Class::AboveRight as u8,++                0x0EB1 |+                0x0EB4 |+                0x0EB5 |+                0x0EB6 |+                0x0EB7 |+                0x0EBB |+                0x0ECC |+                0x0ECD => class = Class::Above as u8,++                0x0EBC => class = Class::Below as u8,++                _ => {}+            }+        } else {+            // Thai virama is below-right+            if u == 0x0E3A {+                class = Class::BelowRight as u8;+            }+        }+    }++    match class {+        // Hebrew+        mcc::CCC10 => Class::Below as u8,         // sheva+        mcc::CCC11 => Class::Below as u8,         // hataf segol+        mcc::CCC12 => Class::Below as u8,         // hataf patah+        mcc::CCC13 => Class::Below as u8,         // hataf qamats+        mcc::CCC14 => Class::Below as u8,         // hiriq+        mcc::CCC15 => Class::Below as u8,         // tsere+        mcc::CCC16 => Class::Below as u8,         // segol+        mcc::CCC17 => Class::Below as u8,         // patah+        mcc::CCC18 => Class::Below as u8,         // qamats+        mcc::CCC20 => Class::Below as u8,         // qubuts+        mcc::CCC22 => Class::Below as u8,         // meteg+        mcc::CCC23 => Class::AttachedAbove as u8, // rafe+        mcc::CCC24 => Class::AboveRight as u8,    // shin dot+        mcc::CCC25 => Class::AboveLeft as u8,     // sin dot+        mcc::CCC19 => Class::AboveLeft as u8,     // holam+        mcc::CCC26 => Class::Above as u8,         // point varika+        mcc::CCC21 => class,                      // dagesh++        // Arabic and Syriac+        mcc::CCC27 => Class::Above as u8, // fathatan+        mcc::CCC28 => Class::Above as u8, // dammatan+        mcc::CCC30 => Class::Above as u8, // fatha+        mcc::CCC31 => Class::Above as u8, // damma+        mcc::CCC33 => Class::Above as u8, // shadda+        mcc::CCC34 => Class::Above as u8, // sukun+        mcc::CCC35 => Class::Above as u8, // superscript alef+        mcc::CCC36 => Class::Above as u8, // superscript alaph+        mcc::CCC29 => Class::Below as u8, // kasratan+        mcc::CCC32 => Class::Below as u8, // kasra++        // Thai+        mcc::CCC103 => Class::BelowRight as u8, // sara u / sara uu+        mcc::CCC107 => Class::AboveRight as u8, // mai++        // Lao+        mcc::CCC118 => Class::Below as u8, // sign u / sign uu+        mcc::CCC122 => Class::Above as u8, // mai++        // Tibetian+        mcc::CCC129 => Class::Below as u8, // sign aa+        mcc::CCC130 => Class::Above as u8, // sign i+        mcc::CCC132 => Class::Below as u8, // sign u++        _ => class,+    }+}++#[no_mangle]+pub extern "C" fn _rb_ot_shape_fallback_mark_position_recategorize_marks(+    _: *const ffi::rb_ot_shape_plan_t,+    _: *mut ffi::rb_font_t,+    buffer: *mut ffi::rb_buffer_t,+) {+    let buffer = Buffer::from_ptr_mut(buffer);+    for info in &mut buffer.info {+        if info.general_category() == GeneralCategory::NonspacingMark {+            let mut class = info.modified_combining_class();+            class = recategorize_combining_class(info.codepoint, class);+            info.set_modified_combining_class(class);+        }+    }+}++fn zero_mark_advances(+    buffer: &mut Buffer,+    start: usize,+    end: usize,+    adjust_offsets_when_zeroing: bool,+) {+    // NOTE(laurmaedje): This whole function is never tested.+    for (info, pos) in buffer.info[start..end].iter().zip(&mut buffer.pos[start..end]) {+        if info.general_category() == GeneralCategory::NonspacingMark {+            if adjust_offsets_when_zeroing {+                pos.x_offset -= pos.x_advance;+                pos.y_offset -= pos.y_advance;+            }+            pos.x_advance = 0;+            pos.y_advance = 0;+        }+    }+}++fn position_mark(+    _: &ShapePlan,+    font: &Font,+    direction: Direction,+    codepoint: u32,+    pos: &mut GlyphPosition,+    base_extents: &mut ffi::rb_glyph_extents_t,+    combining_class: CanonicalCombiningClass,+) {+    use CanonicalCombiningClass as Class;++    let mark_extents = match font.glyph_extents(codepoint) {+        Some(extents) => extents,+        None => return,+    };++    let y_gap = font.units_per_em() / 16;+    pos.x_offset = 0;+    pos.y_offset = 0;++    // We don't position LEFT and RIGHT marks.++    // X positioning+    match combining_class {+        Class::DoubleBelow |+        Class::DoubleAbove if direction.is_horizontal() => {+            pos.x_offset += base_extents.x_bearing+                + if direction.is_forward() { base_extents.width } else { 0 }+                - mark_extents.width / 2 - mark_extents.x_bearing;+        }++        Class::AttachedBelowLeft |+        Class::BelowLeft |+        Class::AboveLeft => {+            // Left align.+            pos.x_offset += base_extents.x_bearing - mark_extents.x_bearing;+        }++        Class::AttachedAboveRight |+        Class::BelowRight |+        Class::AboveRight => {+            // Right align.+            pos.x_offset += base_extents.x_bearing + base_extents.width+                - mark_extents.width - mark_extents.x_bearing;+        }++        Class::AttachedBelow |+        Class::AttachedAbove |+        Class::Below |+        Class::Above |+        _ => {+            // Center align.+            pos.x_offset += base_extents.x_bearing+                + (base_extents.width - mark_extents.width) / 2+                - mark_extents.x_bearing;+        }+    }++    let is_attached = matches!(+        combining_class,+        Class::AttachedBelowLeft |+        Class::AttachedBelow |+        Class::AttachedAbove |+        Class::AttachedAboveRight+    );++    // Y positioning.+    match combining_class {+        Class::DoubleBelow |+        Class::BelowLeft |+        Class::Below |+        Class::BelowRight |+        Class::AttachedBelowLeft |+        Class::AttachedBelow => {+            if !is_attached {+                // Add gap.+                base_extents.height -= y_gap;+            }++            pos.y_offset = base_extents.y_bearing + base_extents.height+                - mark_extents.y_bearing;++            // Never shift up "below" marks.+            if (y_gap > 0) == (pos.y_offset > 0) {+                base_extents.height -= pos.y_offset;+                pos.y_offset = 0;+            }++            base_extents.height += mark_extents.height;+        }++        Class::DoubleAbove |+        Class::AboveLeft |+        Class::Above |+        Class::AboveRight |+        Class::AttachedAbove |+        Class::AttachedAboveRight => {+            if !is_attached {+                // Add gap.+                base_extents.y_bearing += y_gap;+                base_extents.height -= y_gap;+            }++            pos.y_offset = base_extents.y_bearing+                - (mark_extents.y_bearing + mark_extents.height);++            // Don't shift down "above" marks too much.+            if (y_gap > 0) != (pos.y_offset > 0) {+                // NOTE(laurmaedje): In the original this was "unsigned int".+                // Does it matter? I don't think so, I have to cast back anyway, otherwise.

Done.

laurmaedje

comment created time in 10 days

PullRequestReviewEvent

issue openedharfbuzz/harfbuzz

Signedness of int in hb-ot-shape-fallback

In hb-ot-shape-fallback#L304 the correction is stored as an unsigned integer and then only used with other signed arithmetic. Should this be just int? Or ist the reason that correction is always positive because pos.y_offset is always negative due to the previous if-condition and the fact that y_gap is always positive (at least if units_per_em can't be negative?). If so, I am wondering why the if-condition isn't just pos.y_offset < 0.

All of this is probably not really important in practice, but I just wanted to point it out.

created time in 10 days

pull request commentRazrFalcon/rustybuzz

Port fallback shaper.

I removed all those notes. Also good to know that I can use cargo tarpaulin.

Now, what I need to know: Would you have the time to review more PRs here (and maybe in ttf-parser for table-related things) or should I refrain from doing more work here at the moment?

laurmaedje

comment created time in 10 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha e47e51ef7cc0ebcdfbb5b5d471c21d960df53c93

Remove notes.

view details

push time in 10 days

PR opened RazrFalcon/rustybuzz

Port fallback shaper.

Since I'm excited to use this in a full-Rust WASM scenario, I decided to try and tackle one of the things on the roadmap that were marked as easy. I tried making the most straightforward, parallel adaption of the original C++ to make it not too hard to review. (Part of this is that I kept the order of functions as originally, even though I guess in Rust the general -> specific order is more common than the C-compiler-inflicted specific -> general.)

Some things I weren't totally sure about, I marked these with NOTE(laurmaedje). One particular thing I hope I didn't confuse are the Modified and Canonical combining classes.

+612 -508

0 comment

12 changed files

pr created time in 10 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha ce790695e20bde9836b19ebdf6c88939c8594f4b

Port fallback shaper.

view details

push time in 10 days

create barnchlaurmaedje/rustybuzz

branch : port-fallback-shaper-full

created branch time in 10 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha a97f7984a25c8a6d57111dcdae828c35df41aef2

Change Todos to Notes

view details

push time in 10 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha 8936683ef3ca5859ef2348339117ac9d3bf805a3

Port remainder

view details

Laurenz

commit sha b624c1847868e4fff792dc4927283349227bd43d

Formatting

view details

Laurenz

commit sha 2ee07d0fc778d2cc4737724dc6831028e1a001f2

Fix character classes

view details

push time in 10 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha 50b9792b000727e2d4da43661be35b491b66c150

Port position_around_base

view details

push time in 10 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha 3a45743c72605bec136c87986b67f1a61f4800b9

Port position_mark

view details

push time in 11 days

push eventlaurmaedje/rustybuzz

Laurenz

commit sha b5082ee919331fa02bb2d6590581ba9c27216ebb

Port recategorize_combining_class and the empty shape_fallback_kern ...

view details

push time in 11 days

create barnchlaurmaedje/rustybuzz

branch : port-fallback-shaper

created branch time in 11 days

fork laurmaedje/rustybuzz

An incremental harfbuzz port to Rust

fork in 11 days

push eventlaurmaedje/vscode-colorful

Laurenz

commit sha 9185ba2b48e60054b5444a6a1d42ebcb370886a4

Fix unreadable search results 👓

view details

push time in 13 days

startedmooman219/fontdue

started time in 17 days

issue commentlinebender/kurbo

Positive/Negative Insets

I see where you're coming from with the addition/subtraction thing.

I think, the main thing is that in the current implementation addition is the way to "apply" insets to a rectangle. Maybe it's equally possible to see subtraction as the default way of "applying" insets. Then, positive insets shrink a rect and we have

rect - positive_insets => smaller_rect
rect + positive_insets => larger_rect

I'm not sure, but it might even be okay to not actually specify whether positive insets shrink or expand a rectangle. Instead, just define the plus and minus operations and then let its usage define what an Inset means. Here's an example:

let margins = Insets::uniform(5.0);
let padding = Insets::uniform(10.0);

let smaller_rect = rect - padding;
let larger_rect = rect + margins;

This is kind of like in CSS where even though padding goes inwards and margins outwards, both are assigned positive values. It's possible that this is a really bad idea though, it's just a thought.

laurmaedje

comment created time in 19 days

issue openedlinebender/kurbo

Positive/Negative Insets

Positive Insets in kurbo make a rectangle larger and negative ones smaller. I find this pretty confusing because I think of insetting something as moving it inwards. From a very quick search, this is also how it's done in UIKit, Android Graphics and in Java AWT. Maybe there's maybe a good reason why it's different here (and it's also a bit bug-prone change to do now) but I still wanted to point it out because I think its kind of confusing.

created time in 23 days

push eventtypst/shape-group

Laurenz

commit sha fb532cb14668ba4f61cc31e30e98932868489c4d

Add benchmarks and debug renderer 🐞

view details

push time in a month

push eventtypst/shape-group

Laurenz

commit sha cb6452b0cf9ea00c4246c57b7e7d0de6f98a7e7e

Rename some geometry types & tests 🖌

view details

Laurenz

commit sha 02dda391904e4216254b7abf88fb93d051c2c36b

Tolerance can be set for approximate comparisons 📏 No more global length epsilon. This should be decided on a case-by-case basis.

view details

Laurenz

commit sha b097c70f814c69023a025f66084192e7446d11cd

Refactor shapes 💎

view details

Laurenz

commit sha 39421843d1ad567dd3c9a696094712453f56e3d7

Reorganize modules 🧱

view details

Laurenz

commit sha 8d3e769d6be3ebaf283fc33256ec64702bda8faf

New shiny renderer 🎨 Two different way to use the renderer: - Create a `Renderer` instance and render layouts, shapes and more using its methods - Use the set of macros that are backed by a global shared renderer

view details

Laurenz

commit sha d561115b045cd3d1d15a4e25d32ec1589f1520ce

Naive linear interpolating, one-segment collisionless placement 🔲

view details

Laurenz

commit sha 83e2511946bbb0eb28d64f2d47f8a9c253958194

Perform bisection search in segment 🧭

view details

Laurenz

commit sha 70631bded42240b45b68edb1117dc01b7f7e3d7f

Fix bug in bezier basis function 🚧

view details

Laurenz

commit sha 77f11f32260b1173c1534871d09e0811d339a2a9

Incorporate vertical dimensions into collisionless placement ↕

view details

Laurenz

commit sha 87dd95f3cd0b1d72dc558381021b7057472cff46

Special cases for start & end point in `find_one_x` 🌐

view details

Laurenz

commit sha 4b17dd599a65e29ff61c08e1f195e23b79a0d875

Place object at the top when possible ⏫ The only interesting width monotonicity for placement is widening - because shrinking means that if it does not fit at the top, it will also not fit at the bottom. Thus, there is no need for min and max, but one can simply try to place at the top and if it doesn't fit, but does fit at the bottom then min=top and max=bot.

view details

Laurenz

commit sha 592249f74020403b0505ec2df9989cc351c4b6fd

Search over multiple segments 📨

view details

Laurenz

commit sha 38fe47fa95dfb72f4071e2184157d7f64c983bba

Factor out root-finding 🧳

view details

Laurenz

commit sha 2875d60f80967f9b795cbb26fd94a32bfee327ff

Very naive initial collider group construction 🏗

view details

Laurenz

commit sha 00c8a004c1d47b8baca2ec9f829b19fa6568df6d

Tests for bezier basis & solving for coordinates ✅

view details

Laurenz

commit sha 45c4222dde894a0ecd8dc47f3bdd23a7da0276ff

Switch from own geometry types to kurbo 🎋

view details

Laurenz

commit sha 4568b95ebff15c959d20e1a3cf5905abf6fb87ff

Implement ParamCurveSolve for all kinds of segments 🌂

view details

Laurenz

commit sha 3661d71692c4804ea110a75165e1722eb88d009a

Create approx ordering function 🌪

view details

Laurenz

commit sha 345b791b61f6ec359417a1390a52f673f5325b15

Build rows for collider construction 🎞

view details

Laurenz

commit sha fc057a6afa3aecf393d84e769d8c16eb12ec158b

Merge neighbouring borders into placement segments 🧲

view details

push time in a month

push eventtypst/shape-group

Laurenz

commit sha ac72f156e7bc0c06fc95f35da7834fadea1bd0dc

Adapt to new kurbo 🔼

view details

push time in a month

fork laurmaedje/kurbo

A Rust library for manipulating curves

fork in a month

issue closedlinebender/kurbo

Converting iterator of path elements to segments

There is currently no built-in way to transform an iterator of path elements to an iterator of path segments. My use case is to take an argument of type impl Shape and to iterate over its segments. Shape::to_bez_path only returns an iterator over path elements. I could, of course, collect into a BezPath and the call segments() on that, but I'd like to avoid the intermediate allocation.

Looking into the source code, there is the private function BezPath::segments_of_slice, which has a todo that it should maybe be public, and there is the underlying BezPathSegs iterator. Unfortunately, segments_of_slice still wouldn't work with general iterators.

To fix this, how about adding a freestanding function like this:

pub fn segments(path: impl IntoIterator<Item = PathEl>) -> impl Iterator<Item = PathSeg> { ... }

This function would be nicely parallel to flatten and could similarly link to BezPath::segments, which would then be implemented in terms of it. segments_of_slice could then be removed, I think.

If you think this sounds like a good way to do it, I'd be happy to open a PR!

(An alternative, also stated in segments_of_slice's comment, would be to create a trait such that iter.segments() works on any iterator. Personally, I think this wouldn't be worth it and the parallelity with flatten would also be lost.)

closed time in a month

laurmaedje

delete branch laurmaedje/kurbo

delete branch : segments

delete time in a month

Pull request review commentlinebender/kurbo

Restructured path handling in Shape and segments iterator

 pub trait Shape: Sized {     /// iterators from complex shapes without cloning.     ///     /// [GAT's]: https://github.com/rust-lang/rust/issues/44265-    fn to_bez_path(&self, tolerance: f64) -> Self::BezPathIter;+    fn to_path_elements(&self, tolerance: f64) -> Self::PathElementsIter;

I like that, though I'm not sure about the should-not-allocate part since it does allocate for <BezPath as Shape>::path_elements (at least until GATs land).

In any case I'm happy to touch up the docs in a subsequent PR, I think this is in a good place to merge.

Sounds good!

laurmaedje

comment created time in a month

PullRequestReviewEvent

Pull request review commentlinebender/kurbo

Restructured path handling in Shape and segments iterator

 pub trait Shape: Sized {     /// iterators from complex shapes without cloning.     ///     /// [GAT's]: https://github.com/rust-lang/rust/issues/44265-    fn to_bez_path(&self, tolerance: f64) -> Self::BezPathIter;+    fn to_path_elements(&self, tolerance: f64) -> Self::PathElementsIter;

Changed. I've also reworded the documentation a little because it said "Convert to an iterator ..." previously, which is not really accurate.

I changed it to the follwing:

Returns an iterator over this shape expressed as Bézier path elements.

I'm not super happy with the wording, but it's the best I've got. I'm open to suggestions.

laurmaedje

comment created time in a month

PullRequestReviewEvent

push eventlaurmaedje/kurbo

Laurenz

commit sha d0e1effb9522762d387989260b6bcde87c47c69c

Remove to_ prefix

view details

push time in a month

PR opened linebender/kurbo

Restructured path handling shape and add segments iterator

This implements the plans laid out in #136.

BezPathSegs got a bit refactored and renamed to Segments created by the public segments function.

Shape gets the following changes:

  • BezPathIter -> PathElementsIter
  • to_bez_path -> to_path_elements
  • into_bez_path -> into_path and BezPath actually uses this for zero-cost conversion
  • adds new to_path that takes &self

Apart from updating the primitive shapes to the new Shape interface, I was able to make some functions for Arc and Ellipse zero-allocation by using methods directly in Segments instead of creating an intermediate BezPath. I marked the methods on Segments as pub(crate) for now, but maybe (as a previous TODO also already indicated) it would be a good idea to make them fully public.

+194 -107

0 comment

10 changed files

pr created time in a month

create barnchlaurmaedje/kurbo

branch : segments

created branch time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

If BezPath::segments is replaced by a version that takes IntoIterator rather than a slice, that seems to me to take care of all reasonable use cases. I certainly don't have any attachment to the current signature, and there are comments suggesting I was still trying to figure out the best API :)

That definitely applies to the private BezPath::segments_of_slice. However, I just realized that it probably still makes sense to keep the public BezPath::segments because it can omit the tolerance parameter (in contrast to the generic Shape impl) and it can be chained (in contrast to the freestanding segments). So it's kind of similar to BezPath::flatten.

laurmaedje

comment created time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

Yes, this looks right to me. If we're fine with that, I'd have a stab at implementing this tomorrow. The two remaining questions are:

  • Remove BezPath::segments or keep it. I don't really have a strong opinion, here.
  • Whether to have the with-tolerance and without-tolerance variants. I tend to agree with this (having just the one with tolerance), but ultimately I think both are fine, it's just that a general decision needs to be made for the crate.
laurmaedje

comment created time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

The small difference is that the proposed to_path would actually return a BezPath and not an iterator. The iterator would be produced by to_path_elements and that's the only mandatory method, then.

laurmaedje

comment created time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

Then maybe we could have both to_path and into_path with into_path having a default impl in terms of to_path, which can be overriden for BezPath?

Regarding the name, I agree, path_segments or maybe to_path_segments (analogously to to_path) would be good.

Regarding the tolerance: Even with the DEFAULT_TOLERANCE, we would still need both segments and segments_with_tolerance, right? Because sometimes you want to decide tolerance at the call-site. Here, it would really be nice if rust had default arguments ...

laurmaedje

comment created time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

Then, I would propose the following:

  • Rename Shape::BezPathIter to ElementsIter
  • Rename Shape::to_bez_path to elements
  • Rename Shape::into_bez_path to to_path (and maybe also change self to &self?)
  • Add a provided method fn segments(&self, tolerance: f64) -> Segments<Self::ElementsIter> { ... } to Shape
  • Add a freestanding method fn segments<I>(elements: I) -> Segments<I::IntoIter> where I: IntoIterator<Item = PathEl> { ... }
  • Either implement BezPath::segments in terms of Shape::segments or remove it
laurmaedje

comment created time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

Do you mean keep to_bez_path mandatory, deprecate it and basically add an elements alias? Then, users would still be forced to implement that deprecated method.

I think, there are basically two options:

  • Accept breakage, rename everything and depart to kurbo 0.7.
  • Keep everything, deprecate nothing, create the freestanding segments function and
    • optionally add segments to Shape
    • very optionally add elements as an alias for to_bez_path

There is also the thing that BezPath::segments exists, which would not be needed anymore with Shape having segments. This could, however, be deprecated unbreakingly if Shape::segments gets added (I think).

laurmaedje

comment created time in a month

push eventlaurmaedje/kurbo

Laurenz

commit sha 6ac53a78231d35769e00840ddde177f77bf2cfad

Add Rect::is_empty and Size::{area, is_empty}

view details

push time in a month

delete branch laurmaedje/kurbo

delete branch : rect-intersects

delete time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

I think this would work, but still be a breaking change because even though users of Shape can transparently use elements or to_bez_path, implementors of Shape would get an error because the mandatory method elements is missing. If you are fine with this breakage, it would be okay.

laurmaedje

comment created time in a month

delete branch laurmaedje/kurbo

delete branch : is-empty

delete time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

I think there are a couple of problems here in terms of implementation.

  1. If we deprecate to_bez_path, into_bez_path (and probably also BezPathIter) and replace them with elements, to_bezpath and ElementsIter, even with deprecation this would be breaking because one would need to be implemented in terms of the other and (1) implementing to_bez_path through elements would force users to implement elements while (2) implementing elements through to_bez_path would force users to implement a deprecated item. I don't see how to fix this apart from simply renaming the items (instead of deprecating) and accepting the breakage.
  2. If we add segments in terms of elements, the freestanding segments function can't return an abstract impl Iterator<Item = PathSeg> because we need to name the return type in the trait. Also, associated type defaults are unstable, so we can't do fn segments(&self) -> Self::SegmentsIter { ... } and type SegmentsIter: Iterator<Item = PathSeg> = MyIter<Self::ElementsIter>, but would have to name the iterator directly in segments' return type (and it would need to be public). Fixing the type would make it basically impossible to override the method (but then again, I also don't see a real reason to override it).

I don't see a really clear path forward, here.

laurmaedje

comment created time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

I think to_bezpath or to_path are pretty reasonable choices, considering that the method returns a BezPath. I don't like to_bezier all too much because there is no type called Bezier and it's also not clear that it is a path of bezier curves and not a single curve.

laurmaedje

comment created time in a month

pull request commentlinebender/kurbo

Add Rect::is_empty and Size::{area, is_empty}

Ah, I forgot, should I add a comment to intersect about using is_empty to this PR?

laurmaedje

comment created time in a month

PR closed linebender/kurbo

Add Rect::intersects

This adds a method to check whether two rectangles overlap.

Small note: In Rect::contains (for points) the left and top edges are inclusive, while the right and bottom edges are exclusive. Here, however, I think all comparison should be less/greater-than since even if the edges coincide (and the rectangles just touch each other), the intersected area is still zero and the method should return false.

+12 -0

13 comments

1 changed file

laurmaedje

pr closed time in a month

pull request commentlinebender/kurbo

Add Rect::intersects

Since we decided on a.intersect(b).is_empty() I'm going to close this.

laurmaedje

comment created time in a month

PR opened linebender/kurbo

Add Rect::is_empty and Size::{area, is_empty}

This adds is_empty to Rect and Size as discussed in #134. I also added Size::area to implement is_empty in terms of it.

+22 -0

0 comment

2 changed files

pr created time in a month

create barnchlaurmaedje/kurbo

branch : is-empty

created branch time in a month

push eventlaurmaedje/kurbo

Laurenz

commit sha 92112048be289594aa7830b2fabc8eab0b1c7d8e

Implement Mul<PathSeg> for Affine/TranslateScale

view details

push time in a month

pull request commentlinebender/kurbo

Add Rect::intersects

Hmm, that's a good point. I'd also be fine with just going the is_empty route. Then, we could maybe add a doc-comment to intersect telling that the canonical way to just check for intersection is a.intersect(b).is_empty().

laurmaedje

comment created time in a month

issue commentlinebender/kurbo

Converting iterator of path elements to segments

Adding it to Shape sounds good to me. I agree that the naming of to_bez_path is confusing, I tripped over this multiple times. If we would rename to_bez_path, I think both methods should be named similarly, like elements and segments or iter_elements and iter_segments.

laurmaedje

comment created time in a month

pull request commentlinebender/kurbo

Add Rect::intersects

Though, no matter how the intersection problem is dealt with, I think having is_empty would definitely be useful on its own.

laurmaedje

comment created time in a month

pull request commentlinebender/kurbo

Add Rect::intersects

I agree that going with is_empty would be quite elegant, but (at least if I have done this correctly - never used GodBolt before really) it also seems that notably more code is emitted by the compiler, which will probably make things slower: https://godbolt.org/z/YPMcss

Personally, when I needed this, I was surprised that it didn't exist yet. I would have thought that bounding-box intersection testing is a rather common thing to do (maybe more so in physics-related code).

laurmaedje

comment created time in a month

delete branch laurmaedje/kurbo

delete branch : affine-ts-mul-pathseg

delete time in a month

issue openedlinebender/kurbo

Converting iterator of path elements to segments

There is currently no built-in way to transform an iterator of path elements to an iterator of path segments. My use case is to take an argument of type impl Shape and to iterate over its segments. Shape::to_bez_path only returns an iterator over path elements. I could, of course, collect into a BezPath and the call segments() on that, but I'd like to avoid the intermediate allocation.

Looking into the source code, there is the private function BezPath::segments_of_slice, which has a todo that it should maybe be public, and there is the underlying BezPathSegs iterator. Unfortunately, segments_of_slice still wouldn't work with general iterators.

To fix this, how about adding a freestanding function like this:

pub fn segments(path: impl IntoIterator<Item = PathEl>) -> impl Iterator<Item = PathSeg> { ... }

This function would be nicely parallel to flatten and could similarly link to BezPath::segments, which would then be implemented in terms of it. segments_of_slice could then be removed, I think.

If you think this sounds like a good way to do it, I'd be happy to open a PR!

(An alternative, also stated in segments_of_slice's comment, would be to create a trait such that iter.segments() works on any iterator. Personally, I think this wouldn't be worth it and the parallelity with flatten would also be lost.)

created time in a month

pull request commentlinebender/kurbo

Add Rect::intersects

I implemented a recursive bounding-box-based curve-curve intersection algorithm and need to stop when the curve's bounding boxes don't overlap. Something like a.intersect(b) and then somehow checking whether that rect it is empty would probably work, but I think this way it would be cleaner. It's also probably faster (unless the compiler is smart enough). Currently, I have a RectExt extension trait for this, but I thought it might be useful to others.

laurmaedje

comment created time in a month

PR opened linebender/kurbo

Implement Mul<PathSeg> for Affine/TranslateScale

As BezPath and PathEl already have it, this should hopefully be a straightforward addition.

+24 -0

0 comment

1 changed file

pr created time in a month

create barnchlaurmaedje/kurbo

branch : affine-ts-mul-pathseg

created branch time in a month

PR opened linebender/kurbo

Add Rect::intersects

This adds a method to check whether two rectangles overlap.

Small note: In Rect::contains (for points) the left and top edges are inclusive, while the right and bottom edges are exclusive. Here, however, I think all comparison should be less/greater-than since even if the edges coincide (and the rectangles just touch each other), the intersected area is still zero and the method should return false.

+12 -0

0 comment

1 changed file

pr created time in a month

create barnchlaurmaedje/kurbo

branch : rect-intersects

created branch time in a month

fork laurmaedje/kurbo

A Rust library for manipulating curves

fork in a month

startedlinebender/kurbo

started time in a month

delete branch laurmaedje/rechner

delete branch : master

delete time in a month

create barnchlaurmaedje/rechner

branch : main

created branch time in a month

delete branch laurmaedje/arm-hello

delete branch : master

delete time in a month

create barnchlaurmaedje/arm-hello

branch : main

created branch time in a month

delete branch laurmaedje/leval

delete branch : master

delete time in a month

more