Add SVG render for Scheme

This commit is contained in:
Alexander Nozik 2022-10-09 13:03:36 +03:00
parent c679680cf0
commit 28663a010d
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
9 changed files with 977 additions and 34 deletions

View File

@ -10,7 +10,7 @@ val ktorVersion by extra("2.0.3")
allprojects { allprojects {
group = "center.sciprog" group = "center.sciprog"
version = "0.1.0-dev-10" version = "0.1.0-dev-11"
} }
ksciencePublish{ ksciencePublish{

View File

@ -1,16 +1,22 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.maps.scheme.* import center.sciprog.maps.scheme.*
import center.sciprog.maps.svg.FeatureStateSnapshot
import center.sciprog.maps.svg.exportToSvg
import center.sciprog.maps.svg.snapshot
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.awt.Desktop
import java.nio.file.Files
import kotlin.math.PI import kotlin.math.PI
@Composable @Composable
@ -19,20 +25,13 @@ fun App() {
MaterialTheme { MaterialTheme {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val schemeFeaturesState = SchemeFeaturesState.remember {
SchemeView(
config = SchemeViewConfig(
onClick = {
println("${focus.x}, ${focus.y}")
}
)
) {
background(1600f, 1200f) { painterResource("middle-earth.jpg") } background(1600f, 1200f) { painterResource("middle-earth.jpg") }
circle(410.52737 to 868.7676, color = Color.Blue) circle(410.52737 to 868.7676, color = Color.Blue)
text(410.52737 to 868.7676, "Shire", color = Color.Blue) text(410.52737 to 868.7676, "Shire", color = Color.Blue)
circle(1132.0881 to 394.99127, color = Color.Red) circle(1132.0881 to 394.99127, color = Color.Red)
text(1132.0881 to 394.99127, "Ordruin", color = Color.Red) text(1132.0881 to 394.99127, "Ordruin", color = Color.Red)
arc(center = 1132.0881 to 394.99127, radius = 10f, startAngle = 0f, 2 * PI.toFloat()) arc(center = 1132.0881 to 394.99127, radius = 20f, startAngle = 0f, 2 * PI.toFloat())
val hobbitId = circle(410.52737 to 868.7676) val hobbitId = circle(410.52737 to 868.7676)
@ -47,13 +46,52 @@ fun App() {
if (t >= 1.0) t = 0.0 if (t >= 1.0) t = 0.0
} }
} }
} }
val initialViewPoint: SchemeViewPoint = remember {
schemeFeaturesState.features().values.computeBoundingBox(1f)?.computeViewPoint()
?: SchemeViewPoint(SchemeCoordinates(0f, 0f))
}
var viewPoint by remember { mutableStateOf<SchemeViewPoint>(initialViewPoint) }
var snapshot: FeatureStateSnapshot? by remember { mutableStateOf(null) }
if (snapshot == null) {
snapshot = schemeFeaturesState.snapshot()
}
ContextMenuArea(
items = {
listOf(
ContextMenuItem("Export to SVG") {
snapshot?.let {
val path = Files.createTempFile("scheme-kt-", ".svg")
it.exportToSvg(viewPoint, 800.0, 800.0, path)
println(path.toFile())
Desktop.getDesktop().browse(path.toFile().toURI())
}
},
)
}
) {
SchemeView(
initialViewPoint = initialViewPoint,
featuresState = schemeFeaturesState,
config = SchemeViewConfig(
onClick = {
println("${focus.x}, ${focus.y}")
},
onViewChange = { viewPoint = this }
),
)
}
} }
} }
fun main() = application { fun main() = application {
Window(onCloseRequest = ::exitApplication) { Window(title = "Scheme demo", onCloseRequest = ::exitApplication) {
App() App()
} }
} }

View File

@ -9,6 +9,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.input.pointer.* import androidx.compose.ui.input.pointer.*
@ -255,15 +256,17 @@ public actual fun MapView(
val topLeft = feature.oval.topLeft.toOffset() val topLeft = feature.oval.topLeft.toOffset()
val bottomRight = feature.oval.bottomRight.toOffset() val bottomRight = feature.oval.bottomRight.toOffset()
val path = Path().apply { val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
addArcRad(
Rect(topLeft, bottomRight),
feature.startAngle.radians.value.toFloat(),
feature.arcLength.radians.value.toFloat()
)
}
drawPath(path, color = feature.color, style = Stroke()) drawArc(
color = feature.color,
startAngle = feature.startAngle.degrees.toFloat(),
sweepAngle = feature.arcLength.degrees.toFloat(),
useCenter = false,
topLeft = topLeft,
size = size,
style = Stroke()
)
} }

