How can I calculate and create positions of triangle shape? - c#

private void FormationTriangle()
{
newpositions = new List<Vector3>();
for (int x = 0; x < squadMembers.Count; x++)
{
for (int y = x; y < 2 * (squadMembers.Count - x) - 1; y++)
{
Vector3 position = new Vector3(x, y);
newpositions.Add(position);
}
}
move = true;
formation = Formation.Square;
}
The loops are wrong. It put the squadMembers in one line one above the other.
Not even close to a triangle shape.
I want the squadMembers to stand in a triangle shape.
This is the moving part: But the problem is with the loops calculating the triangle shape positions. Other formations I did are working fine.
private void MoveToNextFormation()
{
if (randomSpeed == false)
{
if (step.Length > 0)
step[0] = moveSpeed * Time.deltaTime;
}
for (int i = 0; i < squadMembers.Count; i++)
{
squadMembers[i].transform.LookAt(newpositions[i]);
if (randomSpeed == true)
{
squadMembers[i].transform.position = Vector3.MoveTowards(
squadMembers[i].transform.position, newpositions[i], step[i]);
}
else
{
squadMembers[i].transform.position = Vector3.MoveTowards(
squadMembers[i].transform.position, newpositions[i], step[0]);
}
if (Vector3.Distance(squadMembers[i].transform.position, newpositions[i]) <
threshold)
{
if (squareFormation == true)
{
Vector3 degrees = new Vector3(0, 0, 0);
Quaternion quaternion = Quaternion.Euler(degrees);
squadMembers[i].transform.rotation = Quaternion.Slerp(
squadMembers[i].transform.rotation, quaternion,
rotateSpeed * Time.deltaTime);
}
else
{
squadMembers[i].transform.rotation = Quaternion.Slerp(
squadMembers[i].transform.rotation, quaternions[i],
rotateSpeed * Time.deltaTime);
}
}
}
}

This answer will produce a triangle arranged like this:
x
x x
x x x
x x x x
x x x x x
Or, if there aren't enough to fill a full triangle:
x
x x
x x x
x x x x
x x x
Since you aren't guaranteed a perfectly triangular number of units, you should overestimate how big your triangle is, keep count of how many units you have placed, and then quit placing them when you reach your limit.
First, find the height of the smallest triangular number greater than your number of units, and that triangular number itself:
int height = Mathf.CeilToInt( (Mathf.Sqrt(8*squadMembers.Count+1f)-1f)/2 )
int slots = (int)(height * (height+1f)/2f)
Then, find the position of the first unit. We need to know how many rows of slots we have and how wide the bottom row of slots is:
float verticalModifier = 0.8f; // 0.8f to decrease vertical space
float horizontalModifier = 1.25f; // 1.25f to increase horizontal space
float width = 0.5f * (height-1f);
Vector3 startPos = new Vector3(width* horizontalModifier, 0f, (float)(height-1f) * verticalModifier);
Then, add until you've added enough
int finalRowCount = height - slots + squadMembers.Count;
for (int rowNum = 0 ; rowNum < height && newpositions.Count < squadMembers.Count; rowNum++) {
for (int i = 0 ; i < rowNum+1 && newpositions.Count < squadMembers.Count ; i++ ) {
float xOffset = 0f;
if (rowNum+1 == height) {
// If we're in the last row, stretch it ...
if (finalRowCount !=1) {
// Unless there's only one item in the last row.
// If that's the case, leave it centered.
xOffset = Mathf.Lerp(
rowNum/2f,
-rowNum/2f,
i/(finalRowCount-1f)
) * horizontalModifier;
}
}
else {
xOffset = (i-rowNum /2f) * horizontalModifier;
}
float yOffset = (float)rowNum * verticalModifier;
Vector3 position = new Vector3(
startPos.x + xOffset, 0f, startPos.y - yOffset);
newpositions.Add(position);
}
}

