Get size of an element in Jetpack Compose - android-jetpack-compose

I'm making an isometric grid and using padding for it cells. Currently as parameters I'm waiting for its items width and height. But how can I avoid this and use final cells size in an optimal way?
I tried some solutions (commented code is the original way) from this question but it doesn't look legit for the case: I have extra multiple calculations, an error: Padding must be non-negative and it doesn't show correctly in Android Studio preview.
Modifier.onGloballyPositioned also looks same and incorrect for the case, what's the right way?
#Composable
fun <T> IsometricGrid(
gridWidth: Int,
gridHeight: Int,
cellWidth: Int,
cellHeight: Int,
data: List<T>,
itemContent: #Composable (Int, T) -> Unit
) {
var width by remember { mutableStateOf(0) }
var height by remember { mutableStateOf(0) }
for (y in 0 until gridHeight) {
for (x in 0 until gridWidth) {
// val start = (y % 2 * 0.5 + x) * cellWidth
// val top = y * cellHeight * 0.5
val index = x * gridHeight + y
Box(
modifier = Modifier
.onSizeChanged {
width = it.width
height = it.height
Timber.i("$width, $height")
}
// .padding(start = start.dp, top = top.dp)
.padding(start = ((y % 2 * 0.5 + x) * cellWidth).dp, top = (y * height * 0.5).dp)
) {
itemContent(index, data[index])
}
}
}
}
Added usage:
IsometricGrid(4, 4, 100, 50, listOf<Foo>(...)) { index: Int, foo: Foo ->
Icon(...)
}

start = (y % 2 * 0.5 + x * width).dp, top = (y * height * 0.5).dp
This line is not correct because you add dp extension to pixel instead of converting pixel to dp
What you should be doing
val density = LocalDensity.current
density.run{(y % 2 * 0.5 + x * width).toDp()}
because dp value of any pixel values is calculated as
dpValue = valueInPixel/density
let's say you have 100px with a density = 2.0f
your dp value should be 50.dp. If you calculate it as in your question you find that it returns 100.dp
Try this. I don't have data, so i'm not able to try your function.
#Composable
fun <T> IsometricGrid(
gridWidth: Int,
gridHeight: Int,
cellWidth: Int,
cellHeight: Int,
data: List<T>,
itemContent: #Composable (Int, T) -> Unit
) {
val density = LocalDensity.current
var width by remember { mutableStateOf(0) }
var height by remember { mutableStateOf(0) }
for (y in 0 until gridHeight) {
for (x in 0 until gridWidth) {
// val start = (y % 2 * 0.5 + x) * cellWidth
// val top = y * cellHeight * 0.5
val index = x * gridHeight + y
val startInDp = density.run { ((y % 2 * 0.5f + x) * cellWidth).toDp() }
val topInDp = density.run { (y * height * 0.5f).toDp() }
Box(
modifier = Modifier
.onSizeChanged {
width = it.width
height = it.height
}
// .padding(start = start.dp, top = top.dp)
.padding(
start = startInDp,
top = topInDp
)
) {
itemContent(index, data[index])
}
}
}
}
toDp() is extension for Float in Density interface
/** Convert a [Float] pixel value to a Dp */
#Stable
fun Float.toDp(): Dp = (this / density).dp

Related

How to correctly drag the view around the circle?

