diff --git a/README.mbt.md b/README.mbt.md index db0d708..c9bc17a 100644 --- a/README.mbt.md +++ b/README.mbt.md @@ -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()) @@ -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) diff --git a/REFACTORING_SVG_PACKAGE.md b/REFACTORING_SVG_PACKAGE.md new file mode 100644 index 0000000..10efbcb --- /dev/null +++ b/REFACTORING_SVG_PACKAGE.md @@ -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.) diff --git a/advanced_test.mbt b/advanced_test.mbt index 967e5fe..95af31c 100644 --- a/advanced_test.mbt +++ b/advanced_test.mbt @@ -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() @@ -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() @@ -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() @@ -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", diff --git a/hello_test.mbt b/hello_test.mbt index 2d02816..938e0c4 100644 --- a/hello_test.mbt +++ b/hello_test.mbt @@ -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") @@ -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", diff --git a/moon.pkg.json b/moon.pkg.json index 5cef213..41a7e5b 100644 --- a/moon.pkg.json +++ b/moon.pkg.json @@ -2,6 +2,7 @@ "is-main": false, "import": [ "bobzhang/vg/geometry", - "bobzhang/vg/color" + "bobzhang/vg/color", + "bobzhang/vg/svg" ] } \ No newline at end of file diff --git a/oo_api_test.mbt b/oo_api_test.mbt index 7183829..5c7c941 100644 --- a/oo_api_test.mbt +++ b/oo_api_test.mbt @@ -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()) @@ -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( @@ -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", @@ -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()) @@ -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", diff --git a/renderer_test.mbt b/renderer_test.mbt index 51cf472..4f0d3da 100644 --- a/renderer_test.mbt +++ b/renderer_test.mbt @@ -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()) @@ -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", diff --git a/svg.mbt b/svg.mbt index e774777..18076e3 100644 --- a/svg.mbt +++ b/svg.mbt @@ -1,206 +1,4 @@ -// SVG rendering backend - -///| -/// SVG document structure -pub struct SvgDocument { - width : Double - height : Double - elements : Array[String] -} derive(Show) - -///| -/// Create a new SVG document -pub fn new_svg(width : Double, height : Double) -> SvgDocument { - { width, height, elements: [] } -} - -///| -/// Add an element to the SVG document (OO-style) -pub fn SvgDocument::add_element( - self : SvgDocument, - element : String, -) -> SvgDocument { - { ..self, elements: [..self.elements, element] } -} - -///| -/// Convert a color to SVG color string -fn color_to_svg(c : Color) -> String { - if c.a < 1.0 { - let r = (c.r * 255.0).to_int() - let g = (c.g * 255.0).to_int() - let b = (c.b * 255.0).to_int() - "rgba(\{r},\{g},\{b},\{c.a})" - } else { - @color.to_hex(c) - } -} - -///| -/// Convert a point to SVG coordinate string -fn point_to_svg(p : Point) -> String { - "\{p.x},\{p.y}" -} - -///| -/// Convert a path to SVG path data -fn path_to_svg_data(path : Path) -> String { - let mut data = "" - for segment in path.0 { - match segment { - MoveTo(p) => data = data + "M \{point_to_svg(p)} " - LineTo(p) => data = data + "L \{point_to_svg(p)} " - CurveTo(cp1, cp2, end) => - data = data + - "C \{point_to_svg(cp1)} \{point_to_svg(cp2)} \{point_to_svg(end)} " - QCurveTo(cp, end) => - data = data + "Q \{point_to_svg(cp)} \{point_to_svg(end)} " - EArcTo(rx, ry, rotation, large_arc, sweep, end) => { - let large_flag = if large_arc { "1" } else { "0" } - let sweep_flag = if sweep { "1" } else { "0" } - data = data + - "A \{rx} \{ry} \{rotation} \{large_flag} \{sweep_flag} \{point_to_svg(end)} " - } - Close => data = data + "Z " - } - } - data -} - -///| -/// Render a circle to SVG (OO-style) -pub fn SvgDocument::render_circle( - self : SvgDocument, - center : Point, - radius : Double, - color : Color, -) -> SvgDocument { - let element = - $| - self.add_element(element) -} - -// ===== OBJECT-ORIENTED API METHODS ===== - -///| -/// Render a rectangle to SVG (OO-style) -pub fn SvgDocument::render_rectangle( - self : SvgDocument, - x : Double, - y : Double, - width : Double, - height : Double, - color : Color, -) -> SvgDocument { - let element = - $| - self.add_element(element) -} - -///| -/// Render a path to SVG (OO-style) -pub fn SvgDocument::render_path( - self : SvgDocument, - path : Path, - color : Color, -) -> SvgDocument { - let data = path_to_svg_data(path) - let element = - $| - self.add_element(element) -} - -///| -/// Render a line to SVG (OO-style) -pub fn SvgDocument::render_line( - self : SvgDocument, - start : Point, - end : Point, - color : Color, - thickness : Double, -) -> SvgDocument { - let element = - $| - self.add_element(element) -} - -///| -/// Render text to SVG (OO-style) -pub fn SvgDocument::render_text( - self : SvgDocument, - text : String, - pos : Point, - size : Double, - color : Color, -) -> SvgDocument { - let element = - $|\{text} - self.add_element(element) -} - -///| -/// Render an ellipse to SVG (OO-style) -pub fn SvgDocument::render_ellipse( - self : SvgDocument, - center : Point, - rx : Double, - ry : Double, - color : Color, -) -> SvgDocument { - let element = - $| - self.add_element(element) -} - -///| -/// Render a polygon to SVG (OO-style) -pub fn SvgDocument::render_polygon( - self : SvgDocument, - points : Array[Point], - color : Color, -) -> SvgDocument { - if points.length() < 3 { - self // Can't render polygon with less than 3 points - } else { - let mut points_str = "" - for i = 0; i < points.length(); i = i + 1 { - let p = points[i] - points_str = points_str + "\{p.x},\{p.y}" - if i < points.length() - 1 { - points_str = points_str + " " - } - } - let element = - $| - self.add_element(element) - } -} - -///| -/// Render a linear gradient to SVG (OO-style) -pub fn SvgDocument::render_linear_gradient( - self : SvgDocument, - id : String, - start : Point, - end : Point, - color1 : Color, - color2 : Color, -) -> SvgDocument { - let gradient = - $|" - self.add_element(gradient) -} - -///| -/// Convert to SVG string (OO-style) -pub fn SvgDocument::to_string(self : SvgDocument) -> String { - ( - $| - $| - $|\{self.elements.map(e => " " + e).join("\n")} - $| - ) -} +// SVG rendering backend - Image method only ///| /// Sample an image at regular intervals and render to SVG (OO-style) @@ -210,7 +8,7 @@ pub fn Image::render_image_to_svg( height : Double, samples : Int, ) -> String { - let mut doc = new_svg(width, height) + let mut doc = @svg.new_svg(width, height) let step = width / samples.to_double() for i in 0.. SvgDocument { + { width, height, elements: [] } +} + +///| +/// Add an element to the SVG document (OO-style) +pub fn SvgDocument::add_element( + self : SvgDocument, + element : String, +) -> SvgDocument { + { ..self, elements: [..self.elements, element] } +} + +///| +/// Convert a color to SVG color string +fn color_to_svg(c : @color.Color) -> String { + if c.a < 1.0 { + let r = (c.r * 255.0).to_int() + let g = (c.g * 255.0).to_int() + let b = (c.b * 255.0).to_int() + "rgba(\{r},\{g},\{b},\{c.a})" + } else { + @color.to_hex(c) + } +} + +///| +/// Convert a point to SVG coordinate string +fn point_to_svg(p : @geometry.Point) -> String { + "\{p.x},\{p.y}" +} + +///| +/// Convert a path to SVG path data +fn path_to_svg_data(path : @geometry.Path) -> String { + let mut data = "" + for segment in path.0 { + match segment { + MoveTo(p) => data = data + "M \{point_to_svg(p)} " + LineTo(p) => data = data + "L \{point_to_svg(p)} " + CurveTo(cp1, cp2, end) => + data = data + + "C \{point_to_svg(cp1)} \{point_to_svg(cp2)} \{point_to_svg(end)} " + QCurveTo(cp, end) => + data = data + "Q \{point_to_svg(cp)} \{point_to_svg(end)} " + EArcTo(rx, ry, rotation, large_arc, sweep, end) => { + let large_flag = if large_arc { "1" } else { "0" } + let sweep_flag = if sweep { "1" } else { "0" } + data = data + + "A \{rx} \{ry} \{rotation} \{large_flag} \{sweep_flag} \{point_to_svg(end)} " + } + Close => data = data + "Z " + } + } + data +} + +///| +/// Render a circle to SVG (OO-style) +pub fn SvgDocument::render_circle( + self : SvgDocument, + center : @geometry.Point, + radius : Double, + color : @color.Color, +) -> SvgDocument { + let element = + $| + self.add_element(element) +} + +// ===== OBJECT-ORIENTED API METHODS ===== + +///| +/// Render a rectangle to SVG (OO-style) +pub fn SvgDocument::render_rectangle( + self : SvgDocument, + x : Double, + y : Double, + width : Double, + height : Double, + color : @color.Color, +) -> SvgDocument { + let element = + $| + self.add_element(element) +} + +///| +/// Render a path to SVG (OO-style) +pub fn SvgDocument::render_path( + self : SvgDocument, + path : @geometry.Path, + color : @color.Color, +) -> SvgDocument { + let data = path_to_svg_data(path) + let element = + $| + self.add_element(element) +} + +///| +/// Render a line to SVG (OO-style) +pub fn SvgDocument::render_line( + self : SvgDocument, + start : @geometry.Point, + end : @geometry.Point, + color : @color.Color, + thickness : Double, +) -> SvgDocument { + let element = + $| + self.add_element(element) +} + +///| +/// Render text to SVG (OO-style) +pub fn SvgDocument::render_text( + self : SvgDocument, + text : String, + pos : @geometry.Point, + size : Double, + color : @color.Color, +) -> SvgDocument { + let element = + $|\{text} + self.add_element(element) +} + +///| +/// Render an ellipse to SVG (OO-style) +pub fn SvgDocument::render_ellipse( + self : SvgDocument, + center : @geometry.Point, + rx : Double, + ry : Double, + color : @color.Color, +) -> SvgDocument { + let element = + $| + self.add_element(element) +} + +///| +/// Render a polygon to SVG (OO-style) +pub fn SvgDocument::render_polygon( + self : SvgDocument, + points : Array[@geometry.Point], + color : @color.Color, +) -> SvgDocument { + if points.length() < 3 { + self // Can't render polygon with less than 3 points + } else { + let mut points_str = "" + for i = 0; i < points.length(); i = i + 1 { + let p = points[i] + points_str = points_str + "\{p.x},\{p.y}" + if i < points.length() - 1 { + points_str = points_str + " " + } + } + let element = + $| + self.add_element(element) + } +} + +///| +/// Render a linear gradient to SVG (OO-style) +pub fn SvgDocument::render_linear_gradient( + self : SvgDocument, + id : String, + start : @geometry.Point, + end : @geometry.Point, + color1 : @color.Color, + color2 : @color.Color, +) -> SvgDocument { + let gradient = + $|" + self.add_element(gradient) +} + +///| +/// Convert to SVG string (OO-style) +pub fn SvgDocument::to_string(self : SvgDocument) -> String { + ( + $| + $| + $|\{self.elements.map(e => " " + e).join("\n")} + $| + ) +} diff --git a/svg/types.mbt b/svg/types.mbt new file mode 100644 index 0000000..3ec2786 --- /dev/null +++ b/svg/types.mbt @@ -0,0 +1,9 @@ +// Core types for SVG rendering + +///| +/// SVG document structure +pub struct SvgDocument { + width : Double + height : Double + elements : Array[String] +} derive(Show) diff --git a/svg_test.mbt b/svg_test.mbt index 27fd822..ec36753 100644 --- a/svg_test.mbt +++ b/svg_test.mbt @@ -2,7 +2,7 @@ ///| test "SVG document creation" { - let doc = @vg.new_svg(100.0, 200.0) + let doc = @svg.new_svg(100.0, 200.0) if doc.width != 100.0 || doc.height != 200.0 { fail("SVG document dimensions incorrect") } @@ -13,7 +13,7 @@ test "SVG document creation" { ///| test "adding elements to SVG" { - let doc = @vg.new_svg(100.0, 100.0) + let doc = @svg.new_svg(100.0, 100.0) .add_element("") .add_element("") if doc.elements.length() != 2 { @@ -23,7 +23,7 @@ test "adding elements to SVG" { ///| test "circle rendering" (it : @test.Test) { - let doc = @vg.new_svg(100.0, 100.0).render_circle( + let doc = @svg.new_svg(100.0, 100.0).render_circle( @vg.Point::new(50.0, 50.0), 25.0, @color.red(), @@ -41,7 +41,7 @@ test "circle rendering" (it : @test.Test) { ///| test "rectangle rendering" (it : @test.Test) { - let doc = @vg.new_svg(200.0, 150.0).render_rectangle( + let doc = @svg.new_svg(200.0, 150.0).render_rectangle( 10.0, 20.0, 50.0, @@ -56,7 +56,7 @@ test "rectangle rendering" (it : @test.Test) { ///| test "path rendering" { let path = @vg.Path::rect(0.0, 0.0, 10.0, 10.0) - let doc = @vg.new_svg(100.0, 100.0).render_path(path, @color.green()) + let doc = @svg.new_svg(100.0, 100.0).render_path(path, @color.green()) if doc.elements.length() != 1 { fail("Should have one path element") } @@ -70,7 +70,7 @@ test "path rendering" { test "line rendering" { let start = @vg.Point::new(0.0, 0.0) let end = @vg.Point::new(100.0, 50.0) - let doc = @vg.new_svg(150.0, 100.0).render_line( + let doc = @svg.new_svg(150.0, 100.0).render_line( start, end, @color.black(), @@ -87,7 +87,7 @@ test "line rendering" { ///| test "text rendering" { - let doc = @vg.new_svg(200.0, 100.0).render_text( + let doc = @svg.new_svg(200.0, 100.0).render_text( "Hello World", @vg.Point::new(50.0, 30.0), 16.0, @@ -104,7 +104,7 @@ test "text rendering" { ///| test "SVG string generation" { - let doc = @vg.new_svg(100.0, 100.0).render_circle( + let doc = @svg.new_svg(100.0, 100.0).render_circle( @vg.Point::new(50.0, 50.0), 25.0, @color.red(), @@ -129,7 +129,7 @@ test "SVG string generation" { ///| test "complex SVG document" { - let doc = @vg.new_svg(200.0, 200.0) + let doc = @svg.new_svg(200.0, 200.0) .render_rectangle(10.0, 10.0, 180.0, 180.0, @color.gray(0.9)) .render_circle(@vg.Point::new(100.0, 100.0), 50.0, @color.red()) .render_line( @@ -166,7 +166,7 @@ test "image to SVG rendering" { ///| test "color with alpha in SVG" { let semi_transparent = @color.rgba(1.0, 0.0, 0.0, 0.5) - let doc = @vg.new_svg(100.0, 100.0).render_circle( + let doc = @svg.new_svg(100.0, 100.0).render_circle( @vg.Point::new(50.0, 50.0), 25.0, semi_transparent, @@ -181,7 +181,7 @@ test "color with alpha in SVG" { ///| test "comprehensive svg rendering" (it : @test.Test) { - let complex_doc = @vg.new_svg(300.0, 200.0) + let complex_doc = @svg.new_svg(300.0, 200.0) .render_rectangle(10.0, 10.0, 280.0, 180.0, @color.gray(0.95)) // Background .render_circle(@vg.Point::new(80.0, 60.0), 30.0, @color.red()) // Red circle .render_ellipse(@vg.Point::new(150.0, 60.0), 25.0, 15.0, @color.blue()) // Blue ellipse @@ -211,7 +211,7 @@ test "comprehensive svg rendering" (it : @test.Test) { ///| test "svg string generation" (it : @test.Test) { - let doc = @vg.new_svg(100.0, 100.0).render_circle( + let doc = @svg.new_svg(100.0, 100.0).render_circle( @vg.Point::new(50.0, 50.0), 25.0, @color.red(), @@ -242,7 +242,7 @@ test "path rendering to svg" (it : @test.Test) { ) .line_to(@vg.Point::new(10.0, 30.0)) .close_path() - let doc = @vg.new_svg(120.0, 50.0).render_path(custom_path, @color.magenta()) + let doc = @svg.new_svg(120.0, 50.0).render_path(custom_path, @color.magenta()) inspect( doc, content=( @@ -282,7 +282,7 @@ test "gradient showcase" (it : @test.Test) { ///| test "shape gallery" (it : @test.Test) { - let doc = @vg.new_svg(500.0, 400.0) + let doc = @svg.new_svg(500.0, 400.0) .render_rectangle(0.0, 0.0, 500.0, 400.0, @color.gray(0.98)) // Light background .render_text( "VG Shape Gallery", @@ -362,7 +362,7 @@ test "shape gallery" (it : @test.Test) { ///| test "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.95)) .render_text( "Path Construction Showcase", @@ -457,7 +457,7 @@ test "path showcase" (it : @test.Test) { ///| test "svg rendering validation" (it : @test.Test) { // Create a simple test to validate SVG structure - let doc = @vg.new_svg(200.0, 100.0) + let doc = @svg.new_svg(200.0, 100.0) .render_rectangle(10.0, 10.0, 180.0, 80.0, @color.gray(0.95)) .render_circle(@vg.Point::new(100.0, 50.0), 20.0, @color.red()) .render_text("SVG Test", @vg.Point::new(100.0, 30.0), 14.0, @color.black()) @@ -489,7 +489,7 @@ test "svg rendering validation" (it : @test.Test) { ///| test "minimal svg rendering test" (it : @test.Test) { // Create the simplest possible SVG to test basic rendering - 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.white()) // White background .render_circle(@vg.Point::new(150.0, 100.0), 50.0, @color.red()) // Red circle in center .render_rectangle(50.0, 50.0, 200.0, 100.0, @color.rgba(0.0, 0.0, 1.0, 0.5)) // Semi-transparent blue rect @@ -507,7 +507,7 @@ test "minimal svg rendering test" (it : @test.Test) { ///| test "simple rendering test" (it : @test.Test) { // Ultra-simple SVG that should definitely render - let doc = @vg.new_svg(100.0, 100.0).render_circle( + let doc = @svg.new_svg(100.0, 100.0).render_circle( @vg.Point::new(50.0, 50.0), 30.0, @color.red(), @@ -520,7 +520,7 @@ test "simple rendering test" (it : @test.Test) { ///| test "html wrapped svg test" (it : @test.Test) { // Create an HTML file with embedded SVG for testing in browsers - 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.white()) .render_circle(@vg.Point::new(200.0, 150.0), 50.0, @color.red()) .render_rectangle( diff --git a/types.mbt b/types.mbt index ce807c8..d26a283 100644 --- a/types.mbt +++ b/types.mbt @@ -22,6 +22,11 @@ pub type Transform = @geometry.Transform ///| pub type Color = @color.Color +// Re-export svg type + +///| +pub type SvgDocument = @svg.SvgDocument + ///| /// An image is a function from points to colors pub(all) struct Image((Point) -> Color)