Let's see what the list of positions contains for a simple value, n = 3
First, loop x from 0 to 2 (3 - 1)
Then for each x, loop from x to 4-x (3*2 - x - 1 - 1)
Remembering that a<b is the same as a<=b-1
That gives us...
0,0
0,1
0,2
0,3
0,4
1,1
1,2
1,3
2,2
Which is a lot of positions. Certainly more than 3 units can occupy! At least it is a triangle:
X\Y 0 1 2 3 4
0 # # # # #
1 # # #
2 #
The main problem is that you're generating way more positions than needed and expecting to fill it somehow.
You need to calculate your width and height based on the area formula for a triangle: A = (b*h)/2 and you may even want b=h, where A = number of units.
So, something like this:
int b = Mathf.CeilToInt(Mathf.Sqrt(squadMembers.Count));
for (int x = 0; x < b; x++)
{
//the divide by 2 is accounted for with this 2*
for (int y = x; y < 2 * (b - x) - 1; y++)
{
Vector3 position = new Vector3(x, y);
newpositions.Add(position);
}
}

Related

How can i find the middle of a Hexgrid?

So ive created a code which will use my Standard hexagon Node from Godot and istance it for a given size into a hexgrid, now i wanted to find the exact middle of this Hexgrid to position my player there eg. move the Nodes along the X and Z axis to the standard position of the map 0,0,0.
I thought the exact middle of the Hexgrid is Axis X - Size of the X Axis / 2, Axis Z - Size of the Y Axis / 2.
But this just doesnt seem to work. Any suggestions on how to finde the middle of the Hexgrid and how to move it along the axis?
The Code for the middle looks like this:
public void GenerateGrid()
{
for (int y = 0; y < gridSize.Y; y++)
{
for (int x = 0; x < gridSize.X; x++)
{
Vector3 newPos = GetPositionFromCurrentHex(new Vector2(x, y));
Node3D generator_scene = (Node3D) ResourceLoader.Load<PackedScene>(tilePath).Instantiate();
generator_scene.Position = new Vector3(newPos.X - (gridSize.X / 2.0f), newPos.Y, newPos.Z - (gridSize.Y/ 2.0f));
if (isFlatTopped)
{
generator_scene.RotateY(Mathf.DegToRad(90));
}
AddChild(generator_scene);
}
}
}
And for the Calculation of the Hexgrid:
public Vector3 GetPositionFromCurrentHex(Vector2 coordinate)
{
float col = coordinate.X;
float row = coordinate.Y;
float width;
float height;
float xPos;
float yPos;
bool shouldOffset;
float horizontalDistance;
float verticalDistance;
float offset;
float size = outerSize;
if (!isFlatTopped)
{
shouldOffset = (row % 2) == 0;
width = Mathf.Sqrt(3) * size;
height = 2f * size;
horizontalDistance = width;
verticalDistance = height * (3f / 4f);
offset = (shouldOffset) ? width / 2 : 0;
xPos = (col * (horizontalDistance)) + offset;
yPos = (row * verticalDistance);
}
else
{
shouldOffset = (col % 2) == 0;
width = 2f * size;
height = Mathf.Sqrt(3f) * size;
horizontalDistance = width * (3f / 4f);
verticalDistance = height;
offset = (shouldOffset) ? height / 2 : 0;
xPos = (col * (horizontalDistance));
yPos = (row * verticalDistance) - offset;
}
return new Vector3(xPos, 0, yPos);
}
Answer depends on how you define center in different cases, for example
Is your hexgrid flat top or pointy top?
Is the topmost row of your hexgrid also leftmost or not (offset of rows)
Do you expect your "center" of the hexgrid to be also center of an hexagon, or it can be a point along edge of some hexagons.
If the number of rows and columns are odd, you can get away with calculating the coordinates of 4 corner points (topleft, topright, botleft, botright) and average them. If your grid is pointy-top this is also center of the middle hexagon, if your grid is flat-top it is on the edge between two hexagons.

Raycast not capturing all Vector coordinates

