diff --git a/.tegami/2026-09-25-flushsync.md b/.tegami/2026-09-25-flushsync.md deleted file mode 100644 index 3fd733c4..00000000 --- a/.tegami/2026-09-25-flushsync.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -packages: - npm:@reactunity/renderer: - type: patch ---- - -### `flushSync` works again - -`flushSync` from `@reactunity/renderer` threw `TypeError: not a function` on every call. It called -the reconciler's `flushSync`, which react-reconciler 0.33 renamed to `flushSyncFromReconciler`, and -the reconciler's typings still declare the old name, so nothing caught it. It now commits the -updates its callback schedules before it returns, including on roots rendered with -`disableBatchRendering`. diff --git a/.tegami/2026-09-25-quickjs-0-17.md b/.tegami/2026-09-25-quickjs-0-17.md deleted file mode 100644 index 6e7ffb68..00000000 --- a/.tegami/2026-09-25-quickjs-0-17.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -packages: - upm:com.reactunity.quickjs: - type: patch ---- - -### QuickJS runs JavaScript about a third faster - -The engine moves to quickjs-ng 0.17.0 plus the fork's interpreter work: inline caches on property -access, cached global variable slots, fused compare-and-branch and faster Map, Set, JSON and string -paths. Octane scores 1.33× the previous build on Windows and Linux. Inside Unity, parsing a large -bundle is 1.4–1.7× faster and general JavaScript 1.2–1.5×. Rendering cost is unchanged, because -building UGUI objects on the C# side dominates it. - -Recursion depth on Windows is slightly lower than before. The interpreter's frame on MSVC grew with -this release, so the deepest React tree the Editor can commit at the default -`ScriptRuntime.MaxStackSize` went from 36 levels to 32. diff --git a/.tegami/2026-09-25-unmount-quadratic.md b/.tegami/2026-09-25-unmount-quadratic.md deleted file mode 100644 index ce413f43..00000000 --- a/.tegami/2026-09-25-unmount-quadratic.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -packages: - upm:com.reactunity.core: - type: patch ---- - -### Unmounting a long list is no longer quadratic - -Removing an element re-resolved the styles of every sibling it left behind, on the spot, on top of -the deferred resolve its removal already scheduled. Clearing a list of 1000 elements took 5.8 s; -it now takes 100 ms. Styles still update on the parent's next update, the same way they already -did when a child was added. diff --git a/.tegami/2026-09-26-element-pooling.md b/.tegami/2026-09-26-element-pooling.md deleted file mode 100644 index 74c58f5d..00000000 --- a/.tegami/2026-09-26-element-pooling.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -packages: - upm:com.reactunity.core: - type: patch ---- - -### `PoolingType.All` pools elements under the default renderer, and a reused one starts clean - -Under `PoolingType.All`, the batched renderer (the default) never pooled an element. It sends the -pool key of an element without a `pool` prop as `null`, and the command reader turned that into -`""`, which means `pool={false}`. The unbatched renderer pooled them all along, so the two -disagreed. A missing key now reaches the context as `null` and pools under `All` in both. `pool={false}` -still opts an element out. `Basic`, the default, only pools text and pseudo elements and is -unchanged. - -A reused element also used to keep every prop its previous owner set that the new one does not, -because a create command carries only the props that are present. Styles were already reset. Props -were not, so a reused element could still be hidden by `active={false}`, still checked or disabled, -still pointing at another element's `href`, camera, prefab target, video, SVG content or icon set, -or still limited by an input's `characterLimit` and `contentType`. Each UGUI element now puts its -props back to what a new one has. An element with a camera, prefab target or video lets go of it -when it is unmounted, not when it is reused. - -- A pooled element is dropped from the ref table, so its old ref id no longer resolves to whatever - element reuses it. -- An element whose `Pool()` refuses it is destroyed. It used to be left in the scene. -- Disposing a context no longer pools anything, so a portal under `All` is destroyed with its - context. Before, it was pooled and outlived the context. -- A reused scrollbar has its `data-horizontal`/`data-vertical`/`data-direction` again, and an - input no longer drives a scrollbar that went back to the pool. -- A filtered element lets go of its filter when it is pooled. Its offscreen surface and the - composite it left in its old parent were kept, so whatever reused that parent showed a stale - capture. A reused element that no longer had a filter was also moved back under its first - owner's parent. -- An element whose `backdrop-filter` or `outline` goes away, whether through a style change or - through reuse, stops drawing it. Both used to stay once set. -- UIToolkit and Editor elements are no longer pooled under `All`. A `VisualElement` is cheap to - build, and each kind has native fields a reuse would have to reset one by one. Text and pseudo - elements are pooled as before. diff --git a/.tegami/2026-09-26-json-commands.md b/.tegami/2026-09-26-json-commands.md deleted file mode 100644 index 569bb753..00000000 --- a/.tegami/2026-09-26-json-commands.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -packages: - upm:com.reactunity.core: - type: minor ---- - -### Newtonsoft.Json is no longer a dependency - -The renderer's command buffer is now read by a small JSON reader of ReactUnity's own instead of -Newtonsoft's token tree, which parses it about four times as fast. Mounting a list of 1000 styled -elements takes about 45 ms less, and updating it 16% less. - -A string prop that looks like a date now arrives as written. Newtonsoft turned it into a `DateTime`, -so a prop like `"2026-09-26T10:00:00.000Z"` reached C# reformatted in the current culture. - -`com.unity.nuget.newtonsoft-json` is no longer pulled in by `com.reactunity.core`. A project that -used Newtonsoft without depending on it itself needs to add it to its own manifest. diff --git a/.tegami/2026-09-26-nested-filters.md b/.tegami/2026-09-26-nested-filters.md deleted file mode 100644 index 0dbc3688..00000000 --- a/.tegami/2026-09-26-nested-filters.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -packages: - upm:com.reactunity.core: - type: patch ---- - -### Pointer events and captures work through nested filters - -An element with `filter`, `clip-path`, `mask-image`, `isolation`, `mix-blend-mode` or `perspective` -is drawn from an offscreen capture. When one of those sat inside another, several things went -wrong, and a page built from many of them, like the kitchen-sink Game HUD, showed all of them: - -- **Clicks stopped reaching the page.** The inner filter's raycaster mapped the pointer straight - into the outer filter's camera instead of through the outer capture. It also reported that - offscreen camera as its event camera, which outranks the page camera, so its misplaced hits won - every click. A pointer now goes through each capture it is nested in, and the outermost filter - casts for everything inside it. -- **Overlapping filters took each other's clicks.** Two filtered siblings tied, so which one got a - click came down to the order their raycasters registered in. The one painted on top now wins. -- **Captures flickered.** The object that renders queued captures is created by the first one, so - on that frame a filter could be queued twice. It was moved into the shared render twice and - back once, left there, and drew into other elements' captures for a frame at a time. -- **An outer filter could show nothing, or a stale copy, of an inner one.** Captures ran in an - order that only counted nesting up to the first surface, so an outer clip could be captured - before the fill inside it. And an inner filter that re-captured never told the outer one to - do the same. Inner captures now run first, and a finished one marks its outer filter for - re-capture. diff --git a/.tegami/2026-09-26-ordinal-css-lookups.md b/.tegami/2026-09-26-ordinal-css-lookups.md deleted file mode 100644 index 7871fa48..00000000 --- a/.tegami/2026-09-26-ordinal-css-lookups.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -packages: - upm:com.reactunity.core: - type: patch ---- - -### Faster element creation - -The style system's name tables compared keys with the invariant culture, which under Mono builds a -sort key on every hash, so looking up a property name cost about 13 µs. They compare ordinally now, -which matches the same ASCII names, and an inline `style` prop applies in about half the time. - -UGUI elements are also created with their `RectTransform` in place, instead of adding one that -replaces the `Transform` Unity started them with. Together these take about 80 ms off mounting a -list of 1000 elements. diff --git a/.tegami/publish-lock.yaml b/.tegami/publish-lock.yaml index 1b292442..0165d09c 100644 --- a/.tegami/publish-lock.yaml +++ b/.tegami/publish-lock.yaml @@ -1,27 +1,6 @@ core:changelogs: - - content: "---\npackages:\n npm:@reactunity/renderer:\n type: minor\n upm:com.reactunity.core:\n type: minor\n---\n\n### Logical borders and sizing\n\nThe painted border properties have their logical spellings — `border-inline-color`,\n`border-start-start-radius` and the rest — so `direction: rtl` turns a coloured edge or a rounded corner\naround, and Tailwind's `border-s-*` and `rounded-s-*` do something. `inline-size`, `block-size` and their\n`min-`/`max-` forms are aliases for the width and height properties.\n" - filename: 2026-09-12-logical-borders-and-sizing.md - v: 0.0.0 - - content: "---\npackages:\n npm:@reactunity/renderer:\n type: minor\n upm:com.reactunity.core:\n type: minor\n---\n\n### `scroll-padding`, `scroll-margin` and `scroll-snap-stop`\n\nThe three scroll-snap properties that were missing are in: `scroll-padding` insets the box a snap target\nlines up against, `scroll-margin` outsets the target itself, and `scroll-snap-stop: always` keeps a fling\nfrom carrying past a target on its way to one further along.\n" - filename: 2026-09-12-scroll-padding-margin-stop.md - v: 0.0.0 - - content: "---\npackages:\n npm:@reactunity/renderer:\n type: minor\n upm:com.reactunity.core:\n type: minor\n---\n\n### `background-clip`\n\n`background-clip` takes one value per background layer, and all four of CSS's values now do what they say.\n`text` cuts a layer down to the element's own glyphs — a gradient or an image painted through the text,\nwhich with `color: transparent` is the gradient-text effect. The glyphs are rasterised into a coverage\ntexture by a command buffer and only when they move, so an idle element costs nothing.\n\nThe three boxes used to be one box. Every background was masked at the padding edge, so a background never\nreached under its own border: a dashed, dotted or semi-transparent border showed the page through its gaps\nrather than the element, and a rounded one took the border's inner radius instead of its own. The mask is the border box now, which is both\nwhat `border-box` means and what CSS makes the default; `padding-box` and `content-box` are cut per layer\nin the fragment shader, with the radii that box has left, and cost nothing when there is no border or\npadding to separate them.\n\nMoving that mask corrected an inset `box-shadow` with it. One is cast by the padding edge, which is where\nit is now drawn and rounded — it used to be cast by the border box and then trimmed back to the padding\nbox by the mask, so on a bordered element it sat a border-width out from where CSS puts it.\n" - filename: 2026-09-13-background-clip.md - v: 0.0.0 - - content: "---\npackages:\n npm:@reactunity/renderer:\n type: patch\n upm:com.reactunity.core:\n type: patch\n---\n\n### `calc()` keeps a percentage\n\nA `calc()` that works out to a percentage is one now, on every property Yoga can hold a percentage for:\n`width: calc(1/2 * 100%)` is `50%`, and so `w-1/2`, `left-1/2`, `-translate-x-1/2` and `basis-1/3` — which\nis how a CSS framework spells every fraction it has — lay out instead of dropping. The percentage is\ncarried through the arithmetic rather than resolved on sight, which is what used to throw the answer away:\nthe parent it was resolved against had not been laid out yet, so the declaration came out as nothing at\nall.\n\nIt survives a sub-expression too, parenthesised or a nested `calc()` — `calc(calc(1 / 2 * 100%) * -1)`,\nwhich is what a framework emits for every *negative* fraction utility. A group is worked out before the\ncalculation around it, and the length or percentage it came out as is an operand again exactly as it\nstands, rather than something the arithmetic around it could not read.\n\nOn a property Yoga does not hold as a percentage -- `border-left-width`, `scroll-margin-left` -- a\ncalculated percentage now resolves to the same number the percentage written out gets, rather than\nreaching the property as a value of the wrong type.\n\nA calculation mixing a percentage with a length, `calc(100% - 2rem)`, is still dropped. Yoga has one unit\nper value and no calc of its own, so there is nothing to hand it.\n\n`infinity` is a number as CSS Values 4 asks — the largest length the implementation supports, not a real\ninfinity. `border-radius: calc(infinity * 1px)`, which is what a `rounded-full` utility compiles to, used\nto come out of the corner reduction as NaN and leave the element painting nothing.\n\nUnder `REACT_UNITY_DEVELOPER`, a declaration that resolves to nothing now says so once in the console,\nnaming the property and the value. A dropped declaration was silent before, which is the part of this that\ncost the most to find.\n" - filename: 2026-09-13-calc-percentage.md - v: 0.0.0 - - content: "---\npackages:\n npm:@reactunity/renderer:\n type: minor\n upm:com.reactunity.core:\n type: minor\n---\n\n### `overscroll-behavior`\n\n**Scrolling now chains.** A `` that has reached its end hands the rest of the gesture to the\nnearest scroll box above it, as a page does on the web. Until now the innermost box swallowed every\ntick and every drag it was given, so a short list inside a long page was a dead spot: reaching its\nend stopped the scroll rather than carrying on with the page behind it.\n\n`overscroll-behavior` is how a box keeps a scroll to itself instead. `contain` stops the handover,\n`none` stops it and drops the local overscroll effect with it -- which here is ``'s\n`elasticity` prop, so `none` on an elastic box takes its bounce away where `contain` leaves it. Both\naxes are separate: the shorthand takes one value or two with `x` first, `overscroll-behavior-x` and\n`-y` set them individually, and `overscroll-behavior-inline` and `-block` are the same two under\ntheir logical names. The axis a gesture runs along is the one that decides, so a contained `y` does\nnot trap a sideways scroll.\n\nA gesture stays with whichever box took it, rather than being handed over the moment that one\nreaches its own end: dragging a list down to its end and on past it does not then start dragging the\npage. That is the latching a browser does, and it is measured from where the drag began, so a slow\none is not mistaken for a stuck one.\n\nChaining walks the element tree rather than the transform hierarchy, so a `filter` or a\n`perspective` in between -- both of which reparent their subtree onto an offscreen canvas -- does\nnot break it.\n" - filename: 2026-09-13-overscroll-behavior.md - v: 0.0.0 - - content: "---\npackages:\n npm:@reactunity/renderer:\n type: minor\n upm:com.reactunity.core:\n type: minor\n---\n\n### `perspective`, `perspective-origin` and `backface-visibility`\n\n3D transforms have always composed here — `rotateX` and `rotateY` set a real rotation, and children keep\ntheir own depth the way `transform-style: preserve-3d` describes — but nothing projected them, so a\nrotation only ever foreshortened linearly and never converged anywhere.\n\n`perspective` on a parent now does. Its subtree is captured through a camera of its own, the same offscreen\nmachinery `filter` uses, with an off-axis frustum standing the viewer wherever `perspective-origin` puts\nthem; the capture grows to hold whatever the projection throws outside the element's box, so a card leaning\nout of its stage is not clipped. `backface-visibility: hidden` is separate and needs no perspective: it\nwatches the element's winding — which catches a mirror as well as a rotation, as CSS does — and takes the\nelement out of both drawing and hit testing while its back is turned.\n\nThere is still no `transform-style`. Its `preserve-3d` is what everything already does, and its default\n`flat` would need a shear the transform model has nowhere to put.\n\nA `transform` whose functions run together with no space between them -- `rotateX(35deg)rotateY(-30deg)`,\nwhich is what every CSS minifier emits -- now parses. It was being read as one token naming no function,\nso a built stylesheet silently lost the whole declaration while the same rule worked in source.\n\n**`translate-z` has changed sign.** A positive value now comes *towards* the viewer, as CSS says and as\n`rotateX`/`rotateY` already did. Nothing could show the difference under an orthographic screen-space\ncanvas, which is why it went unnoticed; a `perspective` shows it immediately. Code that leaned on the old\ndirection — most likely ordering elements in a world-space canvas — wants the opposite sign now.\n" - filename: 2026-09-13-perspective-and-backface.md - v: 0.0.0 - - content: "---\npackages:\n npm:@reactunity/renderer:\n type: patch\n---\n\n### Logical border radii survive the Vite build\n\nThe preset names an old CSS target so that Lightning CSS lowers what ReactUnity's CSS subset has no\nparser for. `border-start-start-radius` and its three siblings were caught by that too, and their\nlowered form is a pair of rules selected by `:-webkit-any(:lang(ae),:lang(ar),...)` naming every\nright-to-left language -- pseudo-classes ReactUnity reads as custom states, which never match, so the\nradius was dropped and the console carried a warning for each. Lowering is excluded for the logical\nproperties now, the way it already was for `light-dark()`: both are things the renderer resolves\nitself, and against the element's own direction rather than the document's language.\n" - filename: 2026-09-14-lightningcss-logical-properties.md - v: 0.0.0 - - content: "---\npackages:\n npm:@reactunity/renderer:\n type: minor\n upm:com.reactunity.core:\n type: minor\n---\n\n### Relative color syntax\n\nEvery color function takes a `from ` origin, which is CSS Color 5's relative color syntax:\n`rgb()`, `hsl()`, `hsv()`, `lab()`, `lch()`, `oklab()` and `oklch()`. The origin is converted into the\nfunction's own space and its channels become keywords the channel slots can use, on their own or inside\n`calc()` — so `rgb(from var(--brand) r g b / 50%)` is a brand color at half opacity without repeating it,\nand `oklch(from currentcolor calc(l * 0.8) c h)` is a hover shade of whatever the element's own color\nturns out to be. A keyword may appear in any slot, so `rgb(from red b g r)` is blue.\n\nEach keyword carries the range CSS gives it — `r` is `0`–`255`, `hsl()`'s `s` is `0`–`100`, `oklch()`'s\n`l` is `0`–`1`, a hue is degrees, `alpha` is `0`–`1` — so a literal mixes with one exactly as it would in\na browser. An omitted alpha is the origin's own rather than `1`, so `rgb(from r g b)` is that\ncolor unchanged.\n\nThe origin stays lazy, so it may be a `var()`, `currentColor`, a `color-mix()` or another relative color,\nand one parsed rule resolves against each element it lands on. A literal origin is decomposed while\nparsing instead, so the whole function still folds to a constant.\n\nTwo things the color functions have always got wrong were in the way, and are fixed:\n\n`hsl()` and `hsv()` read saturation and lightness on CSS's `0`–`100` scale rather than `0`–`1`, so the\n`%` may be left off as every framework that emits `hsl(152 37 59)` expects. Written with percentages\nthey are unchanged; written as bare fractions — `hsl(152, 0.37, 0.59)`, which was the only spelling that\nused to work — they now mean what CSS says they mean, which is very nearly black.\n\nOut-of-range components are clamped rather than carried through the conversion, which is what CSS\nColor 4 asks of a specified color and what this arithmetic reaches constantly: `rgb(300 0 0)` is red,\n`hsl(0 150 50)` is a saturation of `100%`, and `calc(l * 4)` is a lightness of `1` rather than a color\nthat paints nothing. A saturation past `100%` used to come out of the conversion as a different hue\nentirely. Alpha is clamped everywhere, hue wraps, and a channel written as `none` is `0` in `rgb()`,\n`hsl()` and `hsv()` as it already was in the Lab family.\n" - filename: 2026-09-14-relative-color-syntax.md + - content: "---\npackages:\n npm:@reactunity/renderer:\n type: patch\n---\n\n### `flushSync` works again\n\n`flushSync` from `@reactunity/renderer` threw `TypeError: not a function` on every call. It called\nthe reconciler's `flushSync`, which react-reconciler 0.33 renamed to `flushSyncFromReconciler`, and\nthe reconciler's typings still declare the old name, so nothing caught it. It now commits the\nupdates its callback schedules before it returns, including on roots rendered with\n`disableBatchRendering`.\n" + filename: 2026-09-25-flushsync.md v: 0.0.0 core:packages: - id: npm:@reactunity/monorepo @@ -31,14 +10,7 @@ core:packages: - id: npm:@reactunity/material updated: true - changelogIds: - - 2026-09-12-logical-borders-and-sizing.md - - 2026-09-12-scroll-padding-margin-stop.md - - 2026-09-13-background-clip.md - - 2026-09-13-calc-percentage.md - - 2026-09-13-overscroll-behavior.md - - 2026-09-13-perspective-and-backface.md - - 2026-09-14-lightningcss-logical-properties.md - - 2026-09-14-relative-color-syntax.md + - 2026-09-25-flushsync.md id: npm:@reactunity/renderer updated: true - id: npm:@reactunity/scripts diff --git a/CHANGELOG.md b/CHANGELOG.md index 93080ddc..e95d2cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,124 @@ +## 0.25.1 + +### `flushSync` works again + +`@reactunity/renderer` + +`flushSync` from `@reactunity/renderer` threw `TypeError: not a function` on every call. It called +the reconciler's `flushSync`, which react-reconciler 0.33 renamed to `flushSyncFromReconciler`, and +the reconciler's typings still declare the old name, so nothing caught it. It now commits the +updates its callback schedules before it returns, including on roots rendered with +`disableBatchRendering`. + +### QuickJS runs JavaScript about a third faster + +`com.reactunity.quickjs` + +The engine moves to quickjs-ng 0.17.0 plus the fork's interpreter work: inline caches on property +access, cached global variable slots, fused compare-and-branch and faster Map, Set, JSON and string +paths. Octane scores 1.33× the previous build on Windows and Linux. Inside Unity, parsing a large +bundle is 1.4–1.7× faster and general JavaScript 1.2–1.5×. Rendering cost is unchanged, because +building UGUI objects on the C# side dominates it. + +Recursion depth on Windows is slightly lower than before. The interpreter's frame on MSVC grew with +this release, so the deepest React tree the Editor can commit at the default +`ScriptRuntime.MaxStackSize` went from 36 levels to 32. + +### Unmounting a long list is no longer quadratic + +`com.reactunity.core` + +Removing an element re-resolved the styles of every sibling it left behind, on the spot, on top of +the deferred resolve its removal already scheduled. Clearing a list of 1000 elements took 5.8 s; +it now takes 100 ms. Styles still update on the parent's next update, the same way they already +did when a child was added. + +### `PoolingType.All` pools elements under the default renderer, and a reused one starts clean + +`com.reactunity.core` + +Under `PoolingType.All`, the batched renderer (the default) never pooled an element. It sends the +pool key of an element without a `pool` prop as `null`, and the command reader turned that into +`""`, which means `pool={false}`. The unbatched renderer pooled them all along, so the two +disagreed. A missing key now reaches the context as `null` and pools under `All` in both. `pool={false}` +still opts an element out. `Basic`, the default, only pools text and pseudo elements and is +unchanged. + +A reused element also used to keep every prop its previous owner set that the new one does not, +because a create command carries only the props that are present. Styles were already reset. Props +were not, so a reused element could still be hidden by `active={false}`, still checked or disabled, +still pointing at another element's `href`, camera, prefab target, video, SVG content or icon set, +or still limited by an input's `characterLimit` and `contentType`. Each UGUI element now puts its +props back to what a new one has. An element with a camera, prefab target or video lets go of it +when it is unmounted, not when it is reused. + +- A pooled element is dropped from the ref table, so its old ref id no longer resolves to whatever + element reuses it. +- An element whose `Pool()` refuses it is destroyed. It used to be left in the scene. +- Disposing a context no longer pools anything, so a portal under `All` is destroyed with its + context. Before, it was pooled and outlived the context. +- A reused scrollbar has its `data-horizontal`/`data-vertical`/`data-direction` again, and an + input no longer drives a scrollbar that went back to the pool. +- A filtered element lets go of its filter when it is pooled. Its offscreen surface and the + composite it left in its old parent were kept, so whatever reused that parent showed a stale + capture. A reused element that no longer had a filter was also moved back under its first + owner's parent. +- An element whose `backdrop-filter` or `outline` goes away, whether through a style change or + through reuse, stops drawing it. Both used to stay once set. +- UIToolkit and Editor elements are no longer pooled under `All`. A `VisualElement` is cheap to + build, and each kind has native fields a reuse would have to reset one by one. Text and pseudo + elements are pooled as before. + +### Newtonsoft.Json is no longer a dependency + +`com.reactunity.core` + +The renderer's command buffer is now read by a small JSON reader of ReactUnity's own instead of +Newtonsoft's token tree, which parses it about four times as fast. Mounting a list of 1000 styled +elements takes about 45 ms less, and updating it 16% less. + +A string prop that looks like a date now arrives as written. Newtonsoft turned it into a `DateTime`, +so a prop like `"2026-09-26T10:00:00.000Z"` reached C# reformatted in the current culture. + +`com.unity.nuget.newtonsoft-json` is no longer pulled in by `com.reactunity.core`. A project that +used Newtonsoft without depending on it itself needs to add it to its own manifest. + +### Pointer events and captures work through nested filters + +`com.reactunity.core` + +An element with `filter`, `clip-path`, `mask-image`, `isolation`, `mix-blend-mode` or `perspective` +is drawn from an offscreen capture. When one of those sat inside another, several things went +wrong, and a page built from many of them, like the kitchen-sink Game HUD, showed all of them: + +- **Clicks stopped reaching the page.** The inner filter's raycaster mapped the pointer straight + into the outer filter's camera instead of through the outer capture. It also reported that + offscreen camera as its event camera, which outranks the page camera, so its misplaced hits won + every click. A pointer now goes through each capture it is nested in, and the outermost filter + casts for everything inside it. +- **Overlapping filters took each other's clicks.** Two filtered siblings tied, so which one got a + click came down to the order their raycasters registered in. The one painted on top now wins. +- **Captures flickered.** The object that renders queued captures is created by the first one, so + on that frame a filter could be queued twice. It was moved into the shared render twice and + back once, left there, and drew into other elements' captures for a frame at a time. +- **An outer filter could show nothing, or a stale copy, of an inner one.** Captures ran in an + order that only counted nesting up to the first surface, so an outer clip could be captured + before the fill inside it. And an inner filter that re-captured never told the outer one to + do the same. Inner captures now run first, and a finished one marks its outer filter for + re-capture. + +### Faster element creation + +`com.reactunity.core` + +The style system's name tables compared keys with the invariant culture, which under Mono builds a +sort key on every hash, so looking up a property name cost about 13 µs. They compare ordinally now, +which matches the same ASCII names, and an inline `style` prop applies in about half the time. + +UGUI elements are also created with their `RectTransform` in place, instead of adding one that +replaces the `Transform` Unity started them with. Together these take about 80 ms off mounting a +list of 1000 elements. + ## 0.25.0 ### Logical borders and sizing diff --git a/packages/create/package.json b/packages/create/package.json index 2d2721f4..cd3edf0f 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -1,6 +1,6 @@ { "name": "@reactunity/create", - "version": "0.25.0", + "version": "0.25.1", "description": "Scaffold a ReactUnity app using npm init", "license": "MIT", "//type": "ESM, as of the chalk 6 / commander 15 upgrade: both dropped CommonJS. index.ts is emitted next to itself as ESM, so `bin` keeps pointing at index.js.", diff --git a/packages/create/scaffold/Packages/manifest.json b/packages/create/scaffold/Packages/manifest.json index d4c15c5d..2604d206 100644 --- a/packages/create/scaffold/Packages/manifest.json +++ b/packages/create/scaffold/Packages/manifest.json @@ -1,7 +1,7 @@ { "dependencies": { - "com.reactunity.core": "0.25.0", - "com.reactunity.quickjs": "0.25.0", + "com.reactunity.core": "0.25.1", + "com.reactunity.quickjs": "0.25.1", "com.unity.ai.navigation": "2.0.14", "com.unity.editorcoroutines": "1.1.0", "com.unity.ide.visualstudio": "2.0.26", diff --git a/packages/create/scaffold/react/package.json b/packages/create/scaffold/react/package.json index 748bbdc1..7a79f10c 100644 --- a/packages/create/scaffold/react/package.json +++ b/packages/create/scaffold/react/package.json @@ -10,11 +10,11 @@ "lint": "react-unity-scripts lint" }, "dependencies": { - "@reactunity/renderer": "^0.25.0", + "@reactunity/renderer": "^0.25.1", "react": "^19.1.0" }, "devDependencies": { - "@reactunity/scripts": "^0.25.0", + "@reactunity/scripts": "^0.25.1", "typescript": "^5.8.3" } } diff --git a/packages/material/package.json b/packages/material/package.json index 90b117b8..4a49f022 100644 --- a/packages/material/package.json +++ b/packages/material/package.json @@ -1,6 +1,6 @@ { "name": "@reactunity/material", - "version": "0.25.0", + "version": "0.25.1", "description": "Material Design components and utilities for React Unity", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/renderer/CHANGELOG.md b/packages/renderer/CHANGELOG.md index e373daec..f6c73e57 100644 --- a/packages/renderer/CHANGELOG.md +++ b/packages/renderer/CHANGELOG.md @@ -1,3 +1,13 @@ +## @reactunity/renderer@0.25.1 + +### `flushSync` works again + +`flushSync` from `@reactunity/renderer` threw `TypeError: not a function` on every call. It called +the reconciler's `flushSync`, which react-reconciler 0.33 renamed to `flushSyncFromReconciler`, and +the reconciler's typings still declare the old name, so nothing caught it. It now commits the +updates its callback schedules before it returns, including on roots rendered with +`disableBatchRendering`. + ## @reactunity/renderer@0.25.0 ### Logical borders and sizing diff --git a/packages/renderer/package.json b/packages/renderer/package.json index d6dea31a..8dd9b3de 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -1,6 +1,6 @@ { "name": "@reactunity/renderer", - "version": "0.25.0", + "version": "0.25.1", "description": "React renderer for Unity3D", "types": "dist/index.d.ts", "typings": "dist/index.d.ts", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index e4e04ae8..649638a8 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@reactunity/scripts", - "version": "0.25.0", + "version": "0.25.1", "description": "Configuration and scripts for React Unity.", "repository": { "type": "git", diff --git a/unity/clearscript/package.json b/unity/clearscript/package.json index 930bfd99..50661b1c 100644 --- a/unity/clearscript/package.json +++ b/unity/clearscript/package.json @@ -6,7 +6,7 @@ "url": "https://github.com/KurtGokhan" }, "displayName": "React Unity ClearScript", - "version": "0.25.0", + "version": "0.25.1", "unity": "2019.4", "description": "ClearScript (V8) Plugin for ReactUnity", "keywords": ["ReactUnity", "ClearScript", "V8"], diff --git a/unity/core/package.json b/unity/core/package.json index 0f4cb010..95600709 100644 --- a/unity/core/package.json +++ b/unity/core/package.json @@ -6,7 +6,7 @@ "url": "https://github.com/KurtGokhan" }, "displayName": "React Unity", - "version": "0.25.0", + "version": "0.25.1", "unity": "2021.3", "description": "React renderer for Unity", "keywords": ["React", "Framework", "UI", "UGUI", "UIToolkit"], diff --git a/unity/jint/package.json b/unity/jint/package.json index e8965c8d..398753fc 100644 --- a/unity/jint/package.json +++ b/unity/jint/package.json @@ -6,7 +6,7 @@ "url": "https://github.com/KurtGokhan" }, "displayName": "React Unity Jint", - "version": "0.25.0", + "version": "0.25.1", "unity": "2019.4", "description": "Jint Plugin for ReactUnity", "keywords": ["ReactUnity", "Jint"], diff --git a/unity/quickjs/package.json b/unity/quickjs/package.json index 0b639c95..bd0ed05e 100644 --- a/unity/quickjs/package.json +++ b/unity/quickjs/package.json @@ -6,7 +6,7 @@ "url": "https://github.com/KurtGokhan" }, "displayName": "React Unity QuickJS", - "version": "0.25.0", + "version": "0.25.1", "unity": "2019.4", "description": "QuickJS Plugin for ReactUnity", "keywords": ["ReactUnity", "QuickJS"],