[orx-shapes] Refactor package layout

This commit is contained in:
Edwin Jakobs
2024-01-23 09:38:43 +01:00
parent 78ba51ca85
commit 8fbb106823
54 changed files with 138 additions and 91 deletions

View File

@@ -0,0 +1,33 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.math.*
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.contour
/**
* A circular arc
*/
class Arc(val center: Vector2, val radius: Double, val angle0: Double, val angle1: Double) : LinearType<Arc> {
fun position(t: Double): Vector2 {
val angle = mix(angle0, angle1, t.clamp(0.0, 1.0))
return Polar(angle, radius).cartesian + center
}
val contour: ShapeContour
get() {
return contour {
moveTo(position(0.0))
circularArcTo(position(0.5), position(1.0))
}
}
override fun div(scale: Double) = Arc(center / scale, radius / scale, angle0 / scale, angle1 / scale)
override fun times(scale: Double) = Arc(center * scale, radius * scale, angle0 * scale, angle1 * scale)
override fun plus(right: Arc) =
Arc(center + right.center, radius + right.radius, angle0 + right.angle0, angle1 + right.angle1)
override fun minus(right: Arc) =
Arc(center - right.center, radius - right.radius, angle0 - right.angle0, angle1 - right.angle1)
}

View File

@@ -0,0 +1,38 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.math.transforms.buildTransform
import org.openrndr.shape.Circle
import org.openrndr.shape.Ellipse
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.contour as buildContour
/**
* Convert [Circle] to [ShapeContour] with a number of segments equal to [segments]
* @param segments the number of segments, at least 4
*/
fun Circle.contour(segments: Int): ShapeContour {
return buildContour {
val p = Polar(0.0, radius)
moveTo(center + p.cartesian)
for (i in 1 until segments+1) {
val lp = Polar(i * 360.0/segments, radius).cartesian + center
arcTo(radius, radius, 360.0/segments, false, true, lp.x, lp.y)
}
close()
}
}
/**
* Convert [Ellipse] to [ShapeContour] with a number of segments equal to [segments]
* @param segments the number of segments, at least 4
*/
fun Ellipse.contour(segments: Int): ShapeContour {
return Circle(Vector2.ZERO, xRadius).contour(segments).transform(
buildTransform {
translate(center)
scale(1.0, yRadius/xRadius)
}
)
}

View File

@@ -0,0 +1,67 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.math.LinearType
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
import org.openrndr.shape.LineSegment
import org.openrndr.shape.ShapeContour
class Net(val point0: Vector2, val point1: Vector2, val circle: Circle) :
LinearType<Net> {
override fun div(scale: Double) =
Net(point0 / scale, point1 / scale, circle / scale)
override fun times(scale: Double) =
Net(point0 * scale, point1 * scale, circle * scale)
override fun plus(right: Net) =
Net(point0 + right.point0, point1 + right.point1, circle + right.circle)
override fun minus(right: Net) =
Net(point0 - right.point0, point1 - right.point1, circle - right.circle)
/**
* Creates a [ShapeContour] with three segments: two [LineSegment] and one [Arc].
* These three components form a contour that resemble a string starting
* at [point0], wrapping around the [circle] and ending at [point1].
* If one of the points is inside the circle only a line segment tangent
* to the circle that starts at the other point is returned. If both
* points are inside the circle an empty contour is returned.
*/
val contour: ShapeContour
get() {
val p0Inside = circle.contains(point0)
val p1Inside = circle.contains(point1)
return when {
!p0Inside && !p1Inside -> {
val tangents0 = circle.tangents(point0)
val tangents1 = circle.tangents(point1)
val th0 =
Polar.fromVector(tangents0.first - circle.center).theta
val th1 =
Polar.fromVector(tangents1.second - circle.center).theta
LineSegment(point0, tangents0.first).contour +
Arc(
circle.center,
circle.radius,
th0,
if (th1 < th0) th1 + 360.0 else th1
).contour +
LineSegment(tangents1.second, point1).contour
}
p0Inside ->
LineSegment(circle.tangents(point1).second, point1).contour
p1Inside ->
LineSegment(circle.tangents(point0).first, point0).contour
else ->
ShapeContour.EMPTY
}
}
}