View File

@ -89,3 +89,7 @@ public fun Angle.normalized(center: Angle = Angle.pi): Angle =
this - Angle.piTimes2 * floor((radians.value + PI - center.radians.value) / PI/2) this - Angle.piTimes2 * floor((radians.value + PI - center.radians.value) / PI/2)
public fun abs(angle: Angle): Angle = if (angle < Angle.zero) -angle else angle public fun abs(angle: Angle): Angle = if (angle < Angle.zero) -angle else angle
public fun Radians.toFloat(): Float = value.toFloat()
public fun Degrees.toFloat(): Float = value.toFloat()

View File

@ -23,6 +23,7 @@ kotlin {
} }
val jvmMain by getting { val jvmMain by getting {
dependencies { dependencies {
implementation("org.jfree:org.jfree.svg:5.0.3")
api(compose.desktop.currentOs) api(compose.desktop.currentOs)
} }
} }

View File

@ -9,6 +9,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.input.pointer.* import androidx.compose.ui.input.pointer.*
@ -18,11 +19,13 @@ import androidx.compose.ui.unit.dp
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.skia.Font import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint import org.jetbrains.skia.Paint
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
private fun Color.toPaint(): Paint = Paint().apply { internal fun Color.toSkiaPaint(): Paint = Paint().apply {
isAntiAlias = true isAntiAlias = true
color = toArgb() color = toArgb()
} }
@ -174,18 +177,19 @@ public fun SchemeView(
val topLeft = feature.oval.leftTop.toOffset() val topLeft = feature.oval.leftTop.toOffset()
val bottomRight = feature.oval.rightBottom.toOffset() val bottomRight = feature.oval.rightBottom.toOffset()
val path = Path().apply { val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
addArcRad(
Rect(topLeft, bottomRight), drawArc(
feature.startAngle, color = feature.color,
feature.arcLength startAngle = (feature.startAngle * 180 / PI).toFloat(),
sweepAngle = (feature.arcLength * 180 / PI).toFloat(),
useCenter = false,
topLeft = topLeft,
size = size,
style = Stroke()
) )
} }
drawPath(path, color = feature.color, style = Stroke())
}
is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset()) is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset())
is SchemeImageFeature -> { is SchemeImageFeature -> {
val offset = feature.position.toOffset() val offset = feature.position.toOffset()
@ -204,7 +208,7 @@ public fun SchemeView(
offset.x + 5, offset.x + 5,
offset.y - 5, offset.y - 5,
Font().apply { size = 16f }, Font().apply { size = 16f },
feature.color.toPaint() feature.color.toSkiaPaint()
) )
} }

View File

