Add SVG render for Scheme
This commit is contained in:
parent
c679680cf0
commit
28663a010d
@ -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{
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,4 +88,8 @@ public val Number.degrees: Degrees get() = Degrees(toDouble())
|
|||||||
public fun Angle.normalized(center: Angle = Angle.pi): Angle =
|
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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,16 +177,17 @@ 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),
|
|
||||||
feature.startAngle,
|
|
||||||
feature.arcLength
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
drawPath(path, color = feature.color, style = Stroke())
|
|
||||||
|
|
||||||
|
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 SchemeBitmapFeature -> drawImage(feature.image, 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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -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)
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user