From 21135c79eefc79c385a83db7c345cd5b1e7db5ca Mon Sep 17 00:00:00 2001 From: Justin Brooks Date: Fri, 29 Nov 2024 22:12:25 -0500 Subject: [PATCH] Make initial command absolute before merging paths --- changelog.md | 1 + .../vgo/core/optimization/MergePaths.kt | 94 ++++++++++++++++++- .../vgo/core/optimization/MergePathsTests.kt | 36 +++++++ 3 files changed, 129 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 6fe254f6..65d3e54d 100644 --- a/changelog.md +++ b/changelog.md @@ -25,6 +25,7 @@ - Decimal separators are locale-invariant. - Crash when using the CLI to convert an SVG containing a clip path to vector drawable. - (Vector Drawable) Path merging avoids merging a single path data string beyond the framework string length limit (#82) +- Paths with an initial relative command are modified to make that command absolute when merged (#111) ### Security diff --git a/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt b/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt index 2d2b1bdf..ebeebbbb 100644 --- a/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt +++ b/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt @@ -7,7 +7,20 @@ import com.jzbrooks.vgo.core.graphic.Extra import com.jzbrooks.vgo.core.graphic.Graphic import com.jzbrooks.vgo.core.graphic.Group import com.jzbrooks.vgo.core.graphic.Path +import com.jzbrooks.vgo.core.graphic.command.Command import com.jzbrooks.vgo.core.graphic.command.CommandPrinter +import com.jzbrooks.vgo.core.graphic.command.CommandVariant +import com.jzbrooks.vgo.core.graphic.command.CubicBezierCurve +import com.jzbrooks.vgo.core.graphic.command.EllipticalArcCurve +import com.jzbrooks.vgo.core.graphic.command.HorizontalLineTo +import com.jzbrooks.vgo.core.graphic.command.LineTo +import com.jzbrooks.vgo.core.graphic.command.MoveTo +import com.jzbrooks.vgo.core.graphic.command.ParameterizedCommand +import com.jzbrooks.vgo.core.graphic.command.QuadraticBezierCurve +import com.jzbrooks.vgo.core.graphic.command.SmoothCubicBezierCurve +import com.jzbrooks.vgo.core.graphic.command.SmoothQuadraticBezierCurve +import com.jzbrooks.vgo.core.graphic.command.VerticalLineTo +import com.jzbrooks.vgo.core.util.math.Point import com.jzbrooks.vgo.core.util.math.Surveyor import com.jzbrooks.vgo.core.util.math.intersects @@ -73,7 +86,7 @@ class MergePaths( if (unableToMerge(previous, current)) { mergedPaths.add(current) } else { - previous.commands += current.commands + previous.commands += makeFirstCommandAbsolute(current.commands) } } @@ -106,7 +119,7 @@ class MergePaths( mergedPaths.add(current) pathLength = currentLength } else { - previous.commands += current.commands + previous.commands += makeFirstCommandAbsolute(current.commands) pathLength = accumulatedLength } } @@ -138,6 +151,83 @@ class MergePaths( first.strokeLineJoin == second.strokeLineJoin && first.strokeMiterLimit == second.strokeMiterLimit + private fun makeFirstCommandAbsolute(commands: List): List { + val firstCommand = commands.firstOrNull() as? ParameterizedCommand<*> ?: return commands + + if (firstCommand.variant == CommandVariant.RELATIVE) { + var currentPoint = Point.ZERO + + when (firstCommand) { + is MoveTo, is LineTo, is SmoothQuadraticBezierCurve -> { + firstCommand.parameters = + firstCommand.parameters.map { point -> + (point + currentPoint).also { point -> currentPoint = point } + } + } + is HorizontalLineTo -> { + firstCommand.parameters = + firstCommand.parameters.map { x -> + (x + currentPoint.x).also { x -> currentPoint = currentPoint.copy(x = x) } + } + } + is VerticalLineTo -> { + firstCommand.parameters = + firstCommand.parameters.map { x -> + (x + currentPoint.x).also { x -> currentPoint = currentPoint.copy(x = x) } + } + } + is CubicBezierCurve -> { + firstCommand.parameters = + firstCommand.parameters.map { parameter -> + val newEnd = parameter.end + currentPoint + parameter + .copy( + startControl = parameter.startControl + currentPoint, + endControl = parameter.endControl + currentPoint, + end = newEnd, + ).also { currentPoint = newEnd } + } + } + is SmoothCubicBezierCurve -> { + firstCommand.parameters = + firstCommand.parameters.map { parameter -> + val newEnd = parameter.end + currentPoint + parameter + .copy( + endControl = parameter.endControl + currentPoint, + end = newEnd, + ).also { currentPoint = newEnd } + } + } + is QuadraticBezierCurve -> { + firstCommand.parameters = + firstCommand.parameters.map { parameter -> + val newEnd = parameter.end + currentPoint + parameter + .copy( + control = parameter.control + currentPoint, + end = newEnd, + ).also { currentPoint = newEnd } + } + } + is EllipticalArcCurve -> { + firstCommand.parameters = + firstCommand.parameters.map { parameter -> + val newEnd = parameter.end + currentPoint + parameter + .copy( + end = newEnd, + ).also { currentPoint = newEnd } + } + } + } + + firstCommand.variant = CommandVariant.ABSOLUTE + } + + return commands + } + sealed interface Constraints { /** Constraints the optimization by preventing merging paths beyond a given maximum length */ data class PathLength( diff --git a/vgo-core/src/test/kotlin/com/jzbrooks/vgo/core/optimization/MergePathsTests.kt b/vgo-core/src/test/kotlin/com/jzbrooks/vgo/core/optimization/MergePathsTests.kt index 43207a53..174eb90a 100644 --- a/vgo-core/src/test/kotlin/com/jzbrooks/vgo/core/optimization/MergePathsTests.kt +++ b/vgo-core/src/test/kotlin/com/jzbrooks/vgo/core/optimization/MergePathsTests.kt @@ -1,18 +1,24 @@ package com.jzbrooks.vgo.core.optimization +import assertk.all import assertk.assertThat import assertk.assertions.containsExactly +import assertk.assertions.first import assertk.assertions.hasSize import assertk.assertions.index import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop import com.jzbrooks.vgo.core.Color import com.jzbrooks.vgo.core.graphic.Group +import com.jzbrooks.vgo.core.graphic.Path import com.jzbrooks.vgo.core.graphic.command.Command import com.jzbrooks.vgo.core.graphic.command.CommandVariant import com.jzbrooks.vgo.core.graphic.command.EllipticalArcCurve import com.jzbrooks.vgo.core.graphic.command.FakeCommandPrinter import com.jzbrooks.vgo.core.graphic.command.LineTo import com.jzbrooks.vgo.core.graphic.command.MoveTo +import com.jzbrooks.vgo.core.graphic.command.ParameterizedCommand import com.jzbrooks.vgo.core.graphic.command.QuadraticBezierCurve import com.jzbrooks.vgo.core.graphic.command.SmoothCubicBezierCurve import com.jzbrooks.vgo.core.util.element.createGraphic @@ -455,4 +461,34 @@ class MergePathsTests { ), ) } + + @Test + fun mergedPathsInitialCommandIsMadeAbsolute() { + val paths = + listOf( + createPath( + listOf(MoveTo(CommandVariant.ABSOLUTE, listOf(Point(0f, 0f)))), + ), + createPath( + listOf(MoveTo(CommandVariant.RELATIVE, listOf(Point(10f, 10f), Point(10f, 10f)))), + ), + ) + + val graphic = createGraphic(paths) + val optimization = MergePaths(MergePaths.Constraints.None) + + traverseBottomUp(graphic) { it.accept(optimization) } + + assertThat(graphic::elements) + .first() + .isInstanceOf() + .prop(Path::commands) + .index(1) + .isInstanceOf>() + .all { + prop(ParameterizedCommand<*>::variant).isEqualTo(CommandVariant.ABSOLUTE) + prop(ParameterizedCommand<*>::variant.name) { it.parameters } + .isEqualTo(listOf(Point(10f, 10f), Point(20f, 20f))) + } + } }