I have a gameobject that occupies the whole screen just for testing purposes. I'm drawing a line btw. What I'm trying to achieve is if the mouse position hits a gameobject it will store the vector2 coordinates in a list. But raycast is not storing all the coordinates. Below is my code
private void Update()
{
if (Input.GetMouseButton(0))
{
Vector2 mousePos = Input.mousePosition;
Vector2 Pos = _camera.ScreenToWorldPoint(mousePos);
if(!mousePositions.Contains(Pos))
mousePositions.Add(Pos);
if (Physics.Raycast(Camera.main.ScreenPointToRay(mousePos), out RaycastHit hit))
{
Vector2 textureCoord = hit.textureCoord;
int pixelX = (int)(textureCoord.x * _templateDirtMask.width);
int pixelY = (int)(textureCoord.y * _templateDirtMask.height);
Vector2Int paintPixelPosition = new Vector2Int(pixelX, pixelY);
if (!linePositions.Contains(paintPixelPosition))
linePositions.Add(paintPixelPosition);
foreach (Vector2Int pos in linePositions)
{
int pixelXOffset = pos.x - (_brush.width / 2);
int pixelYOffset = pos.y - (_brush.height / 2);
for (int x = 0; x < _brush.width; x++)
{
for (int y = 0; y < _brush.height; y++)
{
_templateDirtMask.SetPixel(
pixelXOffset + x,
pixelYOffset + y,
Color.black
);
}
}
}
_templateDirtMask.Apply();
}
}
}
Everytime I checked the element count mousePositions are always greater than linePositions. I don't know what's causing this
the element count mousePositions are always greater than linePosition
well it is quite simple: In
int pixelX = (int)(textureCoord.x * _templateDirtMask.width);
int pixelY = (int)(textureCoord.y * _templateDirtMask.height);
you are casting to int and cut off any decimals after the comma (basically like doing Mathf.FloorToInt).
So you can totally have multiple mouse positions which result in float pixel positions like e.g.
1.2, 1.2
1.4, 1.7
1.02, 1.93
...
all these will map to
Vector2Int paintPixelPosition = new Vector2Int(1, 1);
Besides, you might want to look at some better line drawing algorithms like e.g. this simple one
And then note that calling SetPixel repeatedly is quite expensive. You want to do a single SetPixels call like e.g.
var pixels = _templateDirtMask.GetPixels();
foreach (Vector2Int pos in linePositions)
{
int pixelXOffset = pos.x - (_brush.width / 2);
int pixelYOffset = pos.y - (_brush.height / 2);
for (int x = 0; x < _brush.width; x++)
{
for (int y = 0; y < _brush.height; y++)
{
pixels[(pixelXOffset + x) + (pixelYOffset + y) * _templateDirtMask.width] = Color.black;
}
}
}
_templateDirtMask.SetPixels(pixels);
_templateDirtMask.Apply();
It happens because there is really could be a case, when several elements from mousePositions are associated with one elment from linePositions.
Rough example: your texture resolution is only 1x1px. In this case you linePositons will contain only one element. And this element will be associated with all elements from mosePositions.
So, relation of the number of elements in these lists depends on relation of your texture and screen resolutions.

How to create a 3D mesh from a heightmap represented as a float array

