Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LP-1040 and LP-1041 #1052

Merged
merged 9 commits into from
Mar 21, 2024
Prev Previous commit
Next Next commit
Refactor GeomHelper - dedup geometry padding and arrows support
  • Loading branch information
IKupriyanov-HORIS committed Mar 19, 2024
commit d7721cb267aa9d49d62c5ce8573b8ff182eb8d38
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright (c) 2024. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package org.jetbrains.letsPlot.commons.geometry

import org.jetbrains.letsPlot.commons.intern.math.lineSlope
import org.jetbrains.letsPlot.commons.intern.math.toRadians
import kotlin.math.*


// https://svn.r-project.org/R/trunk/src/library/grid/R/curve.R
fun curve(
start: DoubleVector,
end: DoubleVector,
curvature: Double,
angle: Double,
ncp: Int
): List<DoubleVector> {
// straight line
if (curvature == 0.0 || abs(angle) !in 1.0..179.0) {
return emptyList()
}

val mid = start.add(end).mul(0.5)
val d = end.subtract(start)

val rAngle = toRadians(angle)
val corner = mid.add(
start.subtract(mid).rotate(rAngle)
)

// Calculate angle to rotate region by to align it with x/y axes
val beta = -atan(lineSlope(start, corner))

// Rotate end point about start point to align region with x/y axes
val new = start.add(
d.rotate(beta)
)

// Calculate x-scale factor to make region "square"
val scaleX = lineSlope(start, new)

// Calculate the origin in the "square" region
// (for rotating start point to produce control points)
// (depends on 'curvature')
// 'origin' calculated from 'curvature'
val ratio = 2 * (sin(atan(curvature)).pow(2))
val origin = curvature - curvature / ratio

val ps = DoubleVector(start.x * scaleX, start.y)
val oxy = calcOrigin(
ps = ps,
pe = DoubleVector(new.x * scaleX, new.y),
origin
)

// Direction of rotation
val dir = sign(curvature)

// Angle of rotation depends on location of origin
val maxTheta = PI + sign(origin * dir) * 2 * atan(abs(origin))

val theta = (0 until (ncp + 2))
.map { it * dir * maxTheta / (ncp + 1) }
.drop(1)
.dropLast(1)

// May have BOTH multiple end points AND multiple
// control points to generate (per set of end points)
// Generate consecutive sets of control points by performing
// matrix multiplication

val indices = List(theta.size) { index -> index }

val p = ps.subtract(oxy)
val cp = indices.map { index ->
oxy.add(
p.rotate(theta[index])
)
}
// Reverse transformations (scaling and rotation) to
// produce control points in the original space
.map {
DoubleVector(
it.x / scaleX,
it.y
)
}

val controlPoints = indices.map { index ->
start.add(
cp[index].subtract(start).rotate(-beta)
)
}
return listOf(start) + controlPoints + listOf(end)
}

private fun calcOrigin(
ps: DoubleVector,
pe: DoubleVector,
origin: Double
): DoubleVector {

val mid = ps.add(pe).mul(0.5)
val d = pe.subtract(ps)
val slope = lineSlope(ps, pe)

val oSlope = -1 / slope

// The origin is a point somewhere along the line between
// the end points, rotated by 90 (or -90) degrees
// Two special cases:
// If slope is non-finite then the end points lie on a vertical line, so
// the origin lies along a horizontal line (oSlope = 0)
// If oSlope is non-finite then the end points lie on a horizontal line,
// so the origin lies along a vertical line (oSlope = Inf)
val tmpOX = when {
!slope.isFinite() -> 0.0
!oSlope.isFinite() -> origin * d.x / 2
else -> origin * d.x / 2
}

val tmpOY = when {
!slope.isFinite() -> origin * d.y / 2
!oSlope.isFinite() -> 0.0
else -> origin * d.y / 2
}

return DoubleVector(mid.x + tmpOY, mid.y - tmpOX)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2024. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package org.jetbrains.letsPlot.commons.geometry

import org.jetbrains.letsPlot.commons.intern.math.distance
import org.jetbrains.letsPlot.commons.intern.math.distance2
import org.jetbrains.letsPlot.commons.intern.math.pointOnLine

private fun pad(lineString: List<DoubleVector>, padding: Double): Pair<Int, DoubleVector>? {
if (lineString.size < 2) {
return null
}

val padding2 = padding * padding
val indexOutsidePadding = lineString.indexOfFirst { distance2(lineString.first(), it) >= padding2 }
if (indexOutsidePadding < 1) { // not found or first points already satisfy the padding
return null
}

val adjustedStartPoint = run {
val insidePadding = lineString[indexOutsidePadding - 1]
val outsidePadding = lineString[indexOutsidePadding]
val overPadding = distance(lineString.first(), outsidePadding) - padding

pointOnLine(outsidePadding, insidePadding, overPadding)
}

return indexOutsidePadding to adjustedStartPoint
}

private fun padStart(lineString: List<DoubleVector>, padding: Double): List<DoubleVector> {
val (index, adjustedStartPoint) = pad(lineString, padding) ?: return lineString
return listOf(adjustedStartPoint) + lineString.subList(index, lineString.size)
}

private fun padEnd(lineString: List<DoubleVector>, padding: Double): List<DoubleVector> {
val (index, adjustedEndPoint) = pad(lineString.asReversed(), padding) ?: return lineString
return lineString.subList(0, lineString.size - index) + adjustedEndPoint
}

fun padLineString(
lineString: List<DoubleVector>,
startPadding: Double,
endPadding: Double
): List<DoubleVector> {
val startPadded = padStart(lineString, startPadding)
return padEnd(startPadded, endPadding)
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,20 @@ object AesScaling {
return p.size()!! * 2
}

fun targetSize(p: DataPointAesthetics, atStart: Boolean): Double {
private fun targetSize(p: DataPointAesthetics, atStart: Boolean): Double {
// px -> aes Units
val sizeAes = if (atStart) DataPointAesthetics::sizeStart else DataPointAesthetics::sizeEnd
val strokeAes = if (atStart) DataPointAesthetics::strokeStart else DataPointAesthetics::strokeEnd
return circleDiameter(p, sizeAes) / 2 + pointStrokeWidth(p, strokeAes)
}

fun targetStartSize(p: DataPointAesthetics): Double {
// px -> aes Units
return targetSize(p, true)
}

fun targetEndSize(p: DataPointAesthetics): Double {
// px -> aes Units
return targetSize(p, false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ class ABLineGeom : GeomBase() {

if (lineEnds.size == 2) {
val it = lineEnds.iterator()
val line = helper.createLine(it.next(), it.next(), p)
if (line != null) {
lines.add(line)
}
val (svg) = helper.createLine(it.next(), it.next(), p) ?: continue
lines.add(svg)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ class BoxplotGeom : GeomBase() {
DoubleVector(x, hinge),
DoubleVector(x, fence),
p
)!!
)!!.first
)
// fence line
lines.add(
elementHelper.createLine(
DoubleVector(x - halfFenceWidth, fence),
DoubleVector(x + halfFenceWidth, fence),
p
)!!
)!!.first
)
}

Expand All @@ -97,15 +97,15 @@ class BoxplotGeom : GeomBase() {
DoubleVector(x, hinge),
DoubleVector(x, fence),
p
)!!
)!!.first
)
// fence line
lines.add(
elementHelper.createLine(
DoubleVector(x - halfFenceWidth, fence),
DoubleVector(x + halfFenceWidth, fence),
p
)!!
)!!.first
)

lines.forEach { root.add(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ class CrossBarGeom(
val middle = p[yAes]!!
val width = p[sizeAes]!! * ctx.getResolution(xAes)

val line = elementHelper.createLine(
val (line) = elementHelper.createLine(
afterRotation(DoubleVector(x - width / 2, middle)),
afterRotation(DoubleVector(x + width / 2, middle)),
p
)!!
) ?: continue

// TODO: use strokeScale in createLine() function
// adjust thickness
Expand Down
Loading