Skip to content

Instantly share code, notes, and snippets.

@timsneath
Last active May 27, 2023 18:08
Show Gist options
  • Save timsneath/0f020197a5d4c980342d5c7d9e935cee to your computer and use it in GitHub Desktop.
Save timsneath/0f020197a5d4c980342d5c7d9e935cee to your computer and use it in GitHub Desktop.
Demonstrates a polar coordinate system with Flutter
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart';
const double kTwoPi = 2 * math.pi;
class SectorConstraints extends Constraints {
const SectorConstraints({
this.minDeltaRadius = 0.0,
this.maxDeltaRadius = double.infinity,
this.minDeltaTheta = 0.0,
this.maxDeltaTheta = kTwoPi,
}) : assert(maxDeltaRadius >= minDeltaRadius),
assert(maxDeltaTheta >= minDeltaTheta);
const SectorConstraints.tight({ double deltaRadius = 0.0, double deltaTheta = 0.0 })
: minDeltaRadius = deltaRadius,
maxDeltaRadius = deltaRadius,
minDeltaTheta = deltaTheta,
maxDeltaTheta = deltaTheta;
final double minDeltaRadius;
final double maxDeltaRadius;
final double minDeltaTheta;
final double maxDeltaTheta;
double constrainDeltaRadius(double deltaRadius) {
return deltaRadius.clamp(minDeltaRadius, maxDeltaRadius) as double;
}
double constrainDeltaTheta(double deltaTheta) {
return deltaTheta.clamp(minDeltaTheta, maxDeltaTheta) as double;
}
@override
bool get isTight => minDeltaTheta >= maxDeltaTheta && minDeltaTheta >= maxDeltaTheta;
@override
bool get isNormalized => minDeltaRadius <= maxDeltaRadius && minDeltaTheta <= maxDeltaTheta;
@override
bool debugAssertIsValid({
bool isAppliedConstraint = false,
InformationCollector informationCollector,
}) {
assert(isNormalized);
return isNormalized;
}
}
class SectorDimensions {
const SectorDimensions({ this.deltaRadius = 0.0, this.deltaTheta = 0.0 });
factory SectorDimensions.withConstraints(
SectorConstraints constraints, {
double deltaRadius = 0.0,
double deltaTheta = 0.0,
}) {
return SectorDimensions(
deltaRadius: constraints.constrainDeltaRadius(deltaRadius),
deltaTheta: constraints.constrainDeltaTheta(deltaTheta),
);
}
final double deltaRadius;
final double deltaTheta;
}
class SectorParentData extends ParentData {
double radius = 0.0;
double theta = 0.0;
}
/// Base class for [RenderObject]s that live in a polar coordinate space.
///
/// In a polar coordinate system each point on a plane is determined by a
/// distance from a reference point ("radius") and an angle from a reference
/// direction ("theta").
///
/// See also:
///
/// * <https://en.wikipedia.org/wiki/Polar_coordinate_system>, which defines
/// the polar coordinate space.
/// * [RenderBox], which is the base class for [RenderObject]s that live in a
/// Cartesian coordinate space.
abstract class RenderSector extends RenderObject {
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SectorParentData)
child.parentData = SectorParentData();
}
// RenderSectors always use SectorParentData subclasses, as they need to be
// able to read their position information for painting and hit testing.
@override
SectorParentData get parentData => super.parentData as SectorParentData;
SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) {
return SectorDimensions.withConstraints(constraints);
}
@override
SectorConstraints get constraints => super.constraints as SectorConstraints;
@override
void debugAssertDoesMeetConstraints() {
assert(constraints != null);
assert(deltaRadius != null);
assert(deltaRadius < double.infinity);
assert(deltaTheta != null);
assert(deltaTheta < double.infinity);
assert(constraints.minDeltaRadius <= deltaRadius);
assert(deltaRadius <= math.max(constraints.minDeltaRadius, constraints.maxDeltaRadius));
assert(constraints.minDeltaTheta <= deltaTheta);
assert(deltaTheta <= math.max(constraints.minDeltaTheta, constraints.maxDeltaTheta));
}
@override
void performResize() {
// default behavior for subclasses that have sizedByParent = true
deltaRadius = constraints.constrainDeltaRadius(0.0);
deltaTheta = constraints.constrainDeltaTheta(0.0);
}
@override
void performLayout() {
// descendants have to either override performLayout() to set both
// the dimensions and lay out children, or, set sizedByParent to
// true so that performResize()'s logic above does its thing.
assert(sizedByParent);
}
@override
Rect get paintBounds => Rect.fromLTWH(0.0, 0.0, 2.0 * deltaRadius, 2.0 * deltaRadius);
@override
Rect get semanticBounds => Rect.fromLTWH(-deltaRadius, -deltaRadius, 2.0 * deltaRadius, 2.0 * deltaRadius);
bool hitTest(SectorHitTestResult result, { double radius, double theta }) {
if (radius < parentData.radius || radius >= parentData.radius + deltaRadius ||
theta < parentData.theta || theta >= parentData.theta + deltaTheta)
return false;
hitTestChildren(result, radius: radius, theta: theta);
result.add(SectorHitTestEntry(this, radius: radius, theta: theta));
return true;
}
void hitTestChildren(SectorHitTestResult result, { double radius, double theta }) { }
double deltaRadius;
double deltaTheta;
}
abstract class RenderDecoratedSector extends RenderSector {
RenderDecoratedSector(BoxDecoration decoration) : _decoration = decoration;
BoxDecoration _decoration;
BoxDecoration get decoration => _decoration;
set decoration(BoxDecoration value) {
if (value == _decoration)
return;
_decoration = value;
markNeedsPaint();
}
// offset must point to the center of the circle
@override
void paint(PaintingContext context, Offset offset) {
assert(deltaRadius != null);
assert(deltaTheta != null);
assert(parentData is SectorParentData);
if (_decoration == null)
return;
if (_decoration.color != null) {
final Canvas canvas = context.canvas;
final Paint paint = Paint()..color = _decoration.color;
final Path path = Path();
final double outerRadius = parentData.radius + deltaRadius;
final Rect outerBounds = Rect.fromLTRB(offset.dx-outerRadius, offset.dy-outerRadius, offset.dx+outerRadius, offset.dy+outerRadius);
path.arcTo(outerBounds, parentData.theta, deltaTheta, true);
final double innerRadius = parentData.radius;
final Rect innerBounds = Rect.fromLTRB(offset.dx-innerRadius, offset.dy-innerRadius, offset.dx+innerRadius, offset.dy+innerRadius);
path.arcTo(innerBounds, parentData.theta + deltaTheta, -deltaTheta, false);
path.close();
canvas.drawPath(path, paint);
}
}
}
class SectorChildListParentData extends SectorParentData with ContainerParentDataMixin<RenderSector> { }
class RenderSectorWithChildren extends RenderDecoratedSector with ContainerRenderObjectMixin<RenderSector, SectorChildListParentData> {
RenderSectorWithChildren(BoxDecoration decoration) : super(decoration);
@override
void hitTestChildren(SectorHitTestResult result, { double radius, double theta }) {
RenderSector child = lastChild;
while (child != null) {
if (child.hitTest(result, radius: radius, theta: theta))
return;
final SectorChildListParentData childParentData = child.parentData as SectorChildListParentData;
child = childParentData.previousSibling;
}
}
@override
void visitChildren(RenderObjectVisitor visitor) {
RenderSector child = lastChild;
while (child != null) {
visitor(child);
final SectorChildListParentData childParentData = child.parentData as SectorChildListParentData;
child = childParentData.previousSibling;
}
}
}
class RenderSectorRing extends RenderSectorWithChildren {
// lays out RenderSector children in a ring
RenderSectorRing({
BoxDecoration decoration,
double deltaRadius = double.infinity,
double padding = 0.0,
}) : _padding = padding,
assert(deltaRadius >= 0.0),
_desiredDeltaRadius = deltaRadius,
super(decoration);
double _desiredDeltaRadius;
double get desiredDeltaRadius => _desiredDeltaRadius;
set desiredDeltaRadius(double value) {
assert(value != null);
assert(value >= 0);
if (_desiredDeltaRadius != value) {
_desiredDeltaRadius = value;
markNeedsLayout();
}
}
double _padding;
double get padding => _padding;
set padding(double value) {
assert(value != null);
if (_padding != value) {
_padding = value;
markNeedsLayout();
}
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SectorChildListParentData)
child.parentData = SectorChildListParentData();
}
@override
SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) {
final double outerDeltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius);
final double innerDeltaRadius = math.max(0.0, outerDeltaRadius - padding * 2.0);
final double childRadius = radius + padding;
final double paddingTheta = math.atan(padding / (radius + outerDeltaRadius));
double innerTheta = paddingTheta; // increments with each child
double remainingDeltaTheta = math.max(0.0, constraints.maxDeltaTheta - (innerTheta + paddingTheta));
RenderSector child = firstChild;
while (child != null) {
final SectorConstraints innerConstraints = SectorConstraints(
maxDeltaRadius: innerDeltaRadius,
maxDeltaTheta: remainingDeltaTheta,
);
final SectorDimensions childDimensions = child.getIntrinsicDimensions(innerConstraints, childRadius);
innerTheta += childDimensions.deltaTheta;
remainingDeltaTheta -= childDimensions.deltaTheta;
final SectorChildListParentData childParentData = child.parentData as SectorChildListParentData;
child = childParentData.nextSibling;
if (child != null) {
innerTheta += paddingTheta;
remainingDeltaTheta -= paddingTheta;
}
}
return SectorDimensions.withConstraints(constraints,
deltaRadius: outerDeltaRadius,
deltaTheta: innerTheta);
}
@override
void performLayout() {
assert(parentData is SectorParentData);
deltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius);
assert(deltaRadius < double.infinity);
final double innerDeltaRadius = deltaRadius - padding * 2.0;
final double childRadius = parentData.radius + padding;
final double paddingTheta = math.atan(padding / (parentData.radius + deltaRadius));
double innerTheta = paddingTheta; // increments with each child
double remainingDeltaTheta = constraints.maxDeltaTheta - (innerTheta + paddingTheta);
RenderSector child = firstChild;
while (child != null) {
final SectorConstraints innerConstraints = SectorConstraints(
maxDeltaRadius: innerDeltaRadius,
maxDeltaTheta: remainingDeltaTheta,
);
assert(child.parentData is SectorParentData);
child.parentData.theta = innerTheta;
child.parentData.radius = childRadius;
child.layout(innerConstraints, parentUsesSize: true);
innerTheta += child.deltaTheta;
remainingDeltaTheta -= child.deltaTheta;
final SectorChildListParentData childParentData = child.parentData as SectorChildListParentData;
child = childParentData.nextSibling;
if (child != null) {
innerTheta += paddingTheta;
remainingDeltaTheta -= paddingTheta;
}
}
deltaTheta = innerTheta;
}
// offset must point to the center of our circle
// each sector then knows how to paint itself at its location
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
RenderSector child = firstChild;
while (child != null) {
context.paintChild(child, offset);
final SectorChildListParentData childParentData = child.parentData as SectorChildListParentData;
child = childParentData.nextSibling;
}
}
}
class RenderSectorSlice extends RenderSectorWithChildren {
// lays out RenderSector children in a stack
RenderSectorSlice({
BoxDecoration decoration,
double deltaTheta = kTwoPi,
double padding = 0.0,
}) : _padding = padding, _desiredDeltaTheta = deltaTheta, super(decoration);
double _desiredDeltaTheta;
double get desiredDeltaTheta => _desiredDeltaTheta;
set desiredDeltaTheta(double value) {
assert(value != null);
if (_desiredDeltaTheta != value) {
_desiredDeltaTheta = value;
markNeedsLayout();
}
}
double _padding;
double get padding => _padding;
set padding(double value) {
assert(value != null);
if (_padding != value) {
_padding = value;
markNeedsLayout();
}
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SectorChildListParentData)
child.parentData = SectorChildListParentData();
}
@override
SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) {
assert(parentData is SectorParentData);
final double paddingTheta = math.atan(padding / parentData.radius);
final double outerDeltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta);
final double innerDeltaTheta = outerDeltaTheta - paddingTheta * 2.0;
double childRadius = parentData.radius + padding;
double remainingDeltaRadius = constraints.maxDeltaRadius - (padding * 2.0);
RenderSector child = firstChild;
while (child != null) {
final SectorConstraints innerConstraints = SectorConstraints(
maxDeltaRadius: remainingDeltaRadius,
maxDeltaTheta: innerDeltaTheta,
);
final SectorDimensions childDimensions = child.getIntrinsicDimensions(innerConstraints, childRadius);
childRadius += childDimensions.deltaRadius;
remainingDeltaRadius -= childDimensions.deltaRadius;
final SectorChildListParentData childParentData = child.parentData as SectorChildListParentData;
child = childParentData.nextSibling;
childRadius += padding;
remainingDeltaRadius -= padding;
}
return SectorDimensions.withConstraints(constraints,
deltaRadius: childRadius - parentData.radius,
deltaTheta: outerDeltaTheta);
}
@override
void performLayout() {
assert(parentData is SectorParentData);
deltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta);
assert(deltaTheta <= kTwoPi);
final double paddingTheta = math.atan(padding / parentData.radius);
final double innerTheta = parentData.theta + paddingTheta;
final double innerDeltaTheta = deltaTheta - paddingTheta * 2.0;
double childRadius = parentData.radius + padding;
double remainingDeltaRadius = constraints.maxDeltaRadius - (padding * 2.0);
RenderSector child = firstChild;
while (child != null) {
final SectorConstraints innerConstraints = SectorConstraints(
maxDeltaRadius: remainingDeltaRadius,
maxDeltaTheta: innerDeltaTheta,
);
child.parentData.theta = innerTheta;
child.parentData.radius = childRadius;
child.layout(innerConstraints, parentUsesSize: true);
childRadius += child.deltaRadius;
remainingDeltaRadius -= child.deltaRadius;
final SectorChildListParentData childParentData = child.parentData as SectorChildListParentData;
child = childParentData.nextSibling;
childRadius += padding;
remainingDeltaRadius -= padding;
}
deltaRadius = childRadius - parentData.radius;
}
// offset must point to the center of our circle
// each sector then knows how to paint itself at its location
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
RenderSector child = firstChild;
while (child != null) {
assert(child.parentData is SectorChildListParentData);
context.paintChild(child, offset);
final SectorChildListParentData childParentData = child.parentData as SectorChildListParentData;
child = childParentData.nextSibling;
}
}
}
class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChildMixin<RenderSector> {
RenderBoxToRenderSectorAdapter({ double innerRadius = 0.0, RenderSector child })
: _innerRadius = innerRadius {
this.child = child;
}
double _innerRadius;
double get innerRadius => _innerRadius;
set innerRadius(double value) {
_innerRadius = value;
markNeedsLayout();
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SectorParentData)
child.parentData = SectorParentData();
}
@override
double computeMinIntrinsicWidth(double height) {
if (child == null)
return 0.0;
return getIntrinsicDimensions(height: height).width;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child == null)
return 0.0;
return getIntrinsicDimensions(height: height).width;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child == null)
return 0.0;
return getIntrinsicDimensions(width: width).height;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child == null)
return 0.0;
return getIntrinsicDimensions(width: width).height;
}
Size getIntrinsicDimensions({
double width = double.infinity,
double height = double.infinity,
}) {
assert(child is RenderSector);
assert(child.parentData is SectorParentData);
assert(width != null);
assert(height != null);
if (!width.isFinite && !height.isFinite)
return Size.zero;
final double maxChildDeltaRadius = math.max(0.0, math.min(width, height) / 2.0 - innerRadius);
final SectorDimensions childDimensions = child.getIntrinsicDimensions(SectorConstraints(maxDeltaRadius: maxChildDeltaRadius), innerRadius);
final double dimension = (innerRadius + childDimensions.deltaRadius) * 2.0;
return Size.square(dimension);
}
@override
void performLayout() {
if (child == null || (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight)) {
size = constraints.constrain(Size.zero);
child?.layout(SectorConstraints(maxDeltaRadius: innerRadius), parentUsesSize: true);
return;
}
assert(child is RenderSector);
assert(child.parentData is SectorParentData);
final double maxChildDeltaRadius = math.min(constraints.maxWidth, constraints.maxHeight) / 2.0 - innerRadius;
child.parentData.radius = innerRadius;
child.parentData.theta = 0.0;
child.layout(SectorConstraints(maxDeltaRadius: maxChildDeltaRadius), parentUsesSize: true);
final double dimension = (innerRadius + child.deltaRadius) * 2.0;
size = constraints.constrain(Size(dimension, dimension));
}
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
if (child != null) {
final Rect bounds = offset & size;
// we move the offset to the center of the circle for the RenderSectors
context.paintChild(child, bounds.center);
}
}
@override
bool hitTest(BoxHitTestResult result, { Offset position }) {
if (child == null)
return false;
double x = position.dx;
double y = position.dy;
// translate to our origin
x -= size.width / 2.0;
y -= size.height / 2.0;
// convert to radius/theta
final double radius = math.sqrt(x * x + y * y);
final double theta = (math.atan2(x, -y) - math.pi / 2.0) % kTwoPi;
if (radius < innerRadius)
return false;
if (radius >= innerRadius + child.deltaRadius)
return false;
if (theta > child.deltaTheta)
return false;
child.hitTest(SectorHitTestResult.wrap(result), radius: radius, theta: theta);
result.add(BoxHitTestEntry(this, position));
return true;
}
}
class RenderSolidColor extends RenderDecoratedSector {
RenderSolidColor(
this.backgroundColor, {
this.desiredDeltaRadius = double.infinity,
this.desiredDeltaTheta = kTwoPi,
}) : super(BoxDecoration(color: backgroundColor));
double desiredDeltaRadius;
double desiredDeltaTheta;
final Color backgroundColor;
@override
SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) {
return SectorDimensions.withConstraints(constraints, deltaTheta: desiredDeltaTheta);
}
@override
void performLayout() {
deltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius);
deltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta);
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is PointerDownEvent) {
decoration = const BoxDecoration(color: Color(0xFFFF0000));
} else if (event is PointerUpEvent) {
decoration = BoxDecoration(color: backgroundColor);
}
}
}
/// The result of performing a hit test on [RenderSector]s.
class SectorHitTestResult extends HitTestResult {
/// Creates an empty hit test result for hit testing on [RenderSector].
SectorHitTestResult() : super();
/// Wraps `result` to create a [HitTestResult] that implements the
/// [SectorHitTestResult] protocol for hit testing on [RenderSector]s.
///
/// This method is used by [RenderObject]s that adapt between the
/// [RenderSector]-world and the non-[RenderSector]-world to convert a (subtype of)
/// [HitTestResult] to a [SectorHitTestResult] for hit testing on [RenderSector]s.
///
/// The [HitTestEntry]s added to the returned [SectorHitTestResult] are also
/// added to the wrapped `result` (both share the same underlying data
/// structure to store [HitTestEntry]s).
///
/// See also:
///
/// * [HitTestResult.wrap], which turns a [SectorHitTestResult] back into a
/// generic [HitTestResult].
SectorHitTestResult.wrap(HitTestResult result) : super.wrap(result);
}
/// A hit test entry used by [RenderSector].
class SectorHitTestEntry extends HitTestEntry {
/// Creates a box hit test entry.
///
/// The [radius] and [theta] argument must not be null.
SectorHitTestEntry(RenderSector target, { @required this.radius, @required this.theta })
: assert(radius != null),
assert(theta != null),
super(target);
@override
RenderSector get target => super.target as RenderSector;
/// The radius component of the hit test position in the local coordinates of
/// [target].
final double radius;
/// The theta component of the hit test position in the local coordinates of
/// [target].
final double theta;
}
RenderBox buildSectorExample() {
final RenderSectorRing rootCircle = RenderSectorRing(padding: 10);
rootCircle.add(RenderSolidColor(const Color(0xFF00FFFF), desiredDeltaTheta: kTwoPi * 0.15));
rootCircle.add(RenderSolidColor(const Color(0xFF0000FF), desiredDeltaTheta: kTwoPi * 0.4));
final RenderSectorSlice stack = RenderSectorSlice(padding: 2);
stack.add(RenderSolidColor(const Color(0xFFFFFF00), desiredDeltaRadius: 20));
stack.add(RenderSolidColor(const Color(0xFFFF9000), desiredDeltaRadius: 20));
stack.add(RenderSolidColor(const Color(0xFF00FF00)));
rootCircle.add(stack);
return RenderBoxToRenderSectorAdapter(innerRadius: 50, child: rootCircle);
}
class PolarApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: const Color(0xFF008BFF)),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: WidgetToRenderBoxAdapter(renderBox: buildSectorExample()),
),
),
);
}
}
void main() {
runApp(PolarApp());
}
@jogboms
Copy link

jogboms commented Mar 8, 2022

Hello @timsneath
Seeing this is actually still linked to the docs, I made a fork of this updated to null-safety.
https://gist.github.com/jogboms/ce696a1a9a9492468ef243f75e73a727

@timsneath
Copy link
Author

Thanks so much. Filed flutter/website#6912 to address this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment