)]}'
{
  "commit": "caeae0e723f646ee853461b61e18e04a3081fc1d",
  "tree": "6f54705ddc370cb1a2e186ba1421518cf7bca8b4",
  "parents": [
    "aaa91101383b6e1aadc18fcd556f61016b46b600"
  ],
  "author": {
    "name": "bodymovin",
    "email": "hello@rive.app",
    "time": "Fri Oct 02 00:19:16 2026 +0000"
  },
  "committer": {
    "name": "bodymovin",
    "email": "hello@rive.app",
    "time": "Fri Oct 02 00:19:16 2026 +0000"
  },
  "message": "feat(editor): stroke position (#14437) fa1cd145d3\n* feat(stroke): expose inside/outside stroke position in the editor\n\nThe Rive renderer learned inside and outside strokes in #14117, through\nRenderPaint::strokePosition, but nothing in the scene graph set it. This\nwires it from a new core property to a Position dropdown in the stroke\noptions popout.\n\nCore: Stroke.position, a uint8 (key 470) whose values match\nrive::StrokePosition -- inside 0, center 1, outside 2. It defaults to\ncenter and is only exported when it differs, so existing files are\nunchanged and older runtimes skip it and draw centered.\n\nRuntime: Stroke pushes the position alongside cap and join in\ninitRenderPaint, applyTo and update, and positionChanged marks paint\ndirt. An out of range value draws centered, since the renderer asserts\non positions it doesn\u0027t know.\n\nrive_native: a StrokePosition enum and RenderPaint.strokePosition,\nserialized as one byte right after blendMode under a new PaintDirt bit.\nBeing a leading field it rides the first chunk of a large gradient, like\njoin and cap. renderer_binding reads it back and ignores unknown values.\n\nEditor: the dropdown sits under the cap/join toggles, behind\nENABLE_STROKE_POSITION (on in dev, UAT and early access, off in prod).\nUI only: a stroke authored with a position keeps it with the flag off.\nOnly the Rive renderer draws inside/outside so far; the other backends\ndraw centered. Shift+X remembers the position across a fill round trip.\n\nSilvers: a strokePosition op (43) records the value when a paint leaves\ncenter, so every existing silver is byte for byte unchanged. The C++\nreplayer rejects an unknown value, and the golden comparison checks it.\n\nViewer: reads strokePosition, and also applyLayerMask (42), which\n#14369 added to the C++ writer but not the Dart reader. The instanced\nmesh ops (#14381) had landed after the enum\u0027s closing semicolon, so the\nreader didn\u0027t compile; that is fixed too, and the op parity test passes\nagain.\n\nRenderer behavior this exposes, all intended: inside/outside close open\npaths, so the closing segment is stroked too; joins only exist on the\nouter side of a turn, so inside strokes show them at concave corners and\noutside strokes at convex ones; and which side is inside follows the\nstroke path\u0027s non-zero rule, so same-direction holes and opposite-wound\ncontours resolve the way that rule says.\n\n* test(renderer): pin stroke position on open, concave and overlapping paths\n\nThe stroke_position GMs only draw a circle: closed, convex and without\ncorners, so none of the cases that decide how inside and outside strokes\nlook were covered. These three GMs pin the intended behavior.\n\nstroke_position_shapes: an open zigzag, a square with one concave notch,\nand an even-odd square with a hole, for each join (rows) and each\nposition (columns). Open paths come out closed, and joins show only on\nthe outer side of a turn -- concave corners for inside, convex for\noutside.\n\nstroke_position_shapes_feathered: the same matrix with a feather, which\ntakes the renderer\u0027s other branch (clip by the path, then draw the\nthickened stroke).\n\nstroke_position_winding: what the editor sends, a non-zero stroke path.\nA same-direction hole that an even-odd Fill shows, and two squares wound\nin opposite directions, resolved the way non-zero says.\n\nEach cell draws the stroke, the path\u0027s fill region in translucent purple\nover it, and the path itself as a thin red line.\n\nGoldens come from this PR\u0027s CI run via scripts/rebaseline_pr_goldens.py,\nsince every device renders its own.\n\n* test(renderer): goldens for the stroke position GMs, skipped on canvas2d\n\nAdds the golden images for stroke_position_shapes, _shapes_feathered and\nstroke_position_winding from this PR\u0027s CI candidates, pulled with\nscripts/rebaseline_lfs_gms_and_goldens.py. Every device job reported 0\nfailing, so no existing golden changed; they were only missing these.\nbrowserstack_gms gets the pixel6/gl images, as check_browserstack_gm_images.py\nasks.\n\ncanvas2d already skips stroke_position and stroke_position_clipped because\nit has no stroke position support, and draws every column centered. The new\nGMs join that skip list, so canvas2d and svg get no goldens for them, the\nsame as the existing ones.\n\ngalaxy_s22/glmsaa is still waiting on a device and comes in a follow-up.\n\n* feat(stroke): make stroke position animatable and bindable\n\nStroke.position gets \"animates\" and \"bindable\" in its def, regenerated\ninto rive_core and rml. The C++ runtime and editor_native output is\nunchanged: keyframes (KeyFrameUint) and the number/enum data bind\ncontexts already set it through CoreRegistry::setUint, and\npositionChanged already pushes it onto the paint.\n\nKeys hold. Uints only tween with \"interpolates\", which an enum has no use\nfor, so a partial mix in a blend state or transition switches the value\nrather than landing on a neighbor. In animate mode the Position dropdown\nshows its key button, and the timeline row reads \"Position\".\n\nBinding gets a \"Stroke Position\" system enum (Inside, Center, Outside),\nthe way text and layout enums have one, so a view model can bind by name\ninstead of a number or a hand-built enum in the right order. It is\nappended to SystemEnum so every existing enumType keeps its index, and\ntakes reserved ids [83..86, 0]. The editor maps it to the property with\nthe enumToUint converter; the runtime needs nothing, since it never reads\nenumType.\n\nTests: a keyframe and a registry write (the path binds take) both reach\nthe render paint, and a half mix holds instead of interpolating. In the\neditor, setting position in animate mode keys it and drives the paint\nwithout touching the design value, the paint follows the design value\nagain back in design mode, the dropdown offers a key button, and the\nsystem enum\u0027s names, maps and reserved ids line up with StrokePosition.\n\n* test(renderer): galaxy_s22/glmsaa goldens for the stroke position GMs\n\nThe last device from this PR\u0027s CI: its job was still waiting on a device\nwhen the other goldens went in. It reported 0 failing, so only the three\nnew GMs were missing.\n\n* fix(stroke): read a negative position as center\n\nStroke.position is a plain int in Dart, so a bound number can arrive\nnegative, and strokePositionFromCore only checked the upper bound:\nStrokePosition.values[-1] threw from positionChanged instead of drawing\ncentered like any other unknown value. It checks both bounds now. The C++\nside reads a uint8, so it never sees a negative value.\n\n* fix(stroke): size outside strokes by their full reach\n\npaintBoundsOutset estimated every stroke as reaching half its thickness\npast the path, and shapePaintOutset deliberately skipped stroke position\nbecause Stroke::applyTo never set it. It does now. The renderer draws an\noutside stroke as a centered stroke twice as thick, clipped to the\noutside, so it reaches the full thickness: twice the estimate, and four\ntimes the thickness with a miter join. That outset sizes the offscreen\nsurfaces for layer masks and cache-as-bitmap and drives culling, so an\noutside stroke in a masked or cached layer had its outer edge cut off,\nin the runtime and, through the Dart mirror, on the editor stage.\n\nAn outside stroke now counts its full thickness before the miter and cap\nfactors, on both sides. An inside stroke reaches nothing past the path,\nbut keeps the centered half as a safe over-estimate. The renderer\u0027s own\ncall (calculateBoundsOutset) only ever passes centered strokes, so it is\nunchanged.\n\nBoth paint_outset tests get the same outside-position row, and each side\nchecks that shapePaintOutset reads a real stroke\u0027s position; the C++ one\nfails without the pass-through.\n\nfix(runtime): draw COLRv1 gradients in color emoji instead of solid black (#14552) 55f76f1773\n* fix(runtime): draw COLRv1 gradients in color emoji instead of solid black\n\n* fix(runtime): keep elliptical radial emoji gradients, refresh foreground stops, clamp stop offsets\n\nfix(rive_native): windows hang on exit from GPU teardown in static destructors (#14535) 1f180ed07b\n* fix(rive_native): windows hang on exit from GPU teardown in static destructors\n\nThe process ore context and session consumers map were file statics, so their\ndestructors ran at DLL unload under the loader lock, where ~ContextD3D11\nreleasing the device hangs or crashes in the driver. The hang keeps Rive.exe\nalive and its single instance mutex blocks relaunch; the crash is\nRIVE_NATIVE-SN. Both now outlive the process, like DeferredRenderThread and\ng_contextSession, and a static_assert on each keeps them that way.\n\n* fix(rive_native): release GPU and audio statics before exit on windows instead of leaking them\n\nCo-authored-by: Guido Rosso \u003cguido@rive.app\u003e\nCo-authored-by: Luigi Rosso \u003cluigi-rosso@users.noreply.github.com\u003e\nCo-authored-by: Luigi Rosso \u003cluigi.rosso@gmail.com\u003e\nCo-authored-by: hernan \u003chernan@rive.app\u003e\n",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "dc1d34dbf98ecd3657ca5dabc89a1927aa7308c0",
      "old_mode": 33188,
      "old_path": ".rive_head",
      "new_id": "a5e22d6a4157ae92c5b6873a3276b12423f9335d",
      "new_mode": 33188,
      "new_path": ".rive_head"
    },
    {
      "type": "modify",
      "old_id": "a790ff5e3749e985e33a53eabee59921cebf4a95",
      "old_mode": 33188,
      "old_path": "include/rive/audio/audio_engine.hpp",
      "new_id": "5d07884d1f1b6a47861f71b2ad5970dc8b915d3b",
      "new_mode": 33188,
      "new_path": "include/rive/audio/audio_engine.hpp"
    },
    {
      "type": "modify",
      "old_id": "be03434112960537afb9942916ec816eece0af70",
      "old_mode": 33188,
      "old_path": "include/rive/generated/core_registry.hpp",
      "new_id": "36d6a3fafe8522d322dc9ca34e8aa7aa6ae535e3",
      "new_mode": 33188,
      "new_path": "include/rive/generated/core_registry.hpp"
    },
    {
      "type": "modify",
      "old_id": "bb7ec1f9f548d6326f3b9928f2b5c6961053431b",
      "old_mode": 33188,
      "old_path": "include/rive/generated/shapes/paint/stroke_base.hpp",
      "new_id": "4c301e9f6094f4402da36a815579765d53e64c2c",
      "new_mode": 33188,
      "new_path": "include/rive/generated/shapes/paint/stroke_base.hpp"
    },
    {
      "type": "modify",
      "old_id": "010556a4bdd880f1198708f592ba0fe0df74ea15",
      "old_mode": 33188,
      "old_path": "include/rive/shapes/paint/stroke.hpp",
      "new_id": "0e071945c68ed99498415a7780766ac4f419e7ab",
      "new_mode": 33188,
      "new_path": "include/rive/shapes/paint/stroke.hpp"
    },
    {
      "type": "modify",
      "old_id": "8bf4ff10c4f19136bebff1ed3da25941e514e62a",
      "old_mode": 33188,
      "old_path": "include/rive/text/text.hpp",
      "new_id": "6bbc45287575ed4cc8247a1cc972df29e87cb3ef",
      "new_mode": 33188,
      "new_path": "include/rive/text/text.hpp"
    },
    {
      "type": "modify",
      "old_id": "b00d25d2baf895976a579b5b3a17c2affe00d57a",
      "old_mode": 33188,
      "old_path": "include/rive/text_engine.hpp",
      "new_id": "6e8e46b72a572ecb4b35c3e8b94683d74d9a3ac3",
      "new_mode": 33188,
      "new_path": "include/rive/text_engine.hpp"
    },
    {
      "type": "modify",
      "old_id": "cb944ce68f1787233e69d4b917c1b2902f3ac5a9",
      "old_mode": 33188,
      "old_path": "include/utils/serialize_ops.hpp",
      "new_id": "6bbf7f1fa2d4ea70638043c6f9e14a3cb8f300a7",
      "new_mode": 33188,
      "new_path": "include/utils/serialize_ops.hpp"
    },
    {
      "type": "modify",
      "old_id": "76f038a79e14991d781450512f706e545ee51d1d",
      "old_mode": 33188,
      "old_path": "src/audio/audio_engine.cpp",
      "new_id": "118cec191844f513d2e104429ddf604d5e8dfd37",
      "new_mode": 33188,
      "new_path": "src/audio/audio_engine.cpp"
    },
    {
      "type": "modify",
      "old_id": "b79a4b87bdb17540e680604832d6e72e66350f18",
      "old_mode": 33188,
      "old_path": "src/shapes/paint/paint_outset.cpp",
      "new_id": "a4dec302082404ff937c23a998d6f888b5635dbe",
      "new_mode": 33188,
      "new_path": "src/shapes/paint/paint_outset.cpp"
    },
    {
      "type": "modify",
      "old_id": "7a37fb3363f1e78991324528b71e90adcfca768e",
      "old_mode": 33188,
      "old_path": "src/shapes/paint/stroke.cpp",
      "new_id": "e15cc7764fce3b37ddafaa6eadc12a95d0616bed",
      "new_mode": 33188,
      "new_path": "src/shapes/paint/stroke.cpp"
    },
    {
      "type": "modify",
      "old_id": "73ed67d14b8db638a012b582651bc75dbbfb755f",
      "old_mode": 33188,
      "old_path": "src/text/font_hb.cpp",
      "new_id": "047b1228c92e516426d2b3cf1ac4ef3eb11a3b71",
      "new_mode": 33188,
      "new_path": "src/text/font_hb.cpp"
    },
    {
      "type": "modify",
      "old_id": "72f52c2bf2271230fb3824dcd0c7fb9bf1284030",
      "old_mode": 33188,
      "old_path": "src/text/raw_text.cpp",
      "new_id": "64ce932babab39269b98a582db9ad2579e1aa852",
      "new_mode": 33188,
      "new_path": "src/text/raw_text.cpp"
    },
    {
      "type": "modify",
      "old_id": "a10ca06e11d0b79968b01fe56e25ce405d3e3ade",
      "old_mode": 33188,
      "old_path": "src/text/text.cpp",
      "new_id": "c8728b7a49d668c086ad975140b75cfad3cfd775",
      "new_mode": 33188,
      "new_path": "src/text/text.cpp"
    },
    {
      "type": "modify",
      "old_id": "6ebd0df2dcbf029e070092f9cef4a07e5f9f7e64",
      "old_mode": 33188,
      "old_path": "tests/gm/gmmain.cpp",
      "new_id": "d278e102a34906ae581bd1324ba29aa2f9441cfa",
      "new_mode": 33188,
      "new_path": "tests/gm/gmmain.cpp"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "97ef3f80902a29b9c78ac7b326b1a032741b79b0",
      "new_mode": 33188,
      "new_path": "tests/gm/stroke_position_shapes.cpp"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "8fce7a0c9b4fc8de9992ce083f968997d3a5d585",
      "new_mode": 33188,
      "new_path": "tests/unit_tests/assets/NotoColorEmojiCOLRv1.subset.ttf"
    },
    {
      "type": "modify",
      "old_id": "bf9f56bcb022bd0eabe2101233326903285b2239",
      "old_mode": 33188,
      "old_path": "tests/unit_tests/runtime/color_glyph_test.cpp",
      "new_id": "5a4fad54d9f9ab91e6c6399b492bcf8eed1df5ac",
      "new_mode": 33188,
      "new_path": "tests/unit_tests/runtime/color_glyph_test.cpp"
    },
    {
      "type": "modify",
      "old_id": "4204059a60b651f2162d4195350177e0ef9b4f87",
      "old_mode": 33188,
      "old_path": "tests/unit_tests/runtime/paint_outset_test.cpp",
      "new_id": "15e07df6f835e129582feb87937008f8bcc06df3",
      "new_mode": 33188,
      "new_path": "tests/unit_tests/runtime/paint_outset_test.cpp"
    },
    {
      "type": "modify",
      "old_id": "139b216ef5216c0ec82579eb5c29d9235ff9af8e",
      "old_mode": 33188,
      "old_path": "tests/unit_tests/runtime/serialized_replay_test.cpp",
      "new_id": "98e2c1762ef913e6ba0387857b2b0f0c3aff3e1d",
      "new_mode": 33188,
      "new_path": "tests/unit_tests/runtime/serialized_replay_test.cpp"
    },
    {
      "type": "modify",
      "old_id": "23eddd1b2c268d3324f545d5c9d2b94155f34c26",
      "old_mode": 33188,
      "old_path": "tests/unit_tests/runtime/stroke_test.cpp",
      "new_id": "712c11fc8005ffd8a6b6fab8393e81b6cfa9b5c2",
      "new_mode": 33188,
      "new_path": "tests/unit_tests/runtime/stroke_test.cpp"
    },
    {
      "type": "modify",
      "old_id": "b77ca25b3390c7f60e02c2d018c0872b6d64d7a7",
      "old_mode": 33188,
      "old_path": "utils/serialized_replay.cpp",
      "new_id": "01966fee61ab9d4cf1e201859b73d935d48587e7",
      "new_mode": 33188,
      "new_path": "utils/serialized_replay.cpp"
    },
    {
      "type": "modify",
      "old_id": "1c53baaff95f79df54034906ecf94c9aa00e14c4",
      "old_mode": 33188,
      "old_path": "utils/serializing_factory.cpp",
      "new_id": "8f797fd5db5aff1e57650b60c89cf6be8c2f9531",
      "new_mode": 33188,
      "new_path": "utils/serializing_factory.cpp"
    }
  ]
}