@ -0,0 +1,292 @@
package center.sciprog.maps.svg
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawContext
import androidx.compose.ui.graphics.drawscope.DrawTransform
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import java.awt.Graphics2D
internal fun Paint.toAwt(): java.awt.Paint {
return java.awt.Color(color.toArgb())
}
internal fun DrawContext.asDrawTransform(): DrawTransform = object : DrawTransform {
override val size: Size
get() = this@asDrawTransform.size
override val center: Offset
get() = size.center
override fun inset(left: Float, top: Float, right: Float, bottom: Float) {
this@asDrawTransform.canvas.let {
val updatedSize = Size(size.width - (left + right), size.height - (top + bottom))
require(updatedSize.width >= 0 && updatedSize.height >= 0) {
"Width and height must be greater than or equal to zero"
}
this@asDrawTransform.size = updatedSize
it.translate(left, top)
}
}
override fun clipRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
clipOp: ClipOp,
) {
this@asDrawTransform.canvas.clipRect(left, top, right, bottom, clipOp)
}
override fun clipPath(path: Path, clipOp: ClipOp) {
this@asDrawTransform.canvas.clipPath(path, clipOp)
}
override fun translate(left: Float, top: Float) {
this@asDrawTransform.canvas.translate(left, top)
}
override fun rotate(degrees: Float, pivot: Offset) {
this@asDrawTransform.canvas.apply {
translate(pivot.x, pivot.y)
rotate(degrees)
translate(-pivot.x, -pivot.y)
}
}
override fun scale(scaleX: Float, scaleY: Float, pivot: Offset) {
this@asDrawTransform.canvas.apply {
translate(pivot.x, pivot.y)
scale(scaleX, scaleY)
translate(-pivot.x, -pivot.y)
}
}
override fun transform(matrix: Matrix) {
this@asDrawTransform.canvas.concat(matrix)
}
}
internal class SvgCanvas(val graphics: Graphics2D) : Canvas {
override fun clipPath(path: Path, clipOp: ClipOp) {
TODO("Not yet implemented")
}
override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) {
if (clipOp == ClipOp.Intersect) {
graphics.clipRect(
left.toInt(),
top.toInt(),
(right - left).toInt(),
(top - bottom).toInt()
)
} else {
TODO()
}
}
override fun concat(matrix: Matrix) {
TODO()
// matrix.
// val affine = AffineTransform()
// graphics.transform()
}
override fun disableZ() {
//Do nothing
}
override fun drawArc(
left: Float,
top: Float,
right: Float,
bottom: Float,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
paint: Paint,
) {
graphics.paint = paint.toAwt()
graphics.drawArc(
top.toInt(),
left.toInt(),
(right - left).toInt(),
(top - bottom).toInt(),
startAngle.toInt(),
sweepAngle.toInt()
)
}
override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
graphics.paint = paint.toAwt()
graphics.drawOval(
(center.x - radius).toInt(),
(center.y - radius).toInt(),
(radius * 2).toInt(),
(radius * 2).toInt()
)
}
override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) {
graphics.paint = paint.toAwt()
graphics.drawImage(image.toAwtImage(), null, topLeftOffset.x.toInt(), topLeftOffset.y.toInt())
}
override fun drawImageRect(
image: ImageBitmap,
srcOffset: IntOffset,
srcSize: IntSize,
dstOffset: IntOffset,
dstSize: IntSize,
paint: Paint,
) {
TODO("Not yet implemented")
}
override fun drawLine(p1: Offset, p2: Offset, paint: Paint) {
graphics.paint = paint.toAwt()
graphics.drawLine(p1.x.toInt(), p1.y.toInt(), p2.x.toInt(), p2.y.toInt())
}
override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
graphics.paint = paint.toAwt()
graphics.drawOval(
left.toInt(),
top.toInt(),
(right - left).toInt(),
(top - bottom).toInt()
)
}
override fun drawPath(path: Path, paint: Paint) {
val skiaPath = path.asSkiaPath()
val points: List<Offset> = skiaPath.points.mapNotNull { it?.let { Offset(it.x, it.y) } }
drawPoints(PointMode.Lines, points, paint)
}
override fun drawPoints(pointMode: PointMode, points: List<Offset>, paint: Paint) {
graphics.paint = paint.toAwt()
val xs = IntArray(points.size) { points[it].x.toInt() }
val ys = IntArray(points.size) { points[it].y.toInt() }
when (pointMode) {
PointMode.Polygon -> {
graphics.drawPolygon(xs, ys, points.size)
}
PointMode.Lines -> {
graphics.drawPolyline(xs, ys, points.size)
}
PointMode.Points -> {
val diameter = paint.strokeWidth
if (paint.strokeCap == StrokeCap.Round) {
points.forEach { offset ->
graphics.fillOval(
(offset.x - diameter / 2).toInt(),
(offset.y - diameter / 2).toInt(),
diameter.toInt(),
diameter.toInt()
)
}
} else {
points.forEach { offset ->
graphics.fillRect(
(offset.x - diameter / 2).toInt(),
(offset.y - diameter / 2).toInt(),
diameter.toInt(),
diameter.toInt()
)
}
}
}
}
}
override fun drawRawPoints(pointMode: PointMode, points: FloatArray, paint: Paint) {
require(points.size % 2 == 0) { "The number of floats must be even" }
val offsets = ArrayList<Offset>(points.size / 2)
for (i in points.indices step 2) {
offsets.add(Offset(points[i], points[i + 1]))
}
drawPoints(pointMode, offsets, paint)
}
override fun drawRect(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
graphics.paint = paint.toAwt()
graphics.drawRect(
left.toInt(),
top.toInt(),
(right - left).toInt(),
(top - bottom).toInt()
)
}
override fun drawRoundRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
radiusX: Float,
radiusY: Float,
paint: Paint,
) {
graphics.paint = paint.toAwt()
graphics.drawRoundRect(
left.toInt(),
top.toInt(),
(right - left).toInt(),
(top - bottom).toInt(),
radiusX.toInt(),
radiusY.toInt()
)
}
override fun drawVertices(vertices: Vertices, blendMode: BlendMode, paint: Paint) {
TODO("Not yet implemented")
}
override fun enableZ() {
//do nothing
}
override fun restore() {
TODO("Not yet implemented")
}
override fun rotate(degrees: Float) {
graphics.rotate(degrees.toDouble())
}
override fun save() {
TODO("Not yet implemented")
}
override fun saveLayer(bounds: Rect, paint: Paint) {
TODO("Not yet implemented")
}
override fun scale(sx: Float, sy: Float) {
graphics.scale(sx.toDouble(), sy.toDouble())
}
override fun skew(sx: Float, sy: Float) {
//TODO is this correct?
graphics.shear(sx.toDouble(), sy.toDouble())
}
override fun translate(dx: Float, dy: Float) {
graphics.translate(dx.toDouble(), dy.toDouble())
}
}
internal class SvgDrawContext(val graphics: Graphics2D, override var size: Size) : DrawContext {
override val canvas: Canvas = SvgCanvas(graphics)
override val transform: DrawTransform = asDrawTransform()
}

