Skip to content

Commit

Permalink
editor: Stage UI for N-Slicing
Browse files Browse the repository at this point in the history
This PR adds some stage UI for editing an NSlicer, including adding axis and changing tile modes. The inspect panel's UI still needs some work.

### Structures added
1. I added an NSlicer Editor that takes after vertexEditor. You can only edit axis when the NSlicer is solo'ed. One behavior that's different is that if an axis/tile is selected, escaping won't immediate escape the solo mode. Instead, it'd just deselect everything.
2. The StageNSlicerAxis represents an Axis component. However, StageNSlicerAddAxis has no component.
3. It's just an UI that kind of looks like a StageNSlicerAxis but when dragged, it immediately creates an StageNSlicerAxis.
4. StageNSlicerTileMode isn't backed up by a component by default, but is manually initialized if there happens to be an NSlicerTileMode in core. So in a way, these three stage items all are slightly different.

### A note on creating bounds
The trickiest part to this PR imo is making sure the bounds of the axes and the tiles are calculated correctly. StageNSlicerAxis and StageNSlicerTileMode both use the image's world transform and the artboard's world transform to calculate its space.

When dragging an axis, the mouse diff in image space isn't necessarily how much to change the Axis.offset by. [Here's a thread](https://2dimensions.slack.com/archives/C07D9AE1ZRV/p1725582977896249) that explains the problem. When an axis is dragged, other stored axis will also move their screen position. This data is represented in a new editor-only field called `offsetInLocalSpace`, and is updated by NSlicer on dirt.

There's one hack with the AABB. You can find it be searching 'hack'. Details [in this slack thread](https://2dimensions.slack.com/archives/C07HQ4GS0BH/p1726089308629329).

### Some edge cases
1. I made sure to handle tiling for axes that are very close to each other (or even 0). Previously I think it'd run past 1m iterations. Now, I make it early exit and not bother tiling for really small patches. I wrote a test for this.
2. I also handled a case where if all axes are paired up to be the same, then we still try to keep the fixed patches fixed, but stretch the zero patches so the bounds of the image stay the same (find 'emptyScalableSize' for this logic). This doesn't really serve a practical purpose. Another way to handle it could be to treat it as if there's nothing set so it'd just be the unscaled image, but I thought this implementation looks better.

Demos for these edge cases, also demo'ing it works in Runtime.
![handle-zeros](https://github.com/user-attachments/assets/4fe6dfef-caa3-4abd-b89d-7b59012903ff)

TODO: https://www.notion.so/rive-app/N-Slicing-10622b943b2f4a8e90a78af7f1e0be07
More on N-slicing: https://www.notion.so/rive-app/N-Slice-Tech-Proposal-Image-only-50b25ea8e79c4efabb681110e288f064

### Demos:
Adding an axis
![aaapr-stage-add-axis](https://github.com/user-attachments/assets/5a12def9-802b-4a83-90a6-0b4cf97e8668)

Deleting via backspace
![aaapr-stage-delete](https://github.com/user-attachments/assets/5e608321-12aa-424b-9152-4fd50cdddf55)

Dragging an axis
![aaapr-stage-shadow-line](https://github.com/user-attachments/assets/375ee90a-a867-4f75-b8f6-4f1b068a43f8)

Toggling tile modes
![aaapr-stage-toggle-tile-modes](https://github.com/user-attachments/assets/d593abb1-d206-43b1-a480-bd0de97c2ec1)

Diffs=
95e58ca40 editor: Stage UI for N-Slicing (#8136)

Co-authored-by: Susan Wang <[email protected]>
  • Loading branch information
susan101566 and susan101566 committed Sep 16, 2024
1 parent 6e9a2a5 commit 72174ab
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5719c566261148150e788c80a9672e940baa0542
95e58ca40da30dc4451a826c6e3b086b95fb05cf
80 changes: 64 additions & 16 deletions src/shapes/slice_mesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ void SliceMesh::updateBuffers()

std::vector<float> SliceMesh::uvStops(AxisType forAxis)
{
float imageSize =
forAxis == AxisType::X ? m_nslicer->image()->width() : m_nslicer->image()->height();
float imageScale = std::abs(forAxis == AxisType::X ? m_nslicer->image()->scaleX()
: m_nslicer->image()->scaleY());
if (imageSize == 0 || imageScale == 0)
{
return {};
}

const std::vector<Axis*>& axes = (forAxis == AxisType::X) ? m_nslicer->xs() : m_nslicer->ys();
std::vector<float> result = {0.0};
for (const Axis* axis : axes)
Expand All @@ -137,9 +146,6 @@ std::vector<float> SliceMesh::uvStops(AxisType forAxis)
}
else
{
float imageSize =
forAxis == AxisType::X ? m_nslicer->image()->width() : m_nslicer->image()->height();
imageSize = std::max(1.0f, imageSize);
result.emplace_back(math::clamp(axis->offset() / imageSize, 0, 1));
}
}
Expand All @@ -155,27 +161,57 @@ std::vector<float> SliceMesh::vertexStops(const std::vector<float>& normalizedSt
Image* image = m_nslicer->image();
float imageSize = forAxis == AxisType::X ? image->width() : image->height();
float imageScale = std::abs(forAxis == AxisType::X ? image->scaleX() : image->scaleY());
if (imageSize == 0 || imageScale == 0)
{
return {};
}

auto infinity = std::numeric_limits<float>::infinity();
float fixedPct = 0.0;
for (int i = 0; i < normalizedStops.size() - 1; i++)
int numEmptyScaledPatch = 0;
for (int i = 0; i < (int)normalizedStops.size() - 1; i++)
{
float range = normalizedStops[i + 1] - normalizedStops[i];
if (isFixedSegment(i))
{
fixedPct += normalizedStops[i + 1] - normalizedStops[i];
fixedPct += range;
}
else
{
numEmptyScaledPatch += (range == 0);
}
}

float fixedSize = fixedPct * imageSize;
float scalableSize = std::max(1.0f, imageSize - fixedSize);
float scale = (imageSize * imageScale - fixedSize) / scalableSize;
float scalableSize = imageSize - fixedSize;

float scaleFactor =
scalableSize == 0 ? infinity : (imageSize * imageScale - fixedSize) / scalableSize;
float emptyScalableSize =
numEmptyScaledPatch == 0 ? 0 : ((imageSize - fixedSize / imageScale) / numEmptyScaledPatch);

std::vector<float> result;
float cur = 0.0;
for (int i = 0; i < normalizedStops.size() - 1; i++)
for (int i = 0; i < (int)normalizedStops.size() - 1; i++)
{
result.emplace_back(cur);
float segmentSize = imageSize * (normalizedStops[i + 1] - normalizedStops[i]);
cur += segmentSize * (isFixedSegment(i) ? 1 : scale) / imageScale;
float segment = imageSize * (normalizedStops[i + 1] - normalizedStops[i]);
if (isFixedSegment(i))
{
cur += segment / imageScale;
}
else
{
if (scalableSize == 0)
{
cur += emptyScalableSize;
}
else
{
cur += segment * scaleFactor / imageScale;
}
}
cur = math::clamp(cur, 0, imageSize);
}
result.emplace_back(cur);
return result;
Expand All @@ -200,14 +236,27 @@ uint16_t SliceMesh::tileRepeat(std::vector<SliceMeshVertex>& vertices,

// The size of each repeated tile in image space
Image* image = m_nslicer->image();
const float sizeX = image->width() * (endU - startU) / std::abs(image->scaleX());
const float sizeY = image->height() * (endV - startV) / std::abs(image->scaleY());
float scaleX = std::abs(image->scaleX());
float scaleY = std::abs(image->scaleY());

if (scaleX == 0 || scaleY == 0)
{
return 0;
}

const float sizeX = image->width() * (endU - startU) / scaleX;
const float sizeY = image->height() * (endV - startV) / scaleY;

if (std::abs(sizeX) < 1 || std::abs(sizeY) < 1)
{
return 0;
}

float curX = startX;
float curY = startY;
int curV = start;

int escape = 1000000; // a million
int escape = 1000;
while (curY < endY && escape > 0)
{
escape--;
Expand Down Expand Up @@ -264,7 +313,6 @@ uint16_t SliceMesh::tileRepeat(std::vector<SliceMeshVertex>& vertices,
}
curY += sizeY;
}
assert(escape > 0);
return curV - start;
}

Expand All @@ -282,9 +330,9 @@ void SliceMesh::calc()

std::vector<SliceMeshVertex> vertices;
uint16_t vertexIndex = 0;
for (int patchY = 0; patchY < vs.size() - 1; patchY++)
for (int patchY = 0; patchY < (int)vs.size() - 1; patchY++)
{
for (int patchX = 0; patchX < us.size() - 1; patchX++)
for (int patchX = 0; patchX < (int)us.size() - 1; patchX++)
{
auto tileModeIt = tileModes.find(m_nslicer->patchIndex(patchX, patchY));
auto tileMode =
Expand Down

0 comments on commit 72174ab

Please sign in to comment.