|
8 | 8 | #include <rive/shapes/paint/color.hpp> |
9 | 9 | #include <rive/shapes/paint/fill.hpp> |
10 | 10 | #include <rive/shapes/image.hpp> |
| 11 | +#include <rive/layout_component.hpp> |
11 | 12 | #include <rive/shapes/paint/solid_color.hpp> |
12 | 13 | #include <rive/text/text_value_run.hpp> |
13 | 14 | #include <rive/custom_property_number.hpp> |
@@ -427,3 +428,152 @@ TEST_CASE("Image fit & alignment images 3", "[silver]") |
427 | 428 |
|
428 | 429 | CHECK(silver.matches("image_fit_alignment_3")); |
429 | 430 | } |
| 431 | + |
| 432 | +TEST_CASE("Image fit & alignment with image scaling", "[silver]") |
| 433 | +{ |
| 434 | + rive::SerializingFactory silver; |
| 435 | + auto file = |
| 436 | + ReadRiveFile("assets/image_fit_alignment_updated_test.riv", &silver); |
| 437 | + |
| 438 | + auto artboard = file->artboardNamed("Main"); |
| 439 | + REQUIRE(artboard != nullptr); |
| 440 | + silver.frameSize(artboard->width(), artboard->height()); |
| 441 | + |
| 442 | + auto stateMachine = artboard->stateMachineAt(0); |
| 443 | + int viewModelId = artboard.get()->viewModelId(); |
| 444 | + |
| 445 | + auto vmi = viewModelId == -1 |
| 446 | + ? file->createViewModelInstance(artboard.get()) |
| 447 | + : file->createViewModelInstance(viewModelId, 0); |
| 448 | + |
| 449 | + stateMachine->bindViewModelInstance(vmi); |
| 450 | + stateMachine->advanceAndApply(0.1f); |
| 451 | + |
| 452 | + auto renderer = silver.makeRenderer(); |
| 453 | + artboard->draw(renderer.get()); |
| 454 | + |
| 455 | + int frames = 60; |
| 456 | + for (int i = 0; i < frames; i++) |
| 457 | + { |
| 458 | + silver.addFrame(); |
| 459 | + stateMachine->advanceAndApply(0.016f); |
| 460 | + artboard->draw(renderer.get()); |
| 461 | + } |
| 462 | + |
| 463 | + CHECK(silver.matches("image_fit_alignment_updated_test")); |
| 464 | +} |
| 465 | + |
| 466 | +// The image_fit_alignment asset stores a non-default scaleX/scaleY on its |
| 467 | +// layout images. Pre-7.2 files overwrite that with the layout fit (the stored |
| 468 | +// scale is ignored). 7.2+ files keep it as the user scale and compose it on top |
| 469 | +// of the fit. We patch the file header's minor version to exercise both paths |
| 470 | +// from the same asset. |
| 471 | +TEST_CASE("Layout image composes user scale on top of fit for 7.2 files", |
| 472 | + "[assets]") |
| 473 | +{ |
| 474 | + auto legacyBytes = ReadFile("assets/image_fit_alignment.riv"); |
| 475 | + REQUIRE(legacyBytes.size() > 6); |
| 476 | + REQUIRE(legacyBytes[0] == 'R'); |
| 477 | + REQUIRE(legacyBytes[1] == 'I'); |
| 478 | + REQUIRE(legacyBytes[2] == 'V'); |
| 479 | + REQUIRE(legacyBytes[3] == 'E'); |
| 480 | + // Major/minor are single-byte varuints here. |
| 481 | + REQUIRE(legacyBytes[4] == 7); |
| 482 | + REQUIRE(legacyBytes[5] < 2); |
| 483 | + |
| 484 | + auto modernBytes = legacyBytes; |
| 485 | + modernBytes[5] = 2; // bump the minor version to 7.2 |
| 486 | + |
| 487 | + auto xAxisScale = [](const rive::Mat2D& m) { |
| 488 | + return rive::Vec2D(m[0], m[1]).length(); |
| 489 | + }; |
| 490 | + |
| 491 | + // Loads the file, binds its view model, and holds the file/artboard/state |
| 492 | + // machine alive plus the layout images in artboard order (identical between |
| 493 | + // the two files since they share bytes). |
| 494 | + struct Loaded |
| 495 | + { |
| 496 | + rive::rcp<rive::File> file; |
| 497 | + std::unique_ptr<rive::ArtboardInstance> artboard; |
| 498 | + std::unique_ptr<rive::StateMachineInstance> stateMachine; |
| 499 | + std::vector<rive::Image*> images; |
| 500 | + }; |
| 501 | + auto load = [](std::vector<uint8_t>& bytes, |
| 502 | + rive::Factory* factory) -> Loaded { |
| 503 | + Loaded loaded; |
| 504 | + rive::ImportResult result; |
| 505 | + loaded.file = rive::File::import(bytes, factory, &result); |
| 506 | + REQUIRE(result == rive::ImportResult::success); |
| 507 | + loaded.artboard = loaded.file->artboardNamed("Main"); |
| 508 | + REQUIRE(loaded.artboard != nullptr); |
| 509 | + loaded.stateMachine = loaded.artboard->stateMachineAt(0); |
| 510 | + REQUIRE(loaded.stateMachine != nullptr); |
| 511 | + int viewModelId = loaded.artboard->viewModelId(); |
| 512 | + auto vmi = |
| 513 | + viewModelId == -1 |
| 514 | + ? loaded.file->createViewModelInstance(loaded.artboard.get()) |
| 515 | + : loaded.file->createViewModelInstance(viewModelId, 0); |
| 516 | + loaded.stateMachine->bindViewModelInstance(vmi); |
| 517 | + loaded.stateMachine->advanceAndApply(0.1f); |
| 518 | + for (auto image : loaded.artboard->objects<rive::Image>()) |
| 519 | + { |
| 520 | + if (image->parent() != nullptr && |
| 521 | + image->parent()->is<rive::LayoutComponent>()) |
| 522 | + { |
| 523 | + loaded.images.push_back(image); |
| 524 | + } |
| 525 | + } |
| 526 | + return loaded; |
| 527 | + }; |
| 528 | + |
| 529 | + // Use SerializingFactory so the in-band images actually decode (it gives |
| 530 | + // real image dimensions, which the fit depends on). One per file, kept |
| 531 | + // alive for the file's lifetime. |
| 532 | + rive::SerializingFactory legacyFactory; |
| 533 | + rive::SerializingFactory modernFactory; |
| 534 | + auto legacy = load(legacyBytes, &legacyFactory); |
| 535 | + auto modern = load(modernBytes, &modernFactory); |
| 536 | + REQUIRE(!legacy.images.empty()); |
| 537 | + REQUIRE(legacy.images.size() == modern.images.size()); |
| 538 | + |
| 539 | + // The layout is animated and may start collapsed (fit ~ 0). Advance both |
| 540 | + // files in lockstep (identical state) until a layout image is open, then |
| 541 | + // pick that image. This avoids depending on async decode timing landing on |
| 542 | + // an open frame. |
| 543 | + rive::NoOpRenderer renderer; |
| 544 | + size_t pick = legacy.images.size(); |
| 545 | + for (int frame = 0; frame < 120 && pick == legacy.images.size(); frame++) |
| 546 | + { |
| 547 | + for (size_t i = 0; i < legacy.images.size(); i++) |
| 548 | + { |
| 549 | + if (xAxisScale(legacy.images[i]->worldTransform()) > 1.0f) |
| 550 | + { |
| 551 | + pick = i; |
| 552 | + break; |
| 553 | + } |
| 554 | + } |
| 555 | + if (pick != legacy.images.size()) |
| 556 | + { |
| 557 | + break; |
| 558 | + } |
| 559 | + legacy.stateMachine->advanceAndApply(0.016f); |
| 560 | + modern.stateMachine->advanceAndApply(0.016f); |
| 561 | + } |
| 562 | + REQUIRE(pick != legacy.images.size()); |
| 563 | + |
| 564 | + auto legacyImage = legacy.images[pick]; |
| 565 | + auto modernImage = modern.images[pick]; |
| 566 | + |
| 567 | + // 7.2 keeps the stored (non-default) user scale; legacy overwrites it with |
| 568 | + // the fit. |
| 569 | + float userScaleX = modernImage->scaleX(); |
| 570 | + REQUIRE(userScaleX != Approx(1.0f)); |
| 571 | + CHECK(legacyImage->scaleX() != Approx(userScaleX)); |
| 572 | + |
| 573 | + // Modern renders at fit * userScale; legacy at fit only (same layout |
| 574 | + // state). |
| 575 | + float legacyScale = xAxisScale(legacyImage->worldTransform()); |
| 576 | + float modernScale = xAxisScale(modernImage->worldTransform()); |
| 577 | + REQUIRE(legacyScale > 0.0f); |
| 578 | + CHECK(modernScale == Approx(legacyScale * userScaleX)); |
| 579 | +} |
0 commit comments