View File

@ -0,0 +1,464 @@
package center.sciprog.maps.svg
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import java.awt.BasicStroke
import java.awt.Font
import java.awt.Graphics2D
import java.awt.geom.AffineTransform
import java.awt.image.AffineTransformOp
import java.awt.Color as AWTColor
private fun Color.toAWT(): java.awt.Color = AWTColor(toArgb())
private fun Brush.toAWT(): java.awt.Paint = TODO()
public class SvgDrawScope(public val graphics: Graphics2D, size: Size) : DrawScope {
override val layoutDirection: LayoutDirection
get() = LayoutDirection.Ltr
override val density: Float get() = 1f
override val fontScale: Float get() = 1f
override fun drawArc(
brush: Brush,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
topLeft: Offset,
size: Size,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = brush.toAWT()
when (style) {
Fill -> graphics.fillArc(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt(),
startAngle.toInt(),
sweepAngle.toInt()
)
is Stroke -> graphics.drawArc(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt(),
startAngle.toInt(),
sweepAngle.toInt()
)
}
}
override fun drawArc(
color: Color,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
topLeft: Offset,
size: Size,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = color.toAWT()
when (style) {
Fill -> graphics.fillArc(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt(),
startAngle.toInt(),
sweepAngle.toInt()
)
is Stroke -> graphics.drawArc(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt(),
startAngle.toInt(),
sweepAngle.toInt()
)
}
}
override fun drawCircle(
brush: Brush,
radius: Float,
center: Offset,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = brush.toAWT()
when (style) {
Fill -> graphics.fillOval(
(center.x - radius).toInt(),
(center.y - radius).toInt(),
(radius * 2).toInt(),
(radius * 2).toInt()
)
is Stroke -> graphics.drawOval(
(center.x - radius).toInt(),
(center.y - radius).toInt(),
(radius * 2).toInt(),
(radius * 2).toInt()
)
}
}
override fun drawCircle(
color: Color,
radius: Float,
center: Offset,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = color.toAWT()
when (style) {
Fill -> graphics.fillOval(
(center.x - radius).toInt(),
(center.y - radius).toInt(),
(radius * 2).toInt(),
(radius * 2).toInt()
)
is Stroke -> graphics.drawOval(
(center.x - radius).toInt(),
(center.y - radius).toInt(),
(radius * 2).toInt(),
(radius * 2).toInt()
)
}
}
override fun drawImage(
image: ImageBitmap,
topLeft: Offset,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.drawImage(image.toAwtImage(), null, topLeft.x.toInt(), topLeft.y.toInt())
}
override fun drawImage(
image: ImageBitmap,
srcOffset: IntOffset,
srcSize: IntSize,
dstOffset: IntOffset,
dstSize: IntSize,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
filterQuality: FilterQuality,
) {
val scale: AffineTransform = AffineTransform.getScaleInstance(
dstSize.width.toDouble() / srcSize.width,
dstSize.height.toDouble() / srcSize.height
)
val awtImage = image.toAwtImage().getSubimage(srcOffset.x, srcOffset.y, srcSize.width, srcSize.height)
val op = AffineTransformOp(scale, AffineTransformOp.TYPE_NEAREST_NEIGHBOR)
graphics.drawImage(awtImage, op, dstOffset.x, dstOffset.y)
}
override fun drawImage(
image: ImageBitmap,
srcOffset: IntOffset,
srcSize: IntSize,
dstOffset: IntOffset,
dstSize: IntSize,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
val scale: AffineTransform = AffineTransform.getScaleInstance(
dstSize.width.toDouble() / srcSize.width,
dstSize.height.toDouble() / srcSize.height
)
val awtImage = image.toAwtImage().getSubimage(srcOffset.x, srcOffset.y, srcSize.width, srcSize.height)
val op = AffineTransformOp(scale, AffineTransformOp.TYPE_NEAREST_NEIGHBOR)
graphics.drawImage(awtImage, op, dstOffset.x, dstOffset.y)
}
override fun drawLine(
brush: Brush,
start: Offset,
end: Offset,
strokeWidth: Float,
cap: StrokeCap,
pathEffect: PathEffect?,
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = brush.toAWT()
graphics.stroke = BasicStroke(strokeWidth)
graphics.drawLine(start.x.toInt(), start.y.toInt(), end.x.toInt(), end.y.toInt())
}
override fun drawLine(
color: Color,
start: Offset,
end: Offset,
strokeWidth: Float,
cap: StrokeCap,
pathEffect: PathEffect?,
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = color.toAWT()
graphics.stroke = BasicStroke(strokeWidth)
graphics.drawLine(start.x.toInt(), start.y.toInt(), end.x.toInt(), end.y.toInt())
}
override fun drawOval(
brush: Brush,
topLeft: Offset,
size: Size,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = brush.toAWT()
when (style) {
Fill -> graphics.fillOval(topLeft.x.toInt(), topLeft.y.toInt(), size.width.toInt(), size.height.toInt())
is Stroke -> graphics.drawOval(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt()
)
}
}
override fun drawOval(
color: Color,
topLeft: Offset,
size: Size,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = color.toAWT()
when (style) {
Fill -> graphics.fillOval(topLeft.x.toInt(), topLeft.y.toInt(), size.width.toInt(), size.height.toInt())
is Stroke -> graphics.drawOval(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt()
)
}
}
override fun drawPath(
path: Path,
brush: Brush,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
val skiaPath = path.asSkiaPath()
val points = skiaPath.points.mapNotNull { it?.let { Offset(it.x, it.y) } }
drawPoints(points, PointMode.Lines, brush)
}
override fun drawPath(
path: Path,
color: Color,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
val skiaPath = path.asSkiaPath()
val points = skiaPath.points.mapNotNull { it?.let { Offset(it.x, it.y) } }
drawPoints(points, PointMode.Lines, color)
}
override fun drawPoints(
points: List<Offset>,
pointMode: PointMode,
brush: Brush,
strokeWidth: Float,
cap: StrokeCap,
pathEffect: PathEffect?,
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = brush.toAWT()
graphics.stroke = BasicStroke(strokeWidth)
val xs = IntArray(points.size) { points[it].x.toInt() }
val ys = IntArray(points.size) { points[it].y.toInt() }
graphics.drawPolyline(xs, ys, points.size)
}
override fun drawPoints(
points: List<Offset>,
pointMode: PointMode,
color: Color,
strokeWidth: Float,
cap: StrokeCap,
pathEffect: PathEffect?,
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = color.toAWT()
graphics.stroke = BasicStroke(strokeWidth)
val xs = IntArray(points.size) { points[it].x.toInt() }
val ys = IntArray(points.size) { points[it].y.toInt() }
graphics.drawPolyline(xs, ys, points.size)
}
override fun drawRect(
brush: Brush,
topLeft: Offset,
size: Size,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = brush.toAWT()
when (style) {
Fill -> graphics.fillRect(topLeft.x.toInt(), topLeft.y.toInt(), size.width.toInt(), size.height.toInt())
is Stroke -> graphics.drawRect(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt()
)
}
}
override fun drawRect(
color: Color,
topLeft: Offset,
size: Size,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = color.toAWT()
when (style) {
Fill -> graphics.fillRect(topLeft.x.toInt(), topLeft.y.toInt(), size.width.toInt(), size.height.toInt())
is Stroke -> graphics.drawRect(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt()
)
}
}
override fun drawRoundRect(
brush: Brush,
topLeft: Offset,
size: Size,
cornerRadius: CornerRadius,
alpha: Float,
style: DrawStyle,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = brush.toAWT()
when (style) {
Fill -> graphics.fillRoundRect(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt(),
cornerRadius.x.toInt(),
cornerRadius.y.toInt()
)
is Stroke -> graphics.drawRoundRect(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt(),
cornerRadius.x.toInt(),
cornerRadius.y.toInt()
)
}
}
override fun drawRoundRect(
color: Color,
topLeft: Offset,
size: Size,
cornerRadius: CornerRadius,
style: DrawStyle,
alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode,
) {
graphics.paint = color.toAWT()
when (style) {
Fill -> graphics.fillRoundRect(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt(),
cornerRadius.x.toInt(),
cornerRadius.y.toInt()
)
is Stroke -> graphics.drawRoundRect(
topLeft.x.toInt(),
topLeft.y.toInt(),
size.width.toInt(),
size.height.toInt(),
cornerRadius.x.toInt(),
cornerRadius.y.toInt()
)
}
}
fun drawText(
text: String,
x: Float,
y: Float,
font: Font,
color: Color,
) {
graphics.paint = color.toAWT()
graphics.font = font
graphics.drawString(text, x, y)
}
override val drawContext: DrawContext = SvgDrawContext(graphics, size)
}

View File

@ -0,0 +1,137 @@
package center.sciprog.maps.svg
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.painter.Painter
import center.sciprog.maps.scheme.*
import org.jfree.svg.SVGGraphics2D
import org.jfree.svg.SVGUtils
import java.awt.Font.PLAIN
import kotlin.math.PI
import kotlin.math.abs
class FeatureStateSnapshot(
val features: Map<FeatureId, SchemeFeature>,
val painterCache: Map<PainterFeature, Painter>,
)
@Composable
fun SchemeFeaturesState.snapshot(): FeatureStateSnapshot =
FeatureStateSnapshot(
features(),
features().values.filterIsInstance<PainterFeature>().associateWith { it.painter() })
fun FeatureStateSnapshot.exportToSvg(
viewPoint: SchemeViewPoint,
width: Double,
height: Double,
path: java.nio.file.Path,
) {
fun SchemeCoordinates.toOffset(): Offset = Offset(
(width / 2 + (x - viewPoint.focus.x) * viewPoint.scale).toFloat(),
(height / 2 + (viewPoint.focus.y - y) * viewPoint.scale).toFloat()
)
fun SvgDrawScope.drawFeature(scale: Float, feature: SchemeFeature) {
when (feature) {
is SchemeBackgroundFeature -> {
val offset = SchemeCoordinates(feature.rectangle.left, feature.rectangle.top).toOffset()
val backgroundSize = Size(
(feature.rectangle.width * scale),
(feature.rectangle.height * scale)
)
translate(offset.x, offset.y) {
with(painterCache[feature]!!) {
draw(backgroundSize)
}
}
}
is SchemeFeatureSelector -> drawFeature(scale, feature.selector(scale))
is SchemeCircleFeature -> drawCircle(
feature.color,
feature.size,
center = feature.center.toOffset()
)
is SchemeLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
is SchemeArcFeature -> {
val topLeft = feature.oval.leftTop.toOffset()
val bottomRight = feature.oval.rightBottom.toOffset()
val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
drawArc(
color = feature.color,
startAngle = (feature.startAngle * 180 / PI).toFloat(),
sweepAngle = (feature.arcLength * 180 / PI).toFloat(),
useCenter = false,
topLeft = topLeft,
size = size,
style = Stroke()
)
}
is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset())
is SchemeImageFeature -> {
val offset = feature.position.toOffset()
val imageSize = feature.size.toSize()
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
with(painterCache[feature]!!) {
draw(imageSize)
}
}
}
is SchemeTextFeature -> drawIntoCanvas { canvas ->
val offset = feature.position.toOffset()
drawText(
feature.text,
offset.x + 5,
offset.y - 5,
java.awt.Font(null, PLAIN, 16),
feature.color
)
}
is SchemeDrawFeature -> {
val offset = feature.position.toOffset()
translate(offset.x, offset.y) {
feature.drawFeature(this)
}
}
is SchemeFeatureGroup -> {
feature.children.values.forEach {
drawFeature(scale, it)
}
}
}
}
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(width, height)
val svgScope = SvgDrawScope(svgGraphics2D, Size(width.toFloat(), height.toFloat()))
svgScope.apply {
features.values.filterIsInstance<SchemeBackgroundFeature>().forEach { background ->
drawFeature(viewPoint.scale, background)
}
features.values.filter {
it !is SchemeBackgroundFeature && viewPoint.scale in it.scaleRange
}.forEach { feature ->
drawFeature(viewPoint.scale, feature)
}
}
SVGUtils.writeToSVG(path.toFile(), svgGraphics2D.svgElement)
}