[orx-composition] Add explicit contract checks for lambdas with callsInPlace

This commit is contained in:
Edwin Jakobs
2025-08-20 20:11:59 +02:00
parent aaefb9eaeb
commit 7541865e2c
2 changed files with 149 additions and 73 deletions

View File

@@ -12,6 +12,9 @@ import org.openrndr.math.Vector3
import org.openrndr.math.YPolarity import org.openrndr.math.YPolarity
import org.openrndr.math.transforms.* import org.openrndr.math.transforms.*
import org.openrndr.shape.* import org.openrndr.shape.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmRecord import kotlin.jvm.JvmRecord
/** /**
@@ -80,7 +83,12 @@ data class ShapeNodeIntersection(val node: ShapeNode, val intersection: ContourI
* in a [ShapeContour] closest to some other 2D point. * in a [ShapeContour] closest to some other 2D point.
*/ */
@JvmRecord @JvmRecord
data class ShapeNodeNearestContour(val node: ShapeNode, val point: ContourPoint, val distanceDirection: Vector2, val distance: Double) data class ShapeNodeNearestContour(
val node: ShapeNode,
val point: ContourPoint,
val distanceDirection: Vector2,
val distance: Double
)
/** /**
* Merges two lists of [ShapeNodeIntersection] removing duplicates under the * Merges two lists of [ShapeNodeIntersection] removing duplicates under the
@@ -104,9 +112,10 @@ fun List<ShapeNodeIntersection>.merge(threshold: Double = 0.5): List<ShapeNodeIn
* A Drawer-like interface for the creation of Compositions * A Drawer-like interface for the creation of Compositions
* This should be easier than creating Compositions manually * This should be easier than creating Compositions manually
*/ */
class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositionDimensions, class CompositionDrawer(
composition: Composition? = null, documentBounds: CompositionDimensions = defaultCompositionDimensions,
cursor: GroupNode? = composition?.root as? GroupNode composition: Composition? = null,
cursor: GroupNode? = composition?.root as? GroupNode
) { ) {
val root = (composition?.root as? GroupNode) ?: GroupNode() val root = (composition?.root as? GroupNode) ?: GroupNode()
val composition = composition ?: Composition(root, documentBounds) val composition = composition ?: Composition(root, documentBounds)
@@ -194,7 +203,11 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
drawStyle = styleStack.removeLast() drawStyle = styleStack.removeLast()
} }
@OptIn(ExperimentalContracts::class)
fun isolated(draw: CompositionDrawer.() -> Unit) { fun isolated(draw: CompositionDrawer.() -> Unit) {
contract {
callsInPlace(draw, InvocationKind.EXACTLY_ONCE)
}
pushModel() pushModel()
pushStyle() pushStyle()
draw() draw()
@@ -202,7 +215,11 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
popStyle() popStyle()
} }
@OptIn(ExperimentalContracts::class)
fun GroupNode.with(builder: CompositionDrawer.() -> Unit): GroupNode { fun GroupNode.with(builder: CompositionDrawer.() -> Unit): GroupNode {
contract {
callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
}
val oldCursor = cursor val oldCursor = cursor
cursor = this cursor = this
builder() builder()
@@ -216,7 +233,12 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
* @param id an optional identifier * @param id an optional identifier
* @param builder the function that is executed inside the group context * @param builder the function that is executed inside the group context
*/ */
@OptIn(ExperimentalContracts::class)
fun group(insert: Boolean = true, id: String? = null, builder: CompositionDrawer.() -> Unit): GroupNode { fun group(insert: Boolean = true, id: String? = null, builder: CompositionDrawer.() -> Unit): GroupNode {
contract {
callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
}
val group = GroupNode() val group = GroupNode()
group.id = id group.id = id
val oldCursor = cursor val oldCursor = cursor
@@ -267,8 +289,8 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
* @return an optional org.openrndr.shape.ShapeNodeNearestContour instance * @return an optional org.openrndr.shape.ShapeNodeNearestContour instance
*/ */
fun nearest( fun nearest(
point: Vector2, point: Vector2,
searchFrom: CompositionNode = composition.root as GroupNode searchFrom: CompositionNode = composition.root as GroupNode
): ShapeNodeNearestContour? { ): ShapeNodeNearestContour? {
return distances(point, searchFrom).firstOrNull() return distances(point, searchFrom).firstOrNull()
} }
@@ -297,13 +319,13 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
* @return a sorted list of [ShapeNodeNearestContour] describing distance to every contour * @return a sorted list of [ShapeNodeNearestContour] describing distance to every contour
*/ */
fun distances( fun distances(
point: Vector2, point: Vector2,
searchFrom: CompositionNode = composition.root as GroupNode searchFrom: CompositionNode = composition.root as GroupNode
): List<ShapeNodeNearestContour> { ): List<ShapeNodeNearestContour> {
return searchFrom.findShapes().flatMap { node -> return searchFrom.findShapes().flatMap { node ->
node.shape.contours.filter { !it.empty } node.shape.contours.filter { !it.empty }
.map { it.nearest(point) } .map { it.nearest(point) }
.map { ShapeNodeNearestContour(node, it, point - it.position, it.position.distanceTo(point)) } .map { ShapeNodeNearestContour(node, it, point - it.position, it.position.distanceTo(point)) }
}.sortedBy { it.distance } }.sortedBy { it.distance }
} }
@@ -342,7 +364,7 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
} }
fun CompositionNode.intersections(contour: ShapeContour, mergeThreshold: Double = 0.5) = fun CompositionNode.intersections(contour: ShapeContour, mergeThreshold: Double = 0.5) =
intersections(contour, this, mergeThreshold) intersections(contour, this, mergeThreshold)
/** /**
* Test a given `shape` against org.openrndr.shape.contours in the composition tree * Test a given `shape` against org.openrndr.shape.contours in the composition tree
@@ -361,7 +383,7 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
} }
fun CompositionNode.intersections(shape: Shape, mergeThreshold: Double = 0.5) = fun CompositionNode.intersections(shape: Shape, mergeThreshold: Double = 0.5) =
intersections(shape, this, mergeThreshold) intersections(shape, this, mergeThreshold)
fun shape(shape: Shape, insert: Boolean = true): ShapeNode? { fun shape(shape: Shape, insert: Boolean = true): ShapeNode? {
@@ -388,6 +410,7 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
shapeNode.transform = model shapeNode.transform = model
Matrix44.IDENTITY Matrix44.IDENTITY
} }
TransformMode.APPLY -> { TransformMode.APPLY -> {
shapeNode.transform = Matrix44.IDENTITY shapeNode.transform = Matrix44.IDENTITY
model model
@@ -408,6 +431,7 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
} }
toInsert toInsert
} }
else -> error("unreachable") else -> error("unreachable")
} }
shapeNode.stroke = stroke shapeNode.stroke = stroke
@@ -425,18 +449,19 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
} }
shapeNode shapeNode
} }
else -> { else -> {
val shapeNodes = (if (!clipMode.grouped) composition.findShapes() else cursor.findShapes()) val shapeNodes = (if (!clipMode.grouped) composition.findShapes() else cursor.findShapes())
val toRemove = shapeNodes.pmap { shapeNode -> val toRemove = shapeNodes.pmap { shapeNode ->
val inverse = shapeNode.effectiveTransform.inversed val inverse = shapeNode.effectiveTransform.inversed
val transformedShape = postShape.transform(inverse * model) val transformedShape = postShape.transform(inverse * model)
val operated = val operated =
when (clipMode.op) { when (clipMode.op) {
ClipOp.INTERSECT -> intersection(shapeNode.shape, transformedShape) ClipOp.INTERSECT -> intersection(shapeNode.shape, transformedShape)
ClipOp.UNION -> union(shapeNode.shape, transformedShape) ClipOp.UNION -> union(shapeNode.shape, transformedShape)
ClipOp.DIFFERENCE -> difference(shapeNode.shape, transformedShape) ClipOp.DIFFERENCE -> difference(shapeNode.shape, transformedShape)
else -> error("unsupported base op ${clipMode.op}") else -> error("unsupported base op ${clipMode.op}")
} }
return@pmap if (!operated.empty) { return@pmap if (!operated.empty) {
shapeNode.shape = operated shapeNode.shape = operated
null null
@@ -454,38 +479,45 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
fun shapes(shapes: List<Shape>, insert: Boolean = true) = shapes.map { shape(it, insert) } fun shapes(shapes: List<Shape>, insert: Boolean = true) = shapes.map { shape(it, insert) }
fun rectangle(rectangle: Rectangle, closed: Boolean = true, insert: Boolean = true) = contour(rectangle.contour.let { fun rectangle(rectangle: Rectangle, closed: Boolean = true, insert: Boolean = true) =
if (closed) { contour(rectangle.contour.let {
it if (closed) {
} else { it
it.open } else {
} it.open
}, insert = insert) }
}, insert = insert)
fun rectangle(x: Double, y: Double, width: Double, height: Double, closed: Boolean = true, insert: Boolean = true) = rectangle( fun rectangle(x: Double, y: Double, width: Double, height: Double, closed: Boolean = true, insert: Boolean = true) =
Rectangle(x, y, width, height), closed, insert) rectangle(
Rectangle(x, y, width, height), closed, insert
)
fun rectangles(rectangles: List<Rectangle>, insert: Boolean = true) = rectangles.map { rectangle(it, insert) } fun rectangles(rectangles: List<Rectangle>, insert: Boolean = true) = rectangles.map { rectangle(it, insert) }
fun rectangles(positions: List<Vector2>, width: Double, height: Double, insert: Boolean = true) = rectangles(positions.map { fun rectangles(positions: List<Vector2>, width: Double, height: Double, insert: Boolean = true) =
Rectangle(it, width, height) rectangles(positions.map {
}, insert) Rectangle(it, width, height)
}, insert)
fun rectangles(positions: List<Vector2>, dimensions: List<Vector2>, insert: Boolean) = rectangles((positions zip dimensions).map { fun rectangles(positions: List<Vector2>, dimensions: List<Vector2>, insert: Boolean) =
Rectangle(it.first, it.second.x, it.second.y) rectangles((positions zip dimensions).map {
}, insert) Rectangle(it.first, it.second.x, it.second.y)
}, insert)
fun circle(x: Double, y: Double, radius: Double, closed: Boolean = true, insert: Boolean = true) = circle( fun circle(x: Double, y: Double, radius: Double, closed: Boolean = true, insert: Boolean = true) = circle(
Circle( Circle(
Vector2(x, y), Vector2(x, y),
radius radius
), closed, insert) ), closed, insert
)
fun circle(position: Vector2, radius: Double, closed: Boolean = true, insert: Boolean = true) = circle( fun circle(position: Vector2, radius: Double, closed: Boolean = true, insert: Boolean = true) = circle(
Circle( Circle(
position, position,
radius radius
), closed, insert) ), closed, insert
)
fun circle(circle: Circle, closed: Boolean = true, insert: Boolean = true) = contour(circle.contour.let { fun circle(circle: Circle, closed: Boolean = true, insert: Boolean = true) = contour(circle.contour.let {
if (closed) { if (closed) {
@@ -504,12 +536,13 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
) )
}, insert) }, insert)
fun circles(positions: List<Vector2>, radii: List<Double>, insert: Boolean = true) = circles((positions zip radii).map { fun circles(positions: List<Vector2>, radii: List<Double>, insert: Boolean = true) =
Circle( circles((positions zip radii).map {
it.first, Circle(
it.second it.first,
) it.second
}, insert) )
}, insert)
/* /*
fun ellipse( fun ellipse(
@@ -540,17 +573,17 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
fun lineSegment( fun lineSegment(
startX: Double, startX: Double,
startY: Double, startY: Double,
endX: Double, endX: Double,
endY: Double, endY: Double,
insert: Boolean = true insert: Boolean = true
) = lineSegment(LineSegment(startX, startY, endX, endY), insert) ) = lineSegment(LineSegment(startX, startY, endX, endY), insert)
fun lineSegment( fun lineSegment(
start: Vector2, start: Vector2,
end: Vector2, end: Vector2,
insert: Boolean = true insert: Boolean = true
) = lineSegment(LineSegment(start, end), insert) ) = lineSegment(LineSegment(start, end), insert)
fun lineSegment( fun lineSegment(
@@ -599,19 +632,19 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
} }
fun lineStrip( fun lineStrip(
points: List<Vector2>, points: List<Vector2>,
insert: Boolean = true insert: Boolean = true
) = contour(ShapeContour.fromPoints(points, false, YPolarity.CW_NEGATIVE_Y), insert) ) = contour(ShapeContour.fromPoints(points, false, YPolarity.CW_NEGATIVE_Y), insert)
fun lineLoop( fun lineLoop(
points: List<Vector2>, points: List<Vector2>,
insert: Boolean = true insert: Boolean = true
) = contour(ShapeContour.fromPoints(points, true, YPolarity.CW_NEGATIVE_Y), insert) ) = contour(ShapeContour.fromPoints(points, true, YPolarity.CW_NEGATIVE_Y), insert)
fun text( fun text(
text: String, text: String,
position: Vector2, position: Vector2,
insert: Boolean = true insert: Boolean = true
): TextNode { ): TextNode {
val g = GroupNode() val g = GroupNode()
g.style.transform = Transform.Matrix(transform { translate(position.xy0) }) g.style.transform = Transform.Matrix(transform { translate(position.xy0) })
@@ -642,18 +675,18 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
} }
fun texts(text: List<String>, positions: List<Vector2>) = fun texts(text: List<String>, positions: List<Vector2>) =
(text zip positions).map { (text zip positions).map {
text(it.first, it.second) text(it.first, it.second)
} }
/** /**
* Adds an image to the composition tree * Adds an image to the composition tree
*/ */
fun image( fun image(
image: ColorBuffer, image: ColorBuffer,
x: Double = 0.0, x: Double = 0.0,
y: Double = 0.0, y: Double = 0.0,
insert: Boolean = true insert: Boolean = true
): ImageNode { ): ImageNode {
val node = ImageNode(image, x, y, width = image.width.toDouble(), height = image.height.toDouble()) val node = ImageNode(image, x, y, width = image.width.toDouble(), height = image.height.toDouble())
node.style.transform = Transform.Matrix(this.model) node.style.transform = Transform.Matrix(this.model)
@@ -697,7 +730,11 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
} }
} }
@OptIn(ExperimentalContracts::class)
fun CompositionNode.transform(builder: TransformBuilder.() -> Unit) { fun CompositionNode.transform(builder: TransformBuilder.() -> Unit) {
contract {
callsInPlace(builder, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
}
return this.transform(builder) return this.transform(builder)
} }
@@ -713,12 +750,15 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
is ImageNode -> { is ImageNode -> {
ImageNode(node.image, node.x, node.y, node.width, node.height) ImageNode(node.image, node.x, node.y, node.width, node.height)
} }
is ShapeNode -> { is ShapeNode -> {
ShapeNode(node.shape) ShapeNode(node.shape)
} }
is TextNode -> { is TextNode -> {
TextNode(node.text, node.contour) TextNode(node.text, node.contour)
} }
is GroupNode -> { is GroupNode -> {
val children = node.children.map { nodeCopy(it) }.toMutableList() val children = node.children.map { nodeCopy(it) }.toMutableList()
val groupNode = GroupNode(children) val groupNode = GroupNode(children)
@@ -742,20 +782,39 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
} }
/** /**
* Creates a [Composition]. The draw operations contained inside * Draws a vector composition by applying a provided drawing function.
* the [drawFunction] do not render graphics to the screen, *
* but populate the Composition instead. * @param documentBounds Defines the dimensions and bounds of the composition. Defaults to `defaultCompositionDimensions`.
* @param composition The target composition to be drawn on. If null, a new composition will be created.
* @param cursor Specifies the current position within the composition structure. Defaults to the root of the given composition cast as a `GroupNode`.
* @param drawFunction The actual drawing logic that will be executed in the drawing context of the `CompositionDrawer`.
* @return The resulting `Composition` after applying the drawing function.
*/ */
@OptIn(ExperimentalContracts::class)
fun drawComposition( fun drawComposition(
documentBounds: CompositionDimensions = defaultCompositionDimensions, documentBounds: CompositionDimensions = defaultCompositionDimensions,
composition: Composition? = null, composition: Composition? = null,
cursor: GroupNode? = composition?.root as? GroupNode, cursor: GroupNode? = composition?.root as? GroupNode,
drawFunction: CompositionDrawer.() -> Unit drawFunction: CompositionDrawer.() -> Unit
): Composition = CompositionDrawer(documentBounds, composition, cursor).apply { drawFunction() }.composition ): Composition {
contract {
callsInPlace(drawFunction, InvocationKind.EXACTLY_ONCE)
}
return CompositionDrawer(documentBounds, composition, cursor).apply { drawFunction() }.composition
}
/** /**
* Draw into an existing [Composition]. * Draws the content of an existing composition using the provided drawing function.
*
* @param drawFunction the drawing logic to be executed using a [CompositionDrawer].
* This function allows defining how the composition should be rendered visually.
* @param cursor an optional [GroupNode] that serves as the starting point for drawing.
* Defaults to the root of the composition if not provided.
*/ */
fun Composition.draw(drawFunction: CompositionDrawer.() -> Unit) { @OptIn(ExperimentalContracts::class)
fun Composition.draw(drawFunction: CompositionDrawer.() -> Unit, cursor: GroupNode? = this.root as? GroupNode) {
contract {
callsInPlace(drawFunction, InvocationKind.EXACTLY_ONCE)
}
drawComposition(composition = this, drawFunction = drawFunction) drawComposition(composition = this, drawFunction = drawFunction)
} }

View File

@@ -6,7 +6,16 @@ import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
// Derives Composition dimensions from current Drawer /**
* Draws a composition within the specified document bounds or an existing composition.
* This function utilizes a customizable draw function to define the drawing behavior.
*
* @param documentBounds Specifies the dimensions for the drawing area. Defaults to the full drawable area of the program.
* @param composition An optional existing composition to draw onto. If not provided, a new composition is created.
* @param cursor An optional cursor representing the current position in the composition hierarchy. Defaults to the root of the provided composition.
* @param drawFunction A lambda function defining the drawing operations to be performed using the `CompositionDrawer`.
* @return The resulting composition after applying the draw function.
*/
@OptIn(ExperimentalContracts::class) @OptIn(ExperimentalContracts::class)
fun Program.drawComposition( fun Program.drawComposition(
documentBounds: CompositionDimensions = CompositionDimensions(0.0.pixels, 0.0.pixels, this.drawer.width.toDouble().pixels, this.drawer.height.toDouble().pixels), documentBounds: CompositionDimensions = CompositionDimensions(0.0.pixels, 0.0.pixels, this.drawer.width.toDouble().pixels, this.drawer.height.toDouble().pixels),
@@ -20,6 +29,16 @@ fun Program.drawComposition(
return CompositionDrawer(documentBounds, composition, cursor).apply { drawFunction() }.composition return CompositionDrawer(documentBounds, composition, cursor).apply { drawFunction() }.composition
} }
/**
* Draws a composition using the specified document bounds and drawing logic.
* Optionally, an existing composition and cursor can be passed to update or build upon them.
*
* @param documentBounds The bounding rectangle representing the area to be drawn.
* @param composition An optional existing composition to update. If null, a new composition will be created.
* @param cursor An optional cursor `GroupNode` used as the starting position for appending new elements. Defaults to the root of the provided composition if available.
* @param drawFunction A lambda function containing the drawing operations to be applied.
* @return The resulting `Composition` object after performing the drawing operations.
*/
@OptIn(ExperimentalContracts::class) @OptIn(ExperimentalContracts::class)
fun Program.drawComposition( fun Program.drawComposition(
documentBounds: Rectangle, documentBounds: Rectangle,
@@ -31,6 +50,4 @@ fun Program.drawComposition(
callsInPlace(drawFunction, InvocationKind.EXACTLY_ONCE) callsInPlace(drawFunction, InvocationKind.EXACTLY_ONCE)
} }
return CompositionDrawer(CompositionDimensions(documentBounds), composition, cursor).apply { drawFunction() }.composition return CompositionDrawer(CompositionDimensions(documentBounds), composition, cursor).apply { drawFunction() }.composition
} }