Skip to content

Commit

Permalink
Make initial command absolute before merging paths
Browse files Browse the repository at this point in the history
  • Loading branch information
jzbrooks committed Nov 30, 2024
1 parent 49eac60 commit 21135c7
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -73,7 +86,7 @@ class MergePaths(
if (unableToMerge(previous, current)) {
mergedPaths.add(current)
} else {
previous.commands += current.commands
previous.commands += makeFirstCommandAbsolute(current.commands)
}
}

Expand Down Expand Up @@ -106,7 +119,7 @@ class MergePaths(
mergedPaths.add(current)
pathLength = currentLength
} else {
previous.commands += current.commands
previous.commands += makeFirstCommandAbsolute(current.commands)
pathLength = accumulatedLength
}
}
Expand Down Expand Up @@ -138,6 +151,83 @@ class MergePaths(
first.strokeLineJoin == second.strokeLineJoin &&
first.strokeMiterLimit == second.strokeMiterLimit

private fun makeFirstCommandAbsolute(commands: List<Command>): List<Command> {
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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Path>()
.prop(Path::commands)
.index(1)
.isInstanceOf<ParameterizedCommand<*>>()
.all {
prop(ParameterizedCommand<*>::variant).isEqualTo(CommandVariant.ABSOLUTE)
prop(ParameterizedCommand<*>::variant.name) { it.parameters }
.isEqualTo(listOf(Point(10f, 10f), Point(20f, 20f)))
}
}
}

0 comments on commit 21135c7

Please sign in to comment.