View File

@@ -0,0 +1,50 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.math.LinearType
import org.openrndr.math.Polar
import org.openrndr.shape.Circle
import org.openrndr.shape.LineSegment
import org.openrndr.shape.ShapeContour
class Pulley(val circle0: Circle, val circle1: Circle) : LinearType<Pulley> {
override fun div(scale: Double): Pulley {
return Pulley(circle0 / scale, circle1 / scale)
}
override fun times(scale: Double): Pulley {
return Pulley(circle0 * scale, circle1 * scale)
}
override fun plus(right: Pulley): Pulley {
return Pulley(circle0 + right.circle0, circle1 + right.circle1)
}
override fun minus(right: Pulley): Pulley {
return Pulley(circle0 - right.circle0, circle1 - right.circle1)
}
val contour: ShapeContour
get() {
val tangents = circle0.tangents(circle1)
if (tangents.isEmpty()) {
return ShapeContour.EMPTY
} else {
var k = LineSegment(tangents[0].first, tangents[0].second).contour
run {
var th0 = Polar.fromVector(tangents[0].second - circle1.center).theta
val th1 = Polar.fromVector(tangents[1].second - circle1.center).theta
if (th0 < th1) th0 += 360.0
k += Arc(circle1.center, circle1.radius, th0, th1).contour
}
k += LineSegment(tangents[1].first, tangents[1].second).contour.reversed
run {
val th0 = Polar.fromVector(tangents[0].first - circle0.center).theta
var th1 = Polar.fromVector(tangents[1].first - circle0.center).theta
if (th0 > th1) th1 += 360.0
k += Arc(circle0.center, circle0.radius, th0, th1).contour.reversed
}
return k.close()
}
}
}

View File

@@ -0,0 +1,80 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.shape.Rectangle
import kotlin.math.round
/**
* Splits [Rectangle] into a grid of [Rectangle]s
* @param columns the number of columns in the resulting grid
* @param rows the number of rows in the resulting grid
* @param marginX the unitless margin width
* @param marginY the unitless margin height
* @param gutterX the unitless gutter width, the horizontal space between grid cells
* @param gutterY the unitless gutter height, the vertical space between grid cells
*/
fun Rectangle.grid(
columns: Int,
rows: Int,
marginX: Double = 0.0,
marginY: Double = 0.0,
gutterX: Double = 0.0,
gutterY: Double = 0.0
) = grid(
(width - marginX * 2 - gutterX * (columns - 1)) / columns,
(height - marginY * 2 - gutterY * (rows - 1)) / rows,
marginX, marginY,
gutterX, gutterY
)
/**
* Splits [Rectangle] into a grid of [Rectangle]s
* @param cellWidth the unitless width of a cell
* @param cellHeight the unitless height of a cell
* @param minMarginX the unitless minimum margin width (may increase to produce
* the desired cell aspect ratio)
* @param minMarginY the unitless minimum margin height (may increase to produce
* the desired cell aspect ratio)
* @param gutterX the unitless gutter width, the horizontal space between grid cells
* @param gutterY the unitless gutter height, the vertical space between grid cells
*/
fun Rectangle.grid(
cellWidth: Double,
cellHeight: Double,
minMarginX: Double = 0.0,
minMarginY: Double = 0.0,
gutterX: Double = 0.0,
gutterY: Double = 0.0
): List<List<Rectangle>> {
val availableWidth = (width - minMarginX * 2).coerceAtLeast(0.0)
val availableHeight = (height - minMarginY * 2).coerceAtLeast(0.0)
val cellSpaceX = cellWidth + gutterX
val cellSpaceY = cellHeight + gutterY
val columns = round((availableWidth + gutterX) / cellSpaceX).toInt()
val rows = round((availableHeight + gutterY) / cellSpaceY).toInt()
if (columns == 0 || rows == 0) {
return emptyList()
}
val totalGutterWidth = gutterX * (columns - 1).coerceAtLeast(0)
val totalGutterHeight = gutterY * (rows - 1).coerceAtLeast(0)
val totalWidth = cellWidth * columns + totalGutterWidth
val totalHeight = cellHeight * rows + totalGutterHeight
val x0 = x + (width - totalWidth) / 2
val y0 = y + (height - totalHeight) / 2
return (0 until rows).map { row ->
(0 until columns).map { column ->
Rectangle(
x0 + column * cellSpaceX,
y0 + row * cellSpaceY,
cellWidth, cellHeight
)
}
}
}