I want the green dot to follow the touch point in a circular path, but it doesn't seem to be doing it right.
It seems like there is an unwanted offset somewhere but I can't find it on my own for quite some time.
Here is my code:
#Preview
#Composable
fun Test() {
val touchPoint = remember { mutableStateOf(Offset.Zero) }
Scaffold {
Column() {
Box(Modifier.height(100.dp).fillMaxWidth().background(Color.Blue))
Layout(
modifier = Modifier.aspectRatio(1f).fillMaxSize(),
content = {
Box(
Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.Green)
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, dragAmount ->
change.consumeAllChanges()
touchPoint.value += dragAmount
}
)
}
)
}
) { measurables, constraints ->
val dot = measurables.first().measure(constraints.copy(minHeight = 0, minWidth = 0))
val width = constraints.maxWidth
val height = constraints.maxHeight
val centerX = width / 2
val centerY = height / 2
val lengthFromCenter = width / 2 - dot.width / 2
val touchX = touchPoint.value.x
val touchY = touchPoint.value.y
layout(width, height) {
// I planned to achieve the desired behaviour with the following steps:
// 1. Convert cartesian coordinates to polar ones
val r = sqrt(touchX.pow(2) + touchY.pow(2))
val angle = atan2(touchY.toDouble(), touchX.toDouble())
// 2. Use fixed polar radius
val rFixed = lengthFromCenter
// 3. Convert it back to cartesian coordinates
val x = rFixed * cos(angle)
val y = rFixed * sin(angle)
// 4. Layout on screen
dot.place(
x = (x + centerX - dot.width / 2).roundToInt(),
y = (y + centerY - dot.height / 2).roundToInt()
)
}
}
Box(Modifier.fillMaxSize().background(Color.Blue))
}
}
}
I'm definitely missing something but don't know what exactly. What am I doing wrong?
touchPoint.value += dragAmount
Is in pixel values, and you're updating the position of the dot with pixel values, where it requires dp values. If you update that with
private fun Float.pxToDp(context: Context): Dp = // Float or Int, depends on the value you have, or Double
(this / context.resources.displayMetrics.density).dp
The amount with which it will be moved, will be smaller and reflect the dragging made by the user
You can easily achieve this by using some math:
#Composable
fun CircularView(
content: #Composable () -> Unit
) {
var middle by remember {
mutableStateOf(Offset.Zero)
}
var size by remember {
mutableStateOf(0.dp)
}
var dragAngle by remember {
mutableStateOf(0f)
}
Canvas(modifier = Modifier.size(size)) {
drawCircle(
color = Color.Red,
center = middle,
style = Stroke(1.dp.toPx())
)
}
Layout(
content = content,
modifier = Modifier.pointerInput(true) {
detectDragGestures(
onDrag = { change, _ ->
change.consumeAllChanges()
val positionOfDrag = change.position
val previousPosition = change.previousPosition
dragAngle += atan2(
positionOfDrag.x - middle.x,
positionOfDrag.y - middle.y
) - atan2(
previousPosition.x - middle.x,
previousPosition.y - middle.y
)
}
)
}
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
layout(layoutWidth, layoutHeight) {
val childCount = placeables.size
if (childCount == 0) return#layout
val middleX = layoutWidth / 2f
val middleY = layoutHeight / 2f
middle = Offset(middleX, middleY)
val angleBetween = 2 * PI / childCount
val radius =
min(
layoutWidth - (placeables.maxByOrNull { it.width }?.width ?: 0),
layoutHeight - (placeables.maxByOrNull { it.height }?.height ?: 0)
) / 2
size = (radius * 2).toDp()
placeables.forEachIndexed { index, placeable ->
val angle = index * angleBetween - PI / 2 - dragAngle
val x = middleX + (radius) * cos(angle) - placeable.width / 2f
val y = middleY + (radius) * sin(angle) - placeable.height / 2f
placeable.placeRelative(x = x.toInt(), y = y.toInt())
}
}
}
}
On the calling side:
CircularView {
repeat(10) {
Box(
modifier = Modifier
.background(
Color(
red = random.nextInt(255),
green = random.nextInt(255),
blue = random.nextInt(255)
), shape = CircleShape
)
.size(50.dp),
contentAlignment = Alignment.Center
) {
Text(text = it.toString(), fontSize = 12.sp, color = Color.White)
}
}
}

Histogram based on image as vector graphic

