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 {
|
||||
group = "center.sciprog"
|
||||
version = "0.1.0-dev-10"
|
||||
version = "0.1.0-dev-11"
|
||||
}
|
||||
|
||||
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.
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
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.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.awt.Desktop
|
||||
import java.nio.file.Files
|
||||
import kotlin.math.PI
|
||||
|
||||
@Composable
|
||||
@ -19,20 +25,13 @@ fun App() {
|
||||
MaterialTheme {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
|
||||
SchemeView(
|
||||
config = SchemeViewConfig(
|
||||
onClick = {
|
||||
println("${focus.x}, ${focus.y}")
|
||||
}
|
||||
)
|
||||
) {
|
||||
val schemeFeaturesState = SchemeFeaturesState.remember {
|
||||
background(1600f, 1200f) { painterResource("middle-earth.jpg") }
|
||||
circle(410.52737 to 868.7676, color = Color.Blue)
|
||||
text(410.52737 to 868.7676, "Shire", color = Color.Blue)
|
||||
circle(1132.0881 to 394.99127, 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)
|
||||
|
||||
@ -47,13 +46,52 @@ fun App() {
|
||||
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 {
|
||||
Window(onCloseRequest = ::exitApplication) {
|
||||
Window(title = "Scheme demo", onCloseRequest = ::exitApplication) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.*
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
@ -255,15 +256,17 @@ public actual fun MapView(
|
||||
val topLeft = feature.oval.topLeft.toOffset()
|
||||
val bottomRight = feature.oval.bottomRight.toOffset()
|
||||
|
||||
val path = Path().apply {
|
||||
addArcRad(
|
||||
Rect(topLeft, bottomRight),
|
||||
feature.startAngle.radians.value.toFloat(),
|
||||
feature.arcLength.radians.value.toFloat()
|
||||
)
|
||||
}
|
||||
val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
|
||||
|
||||
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()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
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 {
|
||||
dependencies {
|
||||
implementation("org.jfree:org.jfree.svg:5.0.3")
|
||||
api(compose.desktop.currentOs)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.*
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
@ -18,11 +19,13 @@ import androidx.compose.ui.unit.dp
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.skia.Font
|
||||
import org.jetbrains.skia.Paint
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
private fun Color.toPaint(): Paint = Paint().apply {
|
||||
internal fun Color.toSkiaPaint(): Paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = toArgb()
|
||||
}
|
||||
@ -174,16 +177,17 @@ public fun SchemeView(
|
||||
val topLeft = feature.oval.leftTop.toOffset()
|
||||
val bottomRight = feature.oval.rightBottom.toOffset()
|
||||
|
||||
val path = Path().apply {
|
||||
addArcRad(
|
||||
Rect(topLeft, bottomRight),
|
||||
feature.startAngle,
|
||||
feature.arcLength
|
||||
)
|
||||
}
|
||||
|
||||
drawPath(path, color = feature.color, style = Stroke())
|
||||
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())
|
||||
@ -204,7 +208,7 @@ public fun SchemeView(
|
||||
offset.x + 5,
|
||||
offset.y - 5,
|
||||
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