View File

@@ -0,0 +1,115 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.math.Vector2
import org.openrndr.math.asRadians
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.contour
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
/**
* Creates a regular polygon at [center] with the given [sides] and [radius].
* Specify a [phase] in degrees to rotate it.
*/
fun regularPolygon(sides: Int, center: Vector2 = Vector2.ZERO, radius: Double = 100.0, phase: Double = 0.0): ShapeContour {
val c = contour {
val phi = phase.asRadians
for (i in 0 until sides) {
val x = center.x + radius * cos(i.toDouble() / sides * PI * 2 + phi)
val y = center.y + radius * sin(i.toDouble() / sides * PI * 2 + phi)
moveOrLineTo(x, y)
}
close()
}
return c
}
/**
* Creates a rounded polygon at [center] with the given [sides] and [radius].
* Specify a [phase] in degrees to rotate it.
* [roundFactor] 0.0 = no rounding, 0.5 = default, 1.0 = full rounding.
*/
fun regularPolygonRounded(sides: Int, roundFactor: Double = 0.5, center: Vector2 = Vector2.ZERO, radius: Double = 100.0, phase: Double = 0.0): ShapeContour {
val c = contour {
val phi = phase.asRadians
for (i in 0 until sides) {
val x0 = center.x + radius * cos(i.toDouble() / sides * PI * 2 + phi)
val y0 = center.y + radius * sin(i.toDouble() / sides * PI * 2 + phi)
val x1 = center.x + radius * cos((i + 1.0) / sides * PI * 2 + phi)
val y1 = center.y + radius * sin((i + 1.0) / sides * PI * 2 + phi)
val x2 = center.x + radius * cos((i + 2.0) / sides * PI * 2 + phi)
val y2 = center.y + radius * sin((i + 2.0) / sides * PI * 2 + phi)
val f = roundFactor / 2.0
val dx10 = x1 - x0
val dy10 = y1 - y0
val dx21 = x2 - x1
val dy21 = y2 - y1
val x3 = x0 + dx10 * f
val y3 = y0 + dy10 * f
val x4 = x1 - dx10 * f
val y4 = y1 - dy10 * f
val x5 = x1 + dx21 * f
val y5 = y1 + dy21 * f
moveOrLineTo(x3, y3)
lineTo(x4, y4)
curveTo(x1, y1, x5, y5)
}
close()
}
return c
}
/**
* Creates a beveled polygon at [center] with the given [sides] and [radius].
* Specify a [phase] in degrees to rotate it.
* If 0.0 < [bevelFactor] < 1.0 the number of [sides] is doubled.
* Using 0.5 all sides have equal length. With other values [bevelFactor]
* determines the length ratio between even and odd sides.
*/
fun regularPolygonBeveled(sides: Int, bevelFactor: Double = 0.5, center: Vector2 = Vector2.ZERO, radius: Double = 100.0, phase: Double = 0.0): ShapeContour {
val c = contour {
val phi = phase.asRadians
for (i in 0 until sides) {
val x0 = center.x + radius * cos(i.toDouble() / sides * PI * 2 + phi)
val y0 = center.y + radius * sin(i.toDouble() / sides * PI * 2 + phi)
val x1 = center.x + radius * cos((i + 1.0) / sides * PI * 2 + phi)
val y1 = center.y + radius * sin((i + 1.0) / sides * PI * 2 + phi)
val x2 = center.x + radius * cos((i + 2.0) / sides * PI * 2 + phi)
val y2 = center.y + radius * sin((i + 2.0) / sides * PI * 2 + phi)
val f = bevelFactor / 2.0
val dx10 = x1 - x0
val dy10 = y1 - y0
val dx21 = x2 - x1
val dy21 = y2 - y1
val x3 = x0 + dx10 * f
val y3 = y0 + dy10 * f
val x4 = x1 - dx10 * f
val y4 = y1 - dy10 * f
val x5 = x1 + dx21 * f
val y5 = y1 + dy21 * f
moveOrLineTo(x3, y3)
lineTo(x4, y4)
lineTo(x5, y5)
}
close()
}
return c
}