I am attempting to understand how a 3D mesh has been constructed from a heightmap stored as a one dimensional float array. The only examples I have seen previously have made use of a 2D float array, and I am struggling to wrap my head around the math involved here. Any insight into it would be appreciated. I have commented the code which I do not quite understand yet for your convenience.
Source of code: https://github.com/SebLague/Hydraulic-Erosion
public void ContructMesh () {
Vector3[] verts = new Vector3[mapSize * mapSize];
int[] triangles = new int[(mapSize - 1) * (mapSize - 1) * 6];
int t = 0;
//Note that default mapSize is 255
for (int i = 0; i < mapSize * mapSize; i++) {
//Following code is not properly understood
int x = i % mapSize;
int y = i / mapSize;
int meshMapIndex = y * mapSize + x;
Vector2 percent = new Vector2 (x / (mapSize - 1f), y / (mapSize - 1f));
Vector3 pos = new Vector3 (percent.x * 2 - 1, 0, percent.y * 2 - 1) * scale;
pos += Vector3.up * map[meshMapIndex] * elevationScale; //Elevation scale is 20 by default
verts[meshMapIndex] = pos;
//End of misunderstood code
if (x != mapSize - 1 && y != mapSize - 1) {
t = (y * (mapSize - 1) + x) * 3 * 2;
triangles[t + 0] = meshMapIndex + mapSize;
triangles[t + 1] = meshMapIndex + mapSize + 1;
triangles[t + 2] = meshMapIndex;
triangles[t + 3] = meshMapIndex + mapSize + 1;
triangles[t + 4] = meshMapIndex + 1;
triangles[t + 5] = meshMapIndex;
t += 6;
}
}
Mesh mesh = new Mesh();
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
mesh.vertices = verts;
mesh.triangles = triangles;
mesh.RecalculateNormals ();
What specifically do you not understand?
int x = i % mapSize; // Get the x location of the current point
int y = i / mapSize; // Get the y location of the current point
// This should be equal to i, IDK why this is even calculated
int meshMapIndex = y * mapSize + x;
// How far along either direction are we?
Vector2 percent = new Vector2 (x / (mapSize - 1f), y / (mapSize - 1f));
// Make a new vector that scales the X and Y coordinates up.
// The Y coordinate is set to the Z element in this vector
// Presumably because whatever you use to render uses the Y element as "up"
// And the X-Z plane is the horizontal plane
// Also normalize X and Z to lie between -1*scale and 1*scale
Vector3 pos = new Vector3 (percent.x * 2 - 1, 0, percent.y * 2 - 1) * scale;
// Add the value at the current index, times the scale, as the Y element of pos
pos += Vector3.up * map[meshMapIndex] * elevationScale; //Elevation scale is 20 by default
// The X-Z values of pos give you the location of the vertex in the horizontal plane
// The Y value of pos gives you the height
// save the newly calculated pos in verts
verts[meshMapIndex] = pos;

Replacing hexagons with destroyed hexagons

I want to replace the destroyed hexagons with other standing hexagons. Existing hexagons should fall from the top. For example if I destroy (0,2) positioned hexagon in the picture below, the top left hexagon which position of that hexagon is (0,0) should be moved to (0,2) position and i should create a new hexagon and put it on (0,0) which is empty now because we moved the hexagon on (0,0) to (0,2) earlier.
I have a two dimensional array that stores all the references of hexagons with an index of the hexagon's coordinate (x,y).
--IMPORTANT--
Moving the objects is not important. The important part is we have to know which hexagon will be replaced with another. We have to tell the ARRAY that we changed those hexagons and the hexagons that were just moved or created should have exactly one reference in the index of their new (x,y) positions.
VIDEO FOR BETTER EXPLAINING WHAT I WANT TO DO
https://www.youtube.com/watch?v=QYhq0qwFmmY
Any ideas or help would be appreciated!
Hexagon Coordinate system (ignore red arrows)
public void CreateGrid(int gridWidth, int gridHeight)
{
for (int y = 0; y < gridHeight; y++)
{
for (int x = 0; x < gridWidth; x++)
{
GameObject Hexagon = Instantiate(HexagonPre, Vector2.zero, Quaternion.identity, HexGrid);
int RandColor = Random.Range(0, 5);
if (RandColor == 0)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.blue;
}
else if (RandColor == 1)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.red;
}
else if (RandColor == 2)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.green;
}
else if (RandColor == 3)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.yellow;
}
else if (RandColor == 4)
{
Hexagon.GetComponent<SpriteRenderer>().color = Color.cyan;
}
Vector2 gridPos = new Vector2(x, y);
Hexagon.transform.position = CalcWorldPos(gridPos);
Hexagon.GetComponent<HexCoordinates>().Coordinates = new Vector2Int(x, y);
Hexagon.transform.name = "X: " + x + " | Y: " + y;
}
}
}
Code for destroying hexagons
if (MatchedColors == 2)
{
if(!HexToBeDestroyed.Contains(Hexagons[x, y].gameObject))
HexToBeDestroyed.Add(Hexagons[x, y].gameObject);
if (!HexToBeDestroyed.Contains(Hexagons[x - 1, y].gameObject))
HexToBeDestroyed.Add(Hexagons[x - 1, y].gameObject);
if (!HexToBeDestroyed.Contains(Hexagons[x - 1, y - 1].gameObject))
HexToBeDestroyed.Add(Hexagons[x - 1, y - 1].gameObject);
}
MatchedColors = 0;
}
}
}
}
foreach (GameObject G in HexToBeDestroyed)
{
if (G != null)
{
Destroy(G.gameObject);
}
}
Explanation for code is in comments:
void HexagonFall(GameObject[,] hexArray)
{
// Handle fall for base columns and for offset columns
for (int offset = 0 ; offset < 2 ; offset++)
{
// Handle fall for each column at current offset
for (int x = 0 ; x < hexArray.GetLength(0) ; x++)
{
int bottomYIndex = hexArray.GetLength(1) - offset - 1;
// List of indices of where each hexagon in that column will come from.
// We will fill from bottom to top.
List<Vector2Int> sourceIndices = new List<Vector2Int>();
for (int y = bottomYIndex ; y >= 0 ; y-=2)
{
// HexExists returns true if the hex isn't empty.
// Something along the lines of ` return input!=null; `
// depending on what "empty" hexes look like in the array
if (HexExists(hexArray[x,y]))
{
sourceIndices.Add(new Vector2Int(x,y));
}
}
// We have a list of where to get each bottom hexes from, now do the move/create
for (int y = bottomYIndex; y >= 0 ; y-=2)
{
if (sourceIndices.Count > 0)
{
// If we have any available hexes in column,
// use the bottommost one (at index 0)
hexArray[x,y] = hexArray[sourceIndices[0].x, sourceIndices[0].y];
// We have now found a home for hex previously at sourceIndices[0].
// Remove that index from list so hex will stay put.
sourceIndices.RemoveAt(0);
}
else
{
// Otherwise, we need to generate a new hex
hexArray[x,y] = MakeNewHexAt(new Vector2Int(x,y));
}
// Tell the hex about its new home
hexArray[x,y].GetComponent<HexCoordinates>().Coordinates = new Vector2Int(x, y);
hexArray[x,y].transform.name = "X: " + x + " | Y: " + y;
}
}
}
}
In your hex destroying code, I would change HexToBeDestroyed to be a List of Vector2Int so you can set the array references to null immediately when you Destroy the gameobject:
List<Vector2Int> HexToBeDestroyed = new List<Vector2Int>();
// ...
if (MatchedColors == 2)
{
if(!HexToBeDestroyed.Contains(new Vector2Int(x, y))
HexToBeDestroyed.Add(new Vector2Int(x, y));
if (!HexToBeDestroyed.Contains(new Vector2Int(x - 1, y))
HexToBeDestroyed.Add(new Vector2Int(x - 1, y));
if (!HexToBeDestroyed.Contains(new Vector2Int(x - 1, y - 1)))
HexToBeDestroyed.Add(new Vector2Int(x - 1, y - 1));
}
// ...
foreach (Vector2Int V in HexToBeDestroyed)
{
if (Hexagons[V.x,V.y] != null)
{
Destroy(Hexagons[V.x,V.y]);
Hexagons[V.x,V.y] = null;
}
}
As far as moving the hexes goes, I would add this in the Update of HexCoordinates:
float fallSpeed = 0.5f;
Vector2 goalWorldPosition = GS.CalcWorldPos(Coordinates);
transform.position = Vector2.MoveTowards(transform.position, goalWorldPosition, fallSpeed * Time.deltaTime);

GDI+ curve "overflowing"

I'm currently using GDI+ to draw a line graph, and using Graphics.DrawCurve to smooth out the line. The problem is that the curve doesn't always match the points I feed it, and that makes the curve grow out of the graph frame in some points, as seen below(red is Graphics.DrawLines, green is Graphics.DrawCurve).
How would I go about solving this?
The simplest solution is to set a tension:
The green curve is drawn with the default tension, the blue one set a tension of 0.1f:
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.DrawLines(Pens.Red, points.ToArray());
e.Graphics.DrawCurve(Pens.Green, points.ToArray());
e.Graphics.DrawCurve(Pens.Blue, points.ToArray(), 0.1f);
}
You will need to test what is the best compromise, 0.2f is still ok, 0.3f is already overdrawing quite a bit..
For a really good solution you will need to use DrawBeziers. This will let you draw curves that can go through the points without any overdrawing and with full control of the radius of the curves; but to to so you will need to 'find', i.e. calculate good control points, which is anything but trivial..:
This result is by no means perfect but already complicated enough.. I have displayed the curve points and their respective control points in the same color. For each point there is an incoming and an outgoing control point. For a smooth curve they need to have the same tangents/gradients in their curve points.
I use a few helper functions to calculate a few things about the segments:
A list of gradients
A list of signs of the gradients
A list of segment lengths
Lists of horizontal and of vertical gaps between points
The main function calculates the array of bezier points, that is the curve points and between each pair the previous left and the next right control points.
In the Paint event it is used like this:
List<PointF> bezz = getBezz(points);
using (Pen pen = new Pen(Color.Black, 2f))
e.Graphics.DrawBeziers(pen, bezz.ToArray());
Here are the functions I used:
List<float> getGradients(List<PointF> p)
{
List<float> grads = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
if (dx == 0) grads.Add(dy == 0 ? 0 : dy > 0 ?
float.PositiveInfinity : float.NegativeInfinity);
else grads.Add(dy / dx);
}
return grads;
}
List<float> getLengths(List<PointF> p)
{
List<float> lengs = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
lengs.Add((float)Math.Sqrt(dy * dy + dx * dx));
}
return lengs;
}
List<float> getGaps(List<PointF> p, bool horizontal)
{
List<float> gaps = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
gaps.Add(horizontal ? dx : dy);
}
return gaps;
}
List<int> getSigns(List<float> g)
{
return g.Select(x => x > 0 ? 1 : x == 0 ? 0 : -1).ToList();
}
And finally the main function; here I make a distinction: Extreme points ( minima & maxima) should have their control points on the same height as the points themselves. This will prevent vertical overflowing. They are easy to find: The signs of their gradients will always altenate.
Other points need to have the same gradient for incoming and outcoming control points. I use the average between the segments' gradients. (Maybe a weighed average would be better..) And I weigh their distance according to the segment lengths..
List<PointF> getBezz(List<PointF> points)
{
List<PointF> bezz = new List<PointF>();
int pMax = points.Count;
List<float> hGaps = getGaps(points, true);
List<float> vGaps = getGaps(points, false);
List<float> grads = getGradients(points);
List<float> lengs = getLengths(points);
List<int> signs = getSigns(grads);
PointF[] bezzA = new PointF[pMax * 3 - 2];
// curve points
for (int i = 0; i < pMax; i++) bezzA[i * 3] = points[i];
// left control points
for (int i = 1; i < pMax; i++)
{
float x = points[i].X - hGaps[i - 1] / 2f;
float y = points[i].Y;
if (i < pMax - 1 && signs[i - 1] == signs[i])
{
float m = (grads[i-1] + grads[i]) / 2f;
y = points[i].Y - hGaps[i-1] / 2f * m * vGaps[i-1] / lengs[i-1];
}
bezzA[i * 3 - 1] = new PointF(x, y);
}
// right control points
for (int i = 0; i < pMax - 1; i++)
{
float x = points[i].X + hGaps[i] / 2f;
float y = points[i].Y;
if (i > 0 && signs[i-1] == signs[i])
{
float m = (grads[i-1] + grads[i]) / 2f;
y = points[i].Y + hGaps[i] / 2f * m * vGaps[i] / lengs[i];
}
bezzA[i * 3 + 1] = new PointF(x, y);
}
return bezzA.ToList();
}
Note that I didn't code for the case of points with the same x-coordinate. So this is ok for 'functional graphs' but not for, say figures, like e.g. stars..
Maybe you just want to look at the "overshooting the bounds" problem as not a problem with the overshoot, but with the bounds. In which case, you can determine the actual bounds of a curve using the System.Drawing.Drawing2D.GraphicsPath object:
GraphicsPath gp = new GraphicsPath();
gp.AddCurve(listOfPoints);
RectangleF bounds = gp.GetBounds();
You can draw that GraphicsPath directly:
graphics.DrawPath(Pens.Black, gp);
As far as solving the bounds problem, the line necessarily overshoots the vertex on some axis. It's easier to see this fact when the lines are aligned to the bounds.
Given these points:
In order for them to be curved, they must exceed their bounds in some way:
If you never want to exceed their vertical bounds, you could simply ensure that the bezier handles have the same Y value as the vertex, but they will overshoot on the X:
Or vice-versa:
You could deliberately undershoot just enough to avoid the way curves can overshoot. This can be done by swapping the bezier handles, which would maybe be at the line-centers, with the vertices:

Categories