I would like to transform histograms based on images to vector graphics.
This could be a start:
function preload() {
img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Cirrus_sky_panorama.jpg/1200px-Cirrus_sky_panorama.jpg");
}
function setup() {
createCanvas(400, 400);
background(255);
img.resize(0, 200);
var maxRange = 256
colorMode(HSL, maxRange);
image(img, 0, 0);
var histogram = new Array(maxRange);
for (i = 0; i <= maxRange; i++) {
histogram[i] = 0
}
loadPixels();
for (var x = 0; x < img.width; x += 5) {
for (var y = 0; y < img.height; y += 5) {
var loc = (x + y * img.width) * 4;
var h = pixels[loc];
var s = pixels[loc + 1];
var l = pixels[loc + 2];
var a = pixels[loc + 3];
b = int(l);
histogram[b]++
}
}
image(img, 0, 0);
stroke(300, 100, 80)
push()
translate(10, 0)
for (x = 0; x <= maxRange; x++) {
index = histogram[x];
y1 = int(map(index, 0, max(histogram), height, height - 300));
y2 = height
xPos = map(x, 0, maxRange, 0, width - 20)
line(xPos, y1, xPos, y2);
}
pop()
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.js"></script>
But I would need downloadable vector graphic files as results that are closed shapes without any gaps between. It should look like that for example:
<svg viewBox="0 0 399.84 200"><polygon points="399.84 200 399.84 192.01 361.91 192.01 361.91 182.87 356.24 182.87 356.24 183.81 350.58 183.81 350.58 184.74 344.91 184.74 344.91 188.19 339.87 188.19 339.87 189.89 334.6 189.89 334.6 185.29 328.93 185.29 328.93 171.11 323.26 171.11 323.26 172.55 317.59 172.55 317.59 173.99 311.92 173.99 311.92 179.42 306.88 179.42 306.88 182.03 301.21 182.03 301.21 183.01 295.54 183.01 295.54 179.04 289.87 179.04 289.87 175.67 284.21 175.67 284.21 182.03 278.54 182.03 278.54 176 273.5 176 273.5 172.42 267.83 172.42 267.83 179.42 262.79 179.42 262.79 182.03 257.12 182.03 257.12 183.01 251.45 183.01 251.45 178.63 245.78 178.63 245.78 175.21 240.11 175.21 240.11 182.03 234.86 182.03 234.86 150.42 229.2 150.42 229.2 155.98 223.53 155.98 223.53 158.06 217.86 158.06 217.86 167.44 212.19 167.44 212.19 162.58 206.52 162.58 206.52 155.98 200.85 155.98 200.85 158.06 195.18 158.06 195.18 167.44 189.51 167.44 189.51 177.46 183.84 177.46 183.84 166.93 178.17 166.93 178.17 153.69 172.5 153.69 172.5 155.87 166.82 155.87 166.82 158.05 161.78 158.05 161.78 155.63 156.11 155.63 156.11 160.65 150.84 160.65 150.84 146.59 145.17 146.59 145.17 109.63 139.49 109.63 139.49 113.67 133.82 113.67 133.82 61.48 128.15 61.48 128.15 80.59 123.11 80.59 123.11 93.23 117.44 93.23 117.44 97.97 111.76 97.97 111.76 78.07 106.09 78.07 106.09 61.66 100.42 61.66 100.42 93.23 94.75 93.23 94.75 98.51 89.7 98.51 89.7 85.4 84.03 85.4 84.03 111.03 78.99 111.03 78.99 120.57 73.32 120.57 73.32 124.14 67.65 124.14 67.65 23.48 61.97 23.48 61.97 0 56.3 0 56.3 120.57 50.63 120.57 50.63 167.01 45.38 167.01 45.38 170.83 39.71 170.83 39.71 172.26 34.03 172.26 34.03 178.7 28.36 178.7 28.36 175.36 22.69 175.36 22.69 170.83 17.02 170.83 17.02 172.26 11.34 172.26 11.34 178.7 5.67 178.7 5.67 103.85 0 103.85 0 200 399.84 200"/></svg>
Has anyone an idea how to program that? It doesn't necessarily need to be based on p5.js, but would be cool.
Closing Gaps
In order to have a gapless histogram, you need to meet the following condition:
numberOfBars * barWidth === totalWidth
Right now you are using the p5 line() function to draw your bars. You have not explicitly set the width of your bars, so it uses the default value of 1px wide.
We know that the numberOfBars in your code is always maxRange which is 256.
Right now the total width of your histogram is width - 20, where width is set to 400 by createCanvas(400, 400). So the totalWidth is 380.
256 * 1 !== 380
If you have 256 pixels of bars in a 380 pixel space then there are going to be gaps!
We need to change the barWidth and/or the totalWidth to balance the equation.
For example, you can change your canvas size to 276 (256 + your 20px margin) and the gaps disappear!
createCanvas(276, 400);
However this is not an appropriate solution because now your image is cropped and your pixel data is wrong. But actually...it was already wrong before!
Sampling Pixels
When you call the global loadPixels() function in p5.js you are loading all of the pixels for the whole canvas. This includes the white areas outside of your image.
for (var x = 0; x < img.width; x += 5) {
for (var y = 0; y < img.height; y += 5) {
var loc = (x + y * img.width) * 4;
It is a 1-dimensional array, so your approach of limiting the x and y values here is not giving you the correct position. Your loc variable needs to use the width of the entire canvas rather than the width of just the image, since the pixels array includes the entire canvas.
var loc = (x + y * width) * 4;
Alternatively, you can look at just the pixels of the image by using img.loadPixels() and img.pixels.
img.loadPixels();
for (var x = 0; x < img.width; x += 5) {
for (var y = 0; y < img.height; y += 5) {
var loc = (x + y * img.width) * 4;
var h = img.pixels[loc];
var s = img.pixels[loc + 1];
var l = img.pixels[loc + 2];
var a = img.pixels[loc + 3];
b = int(l);
histogram[b]++;
}
}
The pixel values are always returned in RGBA regardless of the colorMode. So your third channel value is actually the blue, not the lightness. You can make use of the p5.js lightness() function to compute the lightness from the RGBA.
Updated Code
The actual lightness histogram looks dumb because 100% dwarfs all of the other bars.
function preload() {
img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Cirrus_sky_panorama.jpg/1200px-Cirrus_sky_panorama.jpg");
}
function setup() {
const barCount = 100;
const imageHeight = 200;
createCanvas(400, 400);
background(255);
colorMode(HSL, barCount - 1);
img.resize(0, imageHeight);
imageMode(CENTER);
image(img, width / 2, imageHeight / 2);
img.loadPixels();
const histogram = new Array(barCount).fill(0);
for (let x = 0; x < img.width; x += 5) {
for (let y = 0; y < img.height; y += 5) {
const loc = (x + y * img.width) * 4;
const r = img.pixels[loc];
const g = img.pixels[loc + 1];
const b = img.pixels[loc + 2];
const a = img.pixels[loc + 3];
const barIndex = floor(lightness([r, g, b, a]));
histogram[barIndex]++;
}
}
fill(300, 100, 80);
strokeWeight(0);
const maxCount = max(histogram);
const barWidth = width / barCount;
const histogramHeight = height - imageHeight;
for (let i = 0; i < barCount; i++) {
const count = histogram[i];
const y1 = round(map(count, 0, maxCount, height, imageHeight));
const y2 = height;
const x1 = i * barWidth;
const x2 = x1 + barWidth;
rect(x1, y1, barWidth, height - y1);
}
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.js"></script>
But the blue channel histogram looks pretty good!
function preload() {
img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Cirrus_sky_panorama.jpg/1200px-Cirrus_sky_panorama.jpg");
}
function setup() {
const barCount = 100;
const imageHeight = 200;
createCanvas(400, 400);
background(255);
img.resize(0, imageHeight);
imageMode(CENTER);
image(img, width / 2, imageHeight / 2);
img.loadPixels();
const histogram = new Array(barCount).fill(0);
for (let x = 0; x < img.width; x += 5) {
for (let y = 0; y < img.height; y += 5) {
const loc = (x + y * img.width) * 4;
const r = img.pixels[loc];
const g = img.pixels[loc + 1];
const b = img.pixels[loc + 2];
const a = img.pixels[loc + 3];
const barIndex = floor(barCount * b / 255);
histogram[barIndex]++;
}
}
fill(100, 100, 300);
strokeWeight(0);
const maxCount = max(histogram);
const barWidth = width / barCount;
const histogramHeight = height - imageHeight;
for (let i = 0; i < barCount; i++) {
const count = histogram[i];
const y1 = round(map(count, 0, maxCount, height, imageHeight));
const y2 = height;
const x1 = i * barWidth;
const x2 = x1 + barWidth;
rect(x1, y1, barWidth, height - y1);
}
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.js"></script>
Just to add to Linda's excellent answer(+1), you can use p5.svg to render to SVG using p5.js:
let histogram;
function setup() {
createCanvas(660, 210, SVG);
background(255);
noStroke();
fill("#ed225d");
// make an array of 256 random values in the (0, 255) range
histogram = Array.from({length: 256}, () => int(random(255)));
//console.log(histogram);
// plot the histogram
barPlot(histogram, 0, 0, width, height);
// change shape rendering so bars appear connected
document.querySelector('g').setAttribute('shape-rendering','crispEdges');
// save the plot
save("histogram.svg");
}
function barPlot(values, x, y, plotWidth, plotHeight){
let numValues = values.length;
// calculate the width of each bar in the plot
let barWidth = plotWidth / numValues;
// calculate min/max value (to map height)
let minValue = min(values);
let maxValue = max(values);
// for each value
for(let i = 0 ; i < numValues; i++){
// map the value to the plot height
let barHeight = map(values[i], minValue, maxValue, 0, plotHeight);
// render each bar, offseting y
rect(x + (i * barWidth),
y + (plotHeight - barHeight),
barWidth, barHeight);
}
}
<script src="https://unpkg.com/p5#1.3.1/lib/p5.js"></script>
<script src="https://unpkg.com/p5.js-svg#1.0.7"></script>
(In the p5 editor (or when testing locally) a save dialog should pop up.
If you use the browser's Developer Tools to inspect the bar chart it should confirm it's an SVG (not <canvas/>))

Dynamically adjust the weighting of columns in a row based on string length

I have a layout with two texts, one on left side and one on right side. If both texts are long, left one should occupy 60%, and right one 40% of the width. But if right text is shorter than 40%, the left one should take all the available space.
Here are the examples:
and:
So I would like to write something like this:
Row {
Text(text = left, modifier = modifier.padding(8.dp).weight(<min 0.6f>))
Text(text = right, modifier = modifier.padding(8.dp).weight(<max 0.4f>))
}
Is there any way to achieve this?
Finally, I figured it out. Here is the modifier:
fun Modifier.maxWidth(
fraction: Float = 1f,
) = layout { measurable, constraints ->
val maxWidth = (constraints.maxWidth * fraction).roundToInt()
val width = measurable.maxIntrinsicWidth(constraints.maxHeight).coerceAtMost(maxWidth)
val placeable = measurable.measure(Constraints(constraints.minWidth, width, constraints.minHeight, constraints.maxHeight))
layout(width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
So we can use it like this:
Row {
Text(text = left, modifier = modifier.padding(8.dp).weight(1f)) // left text uses all the available space
Text(text = right, modifier = modifier.padding(8.dp).maxWidth(fraction = 0.4f)) // right text can be 40% of the parent or less
}
Measure the length of each string and calculate their weights. Adjust the weights based on the maximum allowed for the right column:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
val paint = android.graphics.Paint()
// First row
val text1 = "This long text should take up maximum 60% of the space with wrapping"
val text2 = "This is not very long"
val width1 = paint.measureText(text1)
val width2 = paint.measureText(text2)
var w2 = width2 / (width1 + width2)
if (w2 > 0.4f) {
w2 = 0.4f
}
val w1 = 1f - w2
// Second row
val text3 = "This text should take up more than 60% of the space"
val text4 = "Short"
val width3 = paint.measureText(text3)
val width4 = paint.measureText(text4)
var w4 = width4 / (width3 + width4)
if (w4 > 0.4f) {
w4 = 0.4f
}
val w3 = 1f - w4
setContent {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 20.dp)
) {
Text(text1, modifier = Modifier.weight(w1))
Text(text2, modifier = Modifier.weight(w2), color = Color.Red)
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 20.dp)
) {
Text(text3, modifier = Modifier.weight(w3))
Text(text4, modifier = Modifier.weight(w4), color = Color.Red)
}
}
}
}
}

BSP Dungeon Generator giving an unfixable error

I'm working on translating this ActionScript tutorial on binary space partitioning into Swift so I can use it in my rogue-like game. I came across a hitch.
In the article, the writer initializes his class like so:
public function Leaf(X:int, Y:int, Width:int, Height:int)
{
// initialize our leaf
x = X;
y = Y;
width = Width;
height = Height;
}
When I translated this into Swift, I ran into an error. The code above doesn't initialize all its declared values. This leads me into an impossible error that I can't seem to fix. Somehow, the writer of the article initializes his leftChild and rightChild variables with this function that is outside the initialization scope.
public function split():Boolean
{
// begin splitting the leaf into two children
if (leftChild != null || rightChild != null)
return false; // we're already split! Abort!
// determine direction of split
// if the width is >25% larger than height, we split vertically
// if the height is >25% larger than the width, we split horizontally
// otherwise we split randomly
var splitH:Boolean = FlxG.random() > 0.5;
if (width > height && width / height >= 1.25)
splitH = false;
else if (height > width && height / width >= 1.25)
splitH = true;
var max:int = (splitH ? height : width) - MIN_LEAF_SIZE; // determine the maximum height or width
if (max <= MIN_LEAF_SIZE)
return false; // the area is too small to split any more...
var split:int = Registry.randomNumber(MIN_LEAF_SIZE, max); // determine where we're going to split
// create our left and right children based on the direction of the split
if (splitH)
{
leftChild = new Leaf(x, y, width, split);
rightChild = new Leaf(x, y + split, width, height - split);
}
else
{
leftChild = new Leaf(x, y, split, height);
rightChild = new Leaf(x + split, y, width - split, height);
}
return true; // split successful!
}
Which is somehow ok in ActionScript, but in Swift it leads me to my problem.
Here is my translated code (Swift):
private let mapWidth:Int = 50
private let mapHeight:Int = 50
class Leaf {
var leftLeaf = [Leaf]()
var rightLeaf = [Leaf]()
var minLeafSize:Int = 6
var x, y, width, height: Int
var leftChild:Leaf
var rightChild:Leaf
init (X:Int, Y:Int, W:Int, H:Int) {
x = Y
y = Y
width = W
height = H
let maxLeafSize:UInt = 20
var leaves = [Leaf]()
// first, create a Leaf to be the 'root' of all Leafs.
let root = Leaf(X: 0, Y: 0, W: mapWidth, H: mapHeight)
leaves.append(root)
var didSplit:Bool = true
// we loop through every Leaf in our Vector over and over again, until no more Leafs can be split.
while (didSplit) {
didSplit = false
for l in leaves {
if l.leftLeaf.isEmpty == true && l.rightLeaf.isEmpty == true {
// if this Leaf is too big, or 75% chance...
if l.width > maxLeafSize || l.height > maxLeafSize || Int(arc4random_uniform(100)) > 25 {
if (l.split()) {
// if we did split, push the child leafs to the Vector so we can loop into them next
leaves.append(l.leftChild)
leaves.append(l.rightChild)
didSplit = true
}
}
}
}
}
}
func split() -> Bool {
if leftLeaf.isEmpty == true || rightLeaf.isEmpty == true {
return false
}
var splitH = arc4random_uniform(100) > 50 ? true : false
if width > height && Double(width / height) >= 1.25 {
splitH = false
}
if height > width && Double(height / width) >= 1.25 {
splitH = true
}
let max:Int = (splitH ? height : width) - minLeafSize // determine the maximum height or width
if max <= minLeafSize { return false }
let split:Int = Int(arc4random_uniform(UInt32(minLeafSize - max) + UInt32(max)))
if (splitH) {
leftChild = Leaf(X: x, Y: y, W: width, H: split)
rightChild = Leaf(X: x, Y: y + split, W: width, H: height - split)
leftLeaf.append(leftChild)
rightLeaf.append(rightChild)
} else {
leftChild = Leaf(X: x, Y: y, W: split, H: height)
rightChild = Leaf(X: x + split, Y: y, W: width - split, H: height);
leftLeaf.append(leftChild)
rightLeaf.append(rightChild)
}
return true
}
}
It is identical (as far as I can figure) to the ActionScript code in the article. But it is giving me an error. The leftChild and rightChild variables aren't initialized in my init method. When I move the split() -> Bool function into the init method it won't let me use the function, giving me an error "Value of type Leaf has no member split()". Removing the l from the if (l.spit()) line gives me a second error "Use of local variable 'split' before its declaration". The split() function has to be outside the initialization scope.
If I attempt to initialize leftChild and rightChild like so:
init (X:Int, Y:Int, W:Int, H:Int) {
x = Y
y = Y
width = W
height = H
leftChild = Leaf(X: x, Y: y, W: width, H: height)
rightChild = Leaf(X: x, Y: y, W: width, H: height)
}
It creates an infinite loop that eventually causes a crash.
The code should be initializing leftChild and rightChild in the split() -> Bool function but I don't think that's how it works in Swift. You should be able to copy/paste it into a Swift file and get the same errors.
Why is this happening? Is my code poorly written? How can I fix this?
In ActionScript, uninitialised variables are automatically evaluated with the special value undefined; also, in ActionScript, undefined == null, which is why if (leftChild != null || rightChild != null) works.
In Swift, you need to explicitly allow your variables to be nilable. The variables you are worried about need to start off as nil (which they automatically will, if you allow them to, by setting their type to Optional - note the question mark):
var leftChild:Leaf?
var rightChild:Leaf?

How to coerce num type between double and int

Looking for a cleaner/better way to coerce "num" type based on generic result "T".
Example:
int iCartesianAngleInDegrees=45, _iDistanceBetween=85;
Vector<int> ivectXY=new Vector<int>(10,30);
ivectXY.moveByDegreesAndDistance(iCartesianAngleInDegrees, _iDistanceBetween);
In the moveByDegreesAndDistance method, my kludge checks runtimeType to invoke toInt or toDouble. It just does not seem proper to me. Any suggestions/hints appreciated?
class Vector<T extends num> extends Coordinate<T> {
...
/**
* Advance x/y in Direction(Cartesian Degrees) for a given Distance
* TODO:PerfTest
**/
Vector<T> moveByDegreesAndDistance(T iDegrees, T iDistance) {
//cartesianToCompass(135) = 225
double _dblAngleInRadians = degreesToRadians(iDegrees),
_dblX = iDistance * math.cos(_dblAngleInRadians),
_dblY = iDistance * math.sin(_dblAngleInRadians);
//This will not automatically coerce type
// x += _dblX as T;
// y += _dblY as T;
if (_dblX is T) {
x += _dblX;
y += _dblY;
} else {
if (x.runtimeType == int) {
x += _dblX.toInt();
y += _dblY.toInt();
} else {
x += _dblX.toDouble();
y += _dblY.toDouble();
}
}
return this;
}
}
_dblAngleInRadians, _dblX, _dblY being double you only have to test if iDegrees is an int and use toInt() in this case.
class Vector<T extends num> extends Coordinate<T> {
...
/**
* Advance x/y in Direction(Cartesian Degrees) for a given Distance
* TODO:PerfTest
**/
Vector<T> moveByDegreesAndDistance(T iDegrees, T iDistance) {
//cartesianToCompass(135) = 225
double _dblAngleInRadians = degreesToRadians(iDegrees),
_dblX = iDistance * math.cos(_dblAngleInRadians),
_dblY = iDistance * math.sin(_dblAngleInRadians);
//This will not automatically coerce type
// x += _dblX as T;
// y += _dblY as T;
final isInt = iDegrees is int;
x += isInt ? _dblX.toInt() : _dblX;
y += isInt ? _dblY.toInt() : _dblY;
return this;
}

Resources