View File

@@ -0,0 +1,83 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.math.Vector2
import org.openrndr.math.asRadians
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.contour
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
fun regularStar(points: Int, innerRadius: Double, outerRadius: Double, center: Vector2 = Vector2.ZERO, phase: Double = 0.0): ShapeContour {
return contour {
val theta = phase.asRadians
val phi = PI * 2.0 / (points * 2)
for (i in 0 until points * 2 step 2) {
val outerPoint = Vector2(cos(i * phi + theta), sin(i * phi + theta)) * outerRadius + center
val innerPoint = Vector2(cos((i + 1) * phi + theta), sin((i + 1) * phi + theta)) * innerRadius + center
moveOrLineTo(outerPoint)
lineTo(innerPoint)
}
close()
}
}
fun regularStarRounded(points: Int, innerRadius: Double, outerRadius: Double,
innerFactor: Double, outerFactor: Double,
center: Vector2 = Vector2.ZERO,
phase: Double = 0.0): ShapeContour {
return contour {
val theta = phase.asRadians
val phi = PI * 2.0 / (points * 2)
for (i in 0 until points * 2 step 2) {
val outerPoint0 = Vector2(cos(i * phi + theta), sin(i * phi + theta)) * outerRadius + center
val innerPoint = Vector2(cos((i + 1) * phi + theta), sin((i + 1) * phi + theta)) * innerRadius + center
val outerPoint1 = Vector2(cos((i + 2) * phi + theta), sin((i + 2) * phi + theta)) * outerRadius + center
val innerPoint1 = Vector2(cos((i + 3) * phi + theta), sin((i + 3) * phi + theta)) * innerRadius + center
val fo = (outerFactor * 0.5)
val fi = (innerFactor * 0.5)
val p0 = innerPoint - (innerPoint - outerPoint0) * fi
val p1 = innerPoint + (outerPoint1 - innerPoint) * fi
val p2 = outerPoint1 - (outerPoint1 - innerPoint) * fo
val p3 = outerPoint1 + (innerPoint1 - outerPoint1) * fo
moveOrLineTo(p0)
curveTo(innerPoint, p1)
lineTo(p2)
curveTo(outerPoint1, p3)
}
close()
}
}
fun regularStarBeveled(points: Int, innerRadius: Double, outerRadius: Double,
innerFactor: Double, outerFactor: Double,
center: Vector2 = Vector2.ZERO,
phase: Double = 0.0): ShapeContour {
return contour {
val theta = phase.asRadians
val phi = PI * 2.0 / (points * 2)
for (i in 0 until points * 2 step 2) {
val outerPoint0 = Vector2(cos(i * phi + theta), sin(i * phi + theta)) * outerRadius + center
val innerPoint = Vector2(cos((i + 1) * phi + theta), sin((i + 1) * phi + theta)) * innerRadius + center
val outerPoint1 = Vector2(cos((i + 2) * phi + theta), sin((i + 2) * phi + theta)) * outerRadius + center
val innerPoint1 = Vector2(cos((i + 3) * phi + theta), sin((i + 3) * phi + theta)) * innerRadius + center
val fo = (outerFactor * 0.5)
val fi = (innerFactor * 0.5)
val p0 = innerPoint - (innerPoint - outerPoint0) * fi
val p1 = innerPoint + (outerPoint1 - innerPoint) * fi
val p2 = outerPoint1 - (outerPoint1 - innerPoint) * fo
val p3 = outerPoint1 + (innerPoint1 - outerPoint1) * fo
moveOrLineTo(p0)
lineTo(p1)
lineTo(p2)
lineTo(p3)
}
close()
}
}

View File

@@ -0,0 +1,67 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.draw.Drawer
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import org.openrndr.shape.contour
import org.openrndr.shape.ShapeContour
import kotlin.math.min
class RoundedRectangle(val corner: Vector2, val width: Double, val height: Double, val radius: Double) {
constructor(x: Double, y: Double, width: Double, height: Double, radius: Double) : this(
Vector2(x, y),
width,
height,
radius
)
constructor(rectangle: Rectangle, radius: Double) : this(
rectangle.corner,
rectangle.width,
rectangle.height,
radius
)
/** the center of the rounded rectangle */
val center: Vector2
get() = corner + Vector2(width / 2, height / 2)
val x: Double get() = corner.x
val y: Double get() = corner.y
/** [ShapeContour] representation of the rounded rectangle */
val contour
get() = contour {
// A higher radius than half the width/height makes it go weird
val r = min(min(radius, width / 2), height / 2)
moveTo(x + r, y)
lineTo(x + width - r, y)
arcTo(r, r, 90.0, false, true, Vector2(x + width, y + r))
lineTo(x + width, y + height - r)
arcTo(r, r, 90.0, false, true, Vector2(x + width - r, y + height))
lineTo(x + r, y + height)
arcTo(r, r, 90.0, false, true, Vector2(x, y + height - r))
lineTo(x, y + r)
arcTo(r, r, 90.0, false, true, Vector2(x + r, y))
close()
}
val shape
get() = contour.shape
}
fun Drawer.roundedRectangle(x: Double, y: Double, width: Double, height: Double, radius: Double) =
contour(RoundedRectangle(x, y, width, height, radius).contour)
fun Drawer.roundedRectangle(position: Vector2, width: Double, height: Double, radius: Double) =
contour(RoundedRectangle(position, width, height, radius).contour)
fun Drawer.roundedRectangle(roundedRectangle: RoundedRectangle) =
contour(roundedRectangle.contour)
fun Rectangle.toRounded(radius: Double) = RoundedRectangle(this, radius)

View File

@@ -0,0 +1,32 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.math.LinearType
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
import org.openrndr.shape.LineSegment
import org.openrndr.shape.ShapeContour
class Tear(val point: Vector2, val circle: Circle) : LinearType<Tear> {
override fun div(scale: Double) = Tear(point / scale, circle / scale)
override fun times(scale: Double) = Tear(point * scale, circle * scale)
override fun plus(right: Tear) = Tear(point + right.point, circle + right.circle)
override fun minus(right: Tear) = Tear(point - right.point, circle - right.circle)
val contour: ShapeContour
get() {
val tangents = circle.tangents(point)
var k = LineSegment(point, tangents.first).contour
run {
val th0 = Polar.fromVector(tangents.first - circle.center).theta
var th1 = Polar.fromVector(tangents.second - circle.center).theta
if (th1 < th0) th1 += 360.0
k += Arc(circle.center, circle.radius, th0, th1).contour
}
k += LineSegment(tangents.second, point).contour
return k.close()
}
}