Skip to content

gomminjae/melan-core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

English | 한국어

Melan-Core

Cross-platform handwriting engine written in Rust. Delivers consistent stroke quality across iOS and Android.

Architecture

┌─────────────────────────────────────────────────────┐
│  Native App (Swift / Kotlin)                        │
│  Touch input → Consume render commands              │
└────────────────────┬────────────────────────────────┘
                     │ FFI
┌────────────────────▼────────────────────────────────┐
│  melan-ffi           UniFFI 0.28 bindings           │
│  RwLock<DrawEngine> → Swift/Kotlin type conversion  │
└────────────────────┬────────────────────────────────┘
                     │
┌────────────────────▼────────────────────────────────┐
│  melan-core          Pure Rust engine               │
│  Coordinates · Bézier · Variable width · Undo/Redo  │
└─────────────────────────────────────────────────────┘

How It Works

Poll-based rendering — Feed input, get render commands back.

engine.begin_stroke(screen, pressure, timestamp)Vec<RenderCommand>
engine.add_point(screen, pressure, timestamp)Vec<RenderCommand>  // incremental
engine.end_stroke()Vec<RenderCommand>  // full re-render

The app just consumes commands via switch and draws. Pressure calculation, Bézier interpolation, coordinate conversion, and undo history are all handled by the engine.

RenderCommand

Command Description
Clear Fill canvas with background color
SaveState / RestoreState Save/restore graphics context
SetTransform Apply zoom & pan transform
DrawVariableWidthPath Draw variable-width Bézier path

Crate Structure

crates/
├── melan-core/        # Pure Rust engine (minimal dependencies)
│   ├── canvas.rs      #   DrawEngine — main API
│   ├── brush.rs       #   Brush config (Pen, Highlighter, Eraser)
│   ├── coordinate/    #   Document ↔ Screen coordinate conversion
│   ├── geometry.rs    #   Catmull-Rom → cubic Bézier conversion
│   ├── render.rs      #   RenderCommand generation
│   ├── format/        #   JSON / Protobuf serialization
│   ├── history.rs     #   Undo / Redo stack
│   ├── layer.rs       #   Layer management
│   └── stroke.rs      #   Stroke builder
│
├── melan-ffi/         # UniFFI bindings (shared by iOS & Android)
│   ├── engine.rs      #   MelanEngine wrapper (RwLock)
│   └── types.rs       #   FFI type definitions + From conversions
│
└── uniffi-bindgen/    # Swift / Kotlin code generation CLI

API

Engine

MelanEngine::new(canvas_size)   Create with custom size
MelanEngine::new_a4()           Create A4 (595×842pt)

Drawing

set_brush(config)                          Set brush
begin_stroke(x, y, pressure, timestamp)    Start stroke
add_point(x, y, pressure, timestamp)       Add point (incremental render)
end_stroke()                               End stroke (full re-render)

Edit

undo() / redo()      Undo / Redo
clear_all()          Clear all strokes

Viewport

zoom(factor, focal_x, focal_y)   Pinch zoom (focal point fixed)
pan(dx, dy)                      Pan
reset_viewport()                 Reset zoom & pan

State & Persistence

full_render()        Full scene render commands
get_state()          Query engine state (stroke_count, can_undo, scale...)
save(format)         Serialize to JSON/Protobuf → Vec<u8>
load(data)           Restore engine state from bytes

Build

Native (Dev & Test)

cargo test --workspace    # Run 44 tests
cargo build -p melan-ffi  # Build FFI library

iOS (xcframework)

rustup target add aarch64-apple-ios aarch64-apple-ios-sim
./scripts/build-ios.sh

Output:

  • MelanCoreFFI.xcframework (device + simulator)
  • Sources/MelanCore/MelanCoreFFI.swift (Swift bindings)

iOS SPM Package

Build artifacts are distributed via SPM from melan-swift.

// Xcode → Add Package → https://github.com/gomminjae/melan-swift.git
import MelanCore

let engine = MelanEngine.newA4()

Android (JNI + Kotlin)

# Install prerequisites
cargo install cargo-ndk
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/<version>

./scripts/build-android.sh

Output:

  • android/jniLibs/<abi>/libmelan_ffi.so (arm64-v8a, armeabi-v7a, x86_64, x86)
  • android/kotlin/ (Kotlin bindings)

Android Usage

Copy android/jniLibs/ and android/kotlin/ into your Android project, then:

import com.melan.core.*

val engine = MelanEngine.newA4()

Coordinate System

Document coordinates (engine internal)
  - Origin: top-left (0, 0)
  - Unit: 1pt = 1/72 inch (PDF points)
  - Y-axis: increases downward

Screen coordinates (platform input)
  - Pass touch coordinates as-is
  - Viewport converts automatically
  - screen = (document + offset) × scale

License

MIT License - see LICENSE

About

Rust handwriting core for iOS/iPadOS/Android (UniFFI)

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors