Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.mbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ test "basic_shapes" (it : @test.Test) {
.close_path()

// Create SVG output with advanced shapes
let svg_doc = @vg.new_svg(200.0, 200.0)
let svg_doc = @svg.new_svg(200.0, 200.0)
.render_circle(@vg.Point::new(100.0, 100.0), 50.0, @color.red())
.render_ellipse(@vg.Point::new(150.0, 100.0), 30.0, 20.0, @color.blue())
.render_path(custom_path, @color.green())
Expand Down Expand Up @@ -225,7 +225,7 @@ test "paths examples" {
}

// Render path to SVG
let svg = @vg.new_svg(100.0, 100.0).render_path(path, @color.green())
let svg = @svg.new_svg(100.0, 100.0).render_path(path, @color.green())

// Use the variables to avoid unused warnings
ignore(rectangle)
Expand Down
195 changes: 195 additions & 0 deletions REFACTORING_SVG_PACKAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# SVG Package Refactoring

**Date:** November 22, 2025

## Overview

This refactoring splits out the `SvgDocument` type and all SVG rendering utilities into a separate `svg` package to improve modularity and reduce coupling in the VG vector graphics library. This follows the same pattern previously used for the geometry and color package refactorings. Importantly, the `Image::render_image_to_svg` method is kept in the main `vg` package as it represents a rendering backend method for the Image type.

## Changes Made

### New Package Structure

Created `svg/` package with:
- `svg/types.mbt` - Core SvgDocument type definition
- `svg/svg.mbt` - All SVG rendering methods and utilities
- `svg/moon.pkg.json` - Package configuration with dependencies on geometry and color packages

### Type Migration

**Moved from `vg` package to `svg` package:**
- `SvgDocument` struct - SVG document representation with width, height, and elements array

**SVG functions moved:**
- Document creation: `new_svg`
- Document manipulation: `SvgDocument::add_element`, `SvgDocument::to_string`
- Rendering methods: `SvgDocument::render_circle`, `SvgDocument::render_rectangle`, `SvgDocument::render_path`, `SvgDocument::render_line`, `SvgDocument::render_text`, `SvgDocument::render_ellipse`, `SvgDocument::render_polygon`, `SvgDocument::render_linear_gradient`
- Helper functions: `color_to_svg`, `point_to_svg`, `path_to_svg_data`

**Kept in `vg` package:**
- `Image::render_image_to_svg` - Image rendering method (uses `@svg` namespace internally)

### Dependency Management

**Main package (`vg`) changes:**
- Updated `moon.pkg.json` to import `bobzhang/vg/svg`
- Modified `types.mbt` to re-export SvgDocument type using type alias:
```moonbit
pub type SvgDocument = @svg.SvgDocument
```
- Updated `svg.mbt` to only contain `Image::render_image_to_svg` method
- Image rendering method now uses `@svg.new_svg` to create SVG documents

**SVG package dependencies:**
- Imports `bobzhang/vg/geometry` for Point and Path types
- Imports `bobzhang/vg/color` for Color type and utilities

### Code Updates Required

**In test files and documentation:**
- Replaced `@vg.new_svg` with `@svg.new_svg`
- All SvgDocument methods now accessed through the `@svg` namespace

**Examples:**
```moonbit
// Before:
let doc = @vg.new_svg(100.0, 100.0)
.render_circle(Point::new(50.0, 50.0), 25.0, @color.red())

// After:
let doc = @svg.new_svg(100.0, 100.0)
.render_circle(Point::new(50.0, 50.0), 25.0, @color.red())
```

**Files updated:**
- All test files (svg_test.mbt, oo_api_test.mbt, renderer_test.mbt, hello_test.mbt, advanced_test.mbt)
- README.mbt.md
- svg.mbt (now only contains Image::render_image_to_svg)
- types.mbt (added SvgDocument type re-export)
- moon.pkg.json (added svg package import)

### Backward Compatibility

The SvgDocument type remains accessible through the main package:
- The SvgDocument type is re-exported from the main package via type alias
- All existing type references like `SvgDocument` still work
- However, the creation and manipulation functions now require the `@svg` namespace
- This is a breaking change in the public API (functions moved from `@vg` to `@svg` namespace)

### Test Results

- All 175 tests pass
- No errors, only existing warnings about using package-qualified names in geometry package tests
- Build verification: `moon check` and `moon test` both succeed

## Migration Guide for External Users

If you're using the VG library and upgrading:

### Using SVG Functions

SVG functions must now be accessed through the `@svg` namespace:

```moonbit
// Creating SVG documents
let doc = @svg.new_svg(200.0, 200.0)

// Rendering shapes
let doc = @svg.new_svg(100.0, 100.0)
.render_circle(@vg.Point::new(50.0, 50.0), 25.0, @color.red())
.render_rectangle(10.0, 10.0, 80.0, 80.0, @color.blue())
.render_text("Hello", @vg.Point::new(50.0, 50.0), 16.0, @color.black())

// Converting to string
let svg_string = doc.to_string()
```

### Using the SvgDocument Type

The SvgDocument type is still available through the main package via type alias:

```moonbit
// Type references work as before
let doc : SvgDocument = @svg.new_svg(100.0, 100.0)
let doc2 : @vg.SvgDocument = @svg.new_svg(100.0, 100.0)
```

### Image to SVG Rendering

The `Image::render_image_to_svg` method remains in the main `vg` package:

```moonbit
let img = @vg.Image::circle(@color.red(), 25.0)
let svg_string = img.render_image_to_svg(100.0, 100.0, 20)
```

### Migration from Previous Version

If you have existing code that used `@vg.new_svg`, update to use `@svg`:

```moonbit
// Before:
let doc = @vg.new_svg(100.0, 100.0)

// After:
let doc = @svg.new_svg(100.0, 100.0)
```

## Benefits

1. **Modularity** - SVG rendering types and utilities can be used independently of the full VG library
2. **Clarity** - Clear separation between SVG rendering backend and core image/graphics functionality
3. **Testability** - SVG package can have its own focused test suite
4. **Reusability** - Other packages can depend on just the svg package if needed for SVG generation
5. **Type Safety** - No change in type safety, all benefits of strong typing preserved
6. **Consistency** - Follows the same pattern as the geometry and color package refactorings
7. **Proper Architecture** - Keeps Image methods in the main package while SVG document operations are in the svg package

## Files Modified

- `svg/` - New package directory with types and rendering utilities
- `types.mbt` - Added SvgDocument type re-export
- `svg.mbt` - Reduced to only contain Image::render_image_to_svg method
- All test files - Updated from `@vg.new_svg()` to `@svg.new_svg()`
- `README.mbt.md` - Updated all examples to use `@svg` namespace
- `moon.pkg.json` - Added svg package import

## Files Structure

```
vg/
├── color/
│ ├── types.mbt # Color struct definition
│ ├── color.mbt # Color utilities and functions
│ ├── color_test.mbt # Color-specific tests
│ └── moon.pkg.json # Color package configuration
├── geometry/ # Geometry package (previously refactored)
│ ├── types.mbt
│ ├── point.mbt
│ ├── path.mbt
│ ├── transform.mbt
│ └── moon.pkg.json
├── svg/
│ ├── types.mbt # SvgDocument struct definition
│ ├── svg.mbt # SVG rendering methods and utilities
│ └── moon.pkg.json # SVG package configuration (depends on geometry and color)
├── types.mbt # Type re-exports (Color, Point, Path, SvgDocument, etc.)
├── svg.mbt # Image::render_image_to_svg only
└── moon.pkg.json # Main package configuration (imports color, geometry, svg packages)
```

## Notes

- The svg package depends on both geometry and color packages
- Type aliases ensure SvgDocument type is available through `@vg.SvgDocument`
- All SVG functions must be accessed via `@svg` namespace (no re-export wrappers)
- `Image::render_image_to_svg` remains in the main `vg` package as specified in the requirements
- This refactoring changes the public API - SVG functions moved from `@vg` to `@svg` namespace
- The pattern matches geometry and color packages: types re-exported, functions in dedicated namespace

## Future Work

With geometry, color, and svg now separated into independent packages, the library has a clean modular architecture. Future work could include:
- Additional rendering backends (Canvas, PDF) as separate packages
- Image composition and manipulation utilities as a separate package
- More advanced SVG features (filters, animations, etc.)
8 changes: 4 additions & 4 deletions advanced_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test "quadratic bezier curves" (it : @test.Test) {
qcurve_path,
content="Path([MoveTo({x: 10, y: 50}), QCurveTo({x: 50, y: 10}, {x: 90, y: 50}), QCurveTo({x: 50, y: 90}, {x: 10, y: 50}), Close])",
)
let doc = @vg.new_svg(100.0, 100.0)
let doc = @svg.new_svg(100.0, 100.0)
.render_rectangle(0.0, 0.0, 100.0, 100.0, @color.gray(0.95))
.render_path(qcurve_path, @color.blue())
let svg_string = doc.to_string()
Expand All @@ -30,7 +30,7 @@ test "elliptical arcs" (it : @test.Test) {
arc_path,
content="Path([MoveTo({x: 20, y: 50}), EArcTo(30, 20, 0, false, true, {x: 80, y: 50}), EArcTo(30, 20, 0, false, true, {x: 20, y: 50}), Close])",
)
let doc = @vg.new_svg(100.0, 100.0)
let doc = @svg.new_svg(100.0, 100.0)
.render_rectangle(0.0, 0.0, 100.0, 100.0, @color.gray(0.95))
.render_path(arc_path, @color.green())
let svg_string = doc.to_string()
Expand All @@ -53,7 +53,7 @@ test "smooth curve stitching" (it : @test.Test) {
smooth_path,
content="Path([MoveTo({x: 10, y: 50}), CurveTo({x: 30, y: 10}, {x: 50, y: 10}, {x: 70, y: 50}), CurveTo({x: 90, y: 90}, {x: 110, y: 90}, {x: 130, y: 50}), CurveTo({x: 150, y: 10}, {x: 150, y: 10}, {x: 170, y: 50})])",
)
let doc = @vg.new_svg(180.0, 100.0)
let doc = @svg.new_svg(180.0, 100.0)
.render_rectangle(0.0, 0.0, 180.0, 100.0, @color.gray(0.95))
.render_path(smooth_path, @color.purple())
let svg_string = doc.to_string()
Expand Down Expand Up @@ -126,7 +126,7 @@ test "checkerboard pattern" (it : @test.Test) {

///|
test "advanced path showcase" (it : @test.Test) {
let doc = @vg.new_svg(400.0, 300.0)
let doc = @svg.new_svg(400.0, 300.0)
.render_rectangle(0.0, 0.0, 400.0, 300.0, @color.gray(0.98))
.render_text(
"Advanced Path Features",
Expand Down
4 changes: 2 additions & 2 deletions hello_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test "path to SVG integration" {
.close_path()

// Render to SVG
let doc = @vg.new_svg(150.0, 100.0).render_path(path, @color.magenta())
let doc = @svg.new_svg(150.0, 100.0).render_path(path, @color.magenta())
let svg_string = doc.to_string()
if not(svg_string.contains("path")) {
fail("Should contain path element")
Expand Down Expand Up @@ -154,7 +154,7 @@ test "color operations" {
///|
test "complete vg library showcase" (it : @test.Test) {
// Create a comprehensive demo showcasing all VG features
let doc = @vg.new_svg(600.0, 500.0)
let doc = @svg.new_svg(600.0, 500.0)
.render_rectangle(0.0, 0.0, 600.0, 500.0, @color.gray(0.97)) // Light background
.render_text(
"MoonBit VG Library - Complete Showcase",
Expand Down
3 changes: 2 additions & 1 deletion moon.pkg.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"is-main": false,
"import": [
"bobzhang/vg/geometry",
"bobzhang/vg/color"
"bobzhang/vg/color",
"bobzhang/vg/svg"
]
}
10 changes: 5 additions & 5 deletions oo_api_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
///|
test "svg oo-style api" (it : @test.Test) {
// Demonstrate fluent OO-style API for SVG
let doc = @vg.new_svg(300.0, 200.0)
let doc = @svg.new_svg(300.0, 200.0)
.render_rectangle(0.0, 0.0, 300.0, 200.0, @color.gray(0.95))
.render_circle(@vg.Point::new(80.0, 100.0), 30.0, @color.red())
.render_rectangle(150.0, 70.0, 60.0, 60.0, @color.green())
Expand Down Expand Up @@ -83,7 +83,7 @@ test "oo-style path with document integration" (it : @test.Test) {
.close_path()

// Use the same path across all OO-style renderers
let svg_doc = @vg.new_svg(250.0, 150.0)
let svg_doc = @svg.new_svg(250.0, 150.0)
.render_rectangle(0.0, 0.0, 250.0, 150.0, @color.white())
.render_path(custom_path, @color.purple())
.render_text(
Expand Down Expand Up @@ -125,7 +125,7 @@ test "oo-style api comparison" (it : @test.Test) {
// Compare functional vs OO-style APIs

// Functional style (original)
let svg_functional = @vg.new_svg(200.0, 100.0)
let svg_functional = @svg.new_svg(200.0, 100.0)
.render_circle(@vg.Point::new(100.0, 50.0), 25.0, @color.red())
.render_text(
"Functional Style",
Expand All @@ -135,7 +135,7 @@ test "oo-style api comparison" (it : @test.Test) {
)

// OO style (new)
let svg_oo = @vg.new_svg(200.0, 100.0)
let svg_oo = @svg.new_svg(200.0, 100.0)
.render_circle(@vg.Point::new(100.0, 50.0), 25.0, @color.red())
.render_text("OO Style", @vg.Point::new(100.0, 80.0), 12.0, @color.black())

Expand Down Expand Up @@ -175,7 +175,7 @@ test "complete oo-style showcase" (it : @test.Test) {
.smooth_ccurve_to(@vg.Point::new(225.0, 125.0), @vg.Point::new(275.0, 75.0))
.earc_to(25.0, 15.0, 0.0, false, true, @vg.Point::new(225.0, 100.0))
.close_path()
let svg_showcase = @vg.new_svg(350.0, 200.0)
let svg_showcase = @svg.new_svg(350.0, 200.0)
.render_rectangle(0.0, 0.0, 350.0, 200.0, @color.gray(0.98))
.render_text(
"Complete OO-Style API Showcase",
Expand Down
4 changes: 2 additions & 2 deletions renderer_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ test "renderer comparison showcase" (it : @test.Test) {
// Create the same graphics with all three renderers

// SVG version
let svg_doc = @vg.new_svg(250.0, 150.0)
let svg_doc = @svg.new_svg(250.0, 150.0)
.render_rectangle(0.0, 0.0, 250.0, 150.0, @color.gray(0.95))
.render_circle(@vg.Point::new(125.0, 75.0), 40.0, @color.red())
.render_text("VG Demo", @vg.Point::new(125.0, 30.0), 16.0, @color.black())
Expand Down Expand Up @@ -195,7 +195,7 @@ test "complete feature showcase all renderers" (it : @test.Test) {
.close_path()

// SVG with all advanced features
let svg_doc = @vg.new_svg(350.0, 200.0)
let svg_doc = @svg.new_svg(350.0, 200.0)
.render_rectangle(0.0, 0.0, 350.0, 200.0, @color.white())
.render_text(
"Complete VG Feature Showcase",
Expand Down
Loading