Skip to content

52-paint: V3 controller brushes + V5 universal two-grip world grab - #432

Merged
AlexZ005 merged 24 commits into
feat/52from
feat/52-paint
Oct 23, 2026
Merged

AlexZ005 merged 24 commits into
feat/52from
feat/52-paint

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

52-paint — V3 controller brushes + V5 universal two-grip world grab

Base: origin/feat/51 (1f3b9dd, the 51 union, per the 52 rules: paint waits for 51-int). Opened by the orchestrator (theprototype-41) from the lane's finished handover; the lane left it unpushed.

Done

  • 74d2073 V5 universal two-grip world grab. Base RED in vr-universal-world-grab §1,3,4,5 (+crash; §2 passes on
    base too, said in the suite header). Lane ALL PASS: vr-universal-world-grab, vr-two-hand-grab, vr-world,
    vr-grips-by-mode, vr-grab, vr-world-grab, vr-comfort-iwer, vr-radial-menu (logs p1-existing.log, v5-run1/2.log).
    The one vr-world dollhouse red was a flake (A/B green on base and lane).

  • 22e92b1 V3 controller brushes. vr-brushes-iwer: base RED (no wheels, crash on cursorDebug), lane ALL PASS
    (v3-run2.log); vr-world, terrain-sculpt, terrain-paint, scatter-edit ALL PASS on the lane (v3-run1.log).

  • Unit 206 files / 2585 tests green; check-ratchet 299/39 = baseline after each phase.

  • Forks: QUESTIONS-52-paint.md Q1-Q3 (taken on the recommendation).

  • 118a4ca vr-brushes-perf (--local, IWER Quest 3, budget target quest, 24 m terrain + 2 000 Cone instances,
    29 draw calls): XR frame summed per frame — idle 2.3 ms median; paint +0.2, scatter +0.3, rotate +3.0 ms median
    over idle (gate 4); p95 over idle 1.1 / 0.9 / 5.9 ms (gate 7). ALL PASS (logs/perf-run2.log). perf-run1 timed
    callbacks one by one (0 ms medians, meaningless) — fixed in that commit. Rotate (51's edit brush over 2k) is
    the heavy one: watch it on the Quest.

Next / half-finished

  • Nothing. Dev servers stopped (5482, base 5492). Not pushed (lane header).

For the orchestrator

Shared files touched (52-measure / 52-browser merges): src/lib/vr/grip.js, src/lib/vrGrip.js,
src/lib/vrRadialMenu.js (Sculpt ring entries + root world:back; the primed sculptRef block now sits ABOVE
registerBuiltins(); — a TDZ trap if another lane adds entries that read it), src/lib/vr/radial.js,
src/components/play/VRMenu.svelte ($vrMenuOpen in deriveSectors deps), src/lib/comfortVignette.js,
src/lib/terrainSculpt.js, src/lib/vr/sculpt.js, src/lib/vr/settingsSchema.js, src/lib/vr/prefs.js,
src/lib/vr/worldSnap.js, src/lib/vr/state.js. Adapted suites: vr-grips-by-mode, vr-two-hand-grab, vr-world-grab, vr-world.

Docs (VR page) should say:

  • Two grips scale the world. In Edit, pressing both grips always grabs the world — whatever the rays point at —
    and pulling the hands apart/together scales it (miniature down to 1:50), twisting turns it. To scale or turn an
    OBJECT with two hands, grab it with one hand and move it a little first (about 2 cm), then add the second grip.
    A grip that has not moved the object yet and is joined within a quarter second by the other grip lets the object
    go back where it was (no undo step) and grabs the world instead.
  • Grips never take the terrain, locked objects, things bigger than ~2 m or a room you stand inside by touch.
  • The scale sticks (with a tick) at 1:50 · 1:10 · 1:5 · 1:2 · 1:1 · 2× · 5× · 10×; the readout shows "1:10" below
    life size. Back to 1:1 sits on the quick menu's root ring while the world is scaled/turned/moved.
  • The comfort vignette closes in while the world scales or turns around you.
  • Settings ▸ VR ▸ "Both grips scale the world": Always (default) / Only in empty air (the old rule: two grips on an
    object scale that object).
  • Brushes on the controller: Quick menu ▸ Sculpt ▸ Paint ▸ layer 1-4 and Scatter ▸ Paint / Erase / Rotate / Scale /
    Relax + Source ▸ (current, in-use sources, primitives). Trigger paints; thumbstick X = radius, Y = strength (or
    density while scatter-painting); a ring on the surface shows the brush; a buzz when a stroke starts; the label
    reads e.g. "Paint 3 · 3.0 m · 50%" / "Sphere · 3.0 m · 0.50/m²". Works in a miniature (ring and stroke follow
    the ray at the world's scale). Tracked hands are asked to pick up a controller.

Changelog lines:

  • VR: both grips now always grab the world in Edit — scale down to a 1:50 miniature, with detent ticks, a "1:10"
    readout, Back to 1:1 on the quick menu and the comfort vignette while scaling. Move an object first to scale it
    with two hands. New Settings ▸ VR "Both grips scale the world" (Always / Only in empty air).
  • VR: terrain Paint (4 layers) and the Scatter brushes (paint, erase, rotate, scale, relax, choose a source) on the
    controller, with a brush ring, radius/strength/density on the stick and a buzz on stroke start — in miniature too.

Real-Quest rows (int checklist + device checklist page): world grab feel + detent ticks + vignette while
scaling; miniature 1:50 and Back to 1:1; grab-then-second-grip object scale/rotate; brush strokes at 72 Hz with 2k
scatter, ROTATE especially (+3 ms on the Deck; no dropped frames); stroke-start buzz; hand tracking shows "Pick up a controller" (not IWER-provable).

🤖 Generated with Claude Code

https://claude.ai/code/session_01UQqbSd3GVvhbqGSSiQpxv4

AlexZ005 and others added 24 commits October 22, 2026 17:57
…tic generator

- terrainScatterCore: a set may carry seed + gen {count|density, scale range, rot, spacing}
  (GENERATED slots, each drawn from its own (seed, k) stream so a slot is a stable id),
  ov {[slot]: {x,z,yaw,scale,del}} (hand edits a re-roll keeps), sources [{source, w}]
  (a mixed set; painted records keep their pick in `pick`), tint, sink; every field optional,
  so a 1.38 set normalizes to the identical object and the block stays v1 until one is used
- instancesOf (the ONE list: generated + overrides, then painted, ids g<k>/p<j>),
  withOverrides, withSeed, generateSlots (place/accept hooks for 51-surface), pack sources
- terrainSurface draws per source with tint colours and sink, colliders from the same list
  (one entry per source of a mixed set), pack items load through packRefs
- terrainPaint: writeScatter / editScatterSet / editScatterInstances / rerollScatterSet /
  addScatterSet / scatterInstancesOf; the brush keeps v2 fields, counts generated instances
  toward its density and erases them as deleted edits
- vitest scatterSetFormat (25): determinism twice + across two wire hops, stable slots,
  masks, spacing on raw slots, re-roll keeps edits, migration byte-identical, weights

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wn file

- ScatterSection.svelte = the Scatter <Section> block of TerrainPaintSection, moved
  verbatim (set list, align, collider, remove, Scatter... button)
- TerrainPaintSection renders <ScatterSection {object} /> where the block was, so the
  terrain Inspector is unchanged; any mesh can carry the section from here on (E2)
- svelte-check 299/39 = baseline

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e/scale/relax brushes, per-source erase, per-terrain splat detail

- scatterEditCore.js (pure, vitest): box pick by projected point, rotate/scale/delete/re-roll
  a selection, group transform about a pivot, rotate/scale/relax/erase brushes (rate-based,
  smoothstep falloff, relax deterministic and clamped to the brush), yaw from a quaternion
- scatterEditAdapter.js: the ONE file that knows the set format (today the 1.38 instance
  bytes; re-pointed at 51-sources' overrides when it lands, QUESTIONS-51-edit Q1)
- scatterEdit.js: Scatter brush TOOLS Paint/Erase/Rotate/Scale/Relax/Select + Affects
  (one model or all = per-source erase); Select = click (Shift/Ctrl adds), box drag, Ctrl+A,
  Delete, Esc; the shared gizmo on a scene-root proxy (move on the ground, turn about up,
  uniform size) previews live and commits ONE terrainScatter entry per drag; toolbox Turn /
  Bigger / Smaller / Re-roll (keeps every other instance's edits) / Delete
- every op = applyTerrainScatter + the existing objectParameters terrainScatter message +
  one terrainScatter undo entry: peers, undo and save need nothing new
- per-terrain splat resolution: Inspector > Terrain paint > Detail 64..512, the map resampled
  (weights re-normalised to 255, coverage kept), one terrainSplat entry + message
- terrainSurface: InstancedMeshes carry scatterIds (picking), scatterHolderOf(); terrainSculpt
  routes the edit brushes; Scene: proxy drag/change + the Select tool's press/move/release
- four duotone tool glyphs; debug hooks scatterEdit / scatterEditCore

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and a brush on any mesh

- buildSurface(positions, index?, normals?): the host's LOCAL triangles + an area CDF;
  placeOnSurface(u, v) is uniform by area (u picks the triangle, its remainder + v the
  barycentric), smooth normals interpolated when the mesh has them
- generateSurfaceSlots(gen, S, {draws}): FILL by count (keeps drawing until count are
  PLACED, cap 30 x count attempts) or density (density x area attempts), masks slope /
  height (local frame, so a moved host never reshuffles) / painted terrain layer, minimum
  spacing as 3D Poisson-disk dart throwing in slot order; slot k = the caller's per-slot
  stream, so raising the count adds without moving and every peer gets the same layout
- surfaceDab: the brush on a curved surface (sphere around the hit, the terrain brush's
  density + spacing rules), eraseInSphere, a triangle grid for the query
- 16-byte records for a mesh host's painted instances (xyz over a stored box, octahedral
  normal, yaw, scale) + boxOf / boxFrom
- vitest scatterSurfaceCore: 25 checks incl. two runs + two peers on one seed, spacing with
  its counterfactual, saturation, records round trip; PERF on 99,904 triangles: build
  ~11-25 ms, fill 5,000 ~5-10 ms, fill 5,000 + spacing 0.3 + slope mask ~26-54 ms (node,
  warm vs cold) - no worker needed
- svelte-check = baseline, vitest 2515 green

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…look, mixed weights, tint, sink

- scatterSources.js: ONE catalog (primitives, Library, pack items, prefabs) shared by the brush
  toolbar, Properties > Scatter and the object menu; a prefab or a scene object scatters as its
  LOOK only (userData dropped, base at y 0, GLB filed in the Library by hash + pushed to peers)
  with a note the source row shows; a pristine kit piece scatters as its pack item (no bytes)
- object menu: "Use as scatter source" (sets the brush's source)
- Properties > Scatter: per-set model list (weights for a mix, remove), "Add a model to the
  mix", Tint and Sink scrubs (one undo entry + one message per drag), Re-roll for seeded sets,
  counts read what a set DRAWS
- terrainScatterCore: withSourceAdded / withSourceRemoved (a removed model's painted records go
  with it; a mix of one becomes a plain set); terrainPaint: addScatterSource,
  removeScatterSource, setScatterSourceWeight, scatterBlockNow; debug hook scatterSources

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (checkJs covers tests/unit)

- svelte-check back to 299/39 = baseline (phase 1 had added 50, all in the test fixtures)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…acing in the set format

E2 - scatter on ANY mesh
- Inspector: the Scatter section (ScatterSection.svelte) renders on every plain mesh right after
  the Water section (Add bubble / pour emitter); its "Add scatter" (#scatter-add) adds a seeded
  FILL set with the brush's current source; a Terrain keeps its section beside Terrain paint
- the set format (terrainScatterCore, 51-sources' file, minimal): a set on a non-terrain mesh
  carries host: 'mesh' + box; its painted records are 16 bytes (scatterSurfaceCore); absent on
  every terrain set, so 1.38 terrain blocks stay byte-identical; overrides may carry y
- terrainSurface: ONE instance list per set (setInstances / scatterSetInstances) for the
  renderer, colliders, brush and edit tools; a mesh host fills its own triangles (surface
  cached per geometry identity); instances live in the host's LOCAL frame, so the existing
  holder (matrixWorld copy) makes them follow a move / turn / scale on every peer
- the brush on a mesh: enterSculpt(uuid, {scatter: true}) = a scatter-only session that never
  touches the geometry (no soup conversion, no size cap, nothing sent); the sphere brush
  (surfaceDab) paints ON the surface, erase removes inside the sphere; one undo per stroke
- SculptToolbar offers Scatter on meshes (Paint stays terrain-only)

E3 - fill mode and masks (ScatterFillRows.svelte, per set)
- Count | Density (Segmented), minimum spacing, slope range, height range (local frame),
  painted terrain layer + coverage, Re-roll, Remove fill; drags preview throttled and commit
  ONE undo entry on release
- COUNT means count PLACED (generateSlots keeps drawing until placed, cap 30 x count) on
  terrains too; terrain masks read the height grid + the splat (a layer-masked fill re-lays
  when the paint changes); the Inspector shows the DRAWN count per set (scatterCounts)
- vitest: scatterSurfaceCore 28 (+ mesh-set format, ids on two peers, edits through the
  16-byte codec); scatterSetFormat's mask test updated to count-placed; 2541 green
- svelte-check 299/39 = baseline; e2e suite scatter-surface written, queued (snapshot run)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…atter host too

- terrainSurface.groupSurfaceOf: every child mesh merged into ONE surface in the group's own
  frame (relative transforms + normal matrices baked), keyed by each child's geometry and pose so
  a refilled kit piece or an edited child re-lays; terrainBounds falls back to it for a group
- isMeshHost: a mesh, or a non-light group with meshes under it; the Inspector's Scatter section
  and the scatter-only brush session take it
- Scene: the sculpt/scatter raycast is recursive for a group host, the brush ring takes the hit
  child's normal
- e2e scatter-surface gains a group-host section (two boxes grouped, a slope-masked fill on both
  top faces, identical on B)
- svelte-check 299/39 = baseline

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tools

- setScatterSelection records the scatter block it was taken against, so the onRevision
  check no longer reads our own (or an undone) block as a foreign change and drops a
  real click's selection or the re-selection after an undo
- e2e scatter-edit: rotate / scale / relax brushes, the Affects-filtered erase, one undo
  entry per stroke, the second peer equal, per-terrain splat detail
- e2e scatter-select: a REAL click and a REAL box drag, the gizmo move / turn / size (one
  entry per drag), Turn / Bigger / Re-roll (keeps every other instance) / Delete, Delete
  and Escape owned by the selection, the toolbox holding still; toolbox shots in
  dark / light / contrast. Split in two for the runner's 480 s cap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…atter set

- scatterConvert.instancesToObjects: the selection or a whole set becomes real objects at
  each instance's exact pose (ground, turn, slope tilt, size); one template per source
  (primitive / Library model / prefab - a logic prefab comes with its logic), duplicated
  per instance; asks first above 500; ONE history batch, so one undo turns them back
- objectsToScatter: several copies of ONE object (a primitive or one Library model) on a
  terrain become instances of that source (position, turn, size kept) and the objects go,
  in one batch
- entry points: Select tool "To objects", Properties > Scatter row "To objects", object
  menu "Make scatter" for a multi-selection
- terrainSurface.scatterPrimitiveSpec (the create command, offset, colour, roughness a
  primitive instance is drawn with); debug hook scatterConvert
- e2e scatter-convert: both directions, the 600-instance confirm, peers, undo, save/reload

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…evice line

- scatterThinCore.js (pure, vitest): thinPlan walks instances nearest first and
  stops where the triangle budget runs out; NEAR_KEEP 30 m is never thinned; the
  last 20 % to the cutoff shrinks smoothly (smoothstep), so the edge reads as a
  fade; scatterBudget = half the target's line; thinProfileOf picks quest / phone
  / desktop from the XR session and a coarse pointer.
- scatterThin.js (runtime): every scatter InstancedMesh registers its full
  matrices, colours and ids; one plan per frame at most every 150 ms or after a
  0.75 m move; kept instances are compacted to the front and `count` lowered;
  userData.instanceIds is rewritten in DRAW order so a pick maps instanceId right.
  LOCAL only: nothing is saved or sent, and colliders never read it
  (scatterPhysicsSets builds every instance from the set).
- Settings > Scene > Performance "Distant scatter" (Auto / Headset / Phone /
  Desktop / All), safeStorage key scatterDetail.
- budgetMeterCore: a thinned InstancedMesh counts userData.instanceTotal, so the
  meter shows the content, not what this device happens to draw.
- e2e scatter-sources: desktop + N6 folded peers; params + seed in one message
  (317 bytes for 6000 instances); the N6 draws 1488 of 6000 inside its 250k line
  with every instance within 30 m drawn; a ball dropped on a rock the N6 does not
  draw rests on it there and at the same height on the desktop; hand edits +
  re-roll; a weighted mix through the real select; tint + sink; a prefab as a
  look-only Library source; a 1.38 block stored byte-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-independent order

- groupSurfaceOf sorts the child meshes by their pose in the group, vertex count and name (never
  a uuid, never tree order: a peer that built the group from messages adds the children as they
  arrive), so a seeded fill lands on the same triangles everywhere
- e2e scatter-surface: density checked at the format's 4/m2 cap, spacing at a packable 0.2 m plus
  a past-saturation check (COUNT places what fits and stops), the grouped boxes are placed with
  replicated moves - 39/39 green two peers; terrain-paint 32, terrain-sculpt (2x lane, 2x base
  A/B: an earlier one-off red was a flake), mesh-sculpt 13, scatter-colliders 23 green

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…moved Scatter section

- 51-sources added its per-set rows (model mix + weights, add a model, tint, sink) to the
  Scatter section in TerrainPaintSection.svelte; 51-surface had moved that section VERBATIM
  into ScatterSection.svelte (512623e, QUESTIONS Q2). TerrainPaintSection keeps our side;
  their rows, scrub helpers and imports now live in ScatterSection.svelte.
- Drawn counts stay on scatterCounts (masks + mesh surfaces aware); their "Generated from
  seed N" Re-roll row is folded into the Fill block's Re-roll (the seed is in its title),
  so a set shows one Re-roll.
- withSourceRemoved honours a host:'mesh' set's 16-byte records through io.records (as
  withOverrides does); rewriteSet passes surfaceRecords.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ry element a 0.01 ms one

motion.css set transition-duration: 0.01ms !important on every element under data-motion=reduce.
Every element's default is `transition: all 0s`, so that turned each one into an element that
transitions EVERY property: a style set and read back in the same task read the old value (found by
CI kit-diff, which shoots /kit with reducedMotion: 'reduce' - every row of the contrast table read the
first pair's 13.26:1). Transitions now go to 0s; animations keep 0.01ms so animationend still fires.
Nothing waits on transitionend except Connect's re-measure, which also measures on the next tick.
a11y-scale reads 0s for the reduced probe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Release 1.41.0 — Accessibility: UI scale, High contrast, full keyboard, screen readers, accessible games and VR comfort
…o the 51-sources format

Conflicts (QUESTIONS-51-surface A2), both sides kept:
- SculptToolbar: Scatter panel on any host; source note row kept; 51-edit's tool row
  (Paint/Erase/Rotate/Scale/Relax/Select) on a terrain, Paint + Erase (the 47 erase flag)
  on a mesh host; Affects lists the drawn count + sourceLabel.
- TerrainPaintSection: <ScatterSection> kept, 51-edit's Detail (splat resolution) row kept;
  the per-set "To objects" button moved into ScatterSection (terrain sets).
- terrainSculpt: exitSculpt clears sculptScatterOnly AND ends the edit session;
  paintOp keeps scatter on meshes, 'scatter-edit' on a terrain only; strokeMove passes y;
  startScatterEditSession moved back into enterSculpt (auto-merge had put it in
  enterMeshScatter, where isTerrain does not exist).
- terrainSurface layoutScatter: HEAD (sources/tint/sink/thinning); picking reads
  mesh.userData.instanceIds.

Format seam (QUESTIONS-51-edit Q1): scatterEditAdapter re-pointed at 51-sources' API -
lists = scatterSetInstances (string ids g<k>/p<j>), blockOf diffs each edited list into
withOverrides (moves -> overrides, drops -> del, painted records rewritten; seed/gen/
sources/tint/sink ride through), id-less entries append painted records. Picking maps
instanceIds -> list index; scatterEditCore setPoses/transformGroup keep id/src/tint;
scatterConvert makes each instance of a mixed set from its own source and honours sink.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…verrides, painted rewrites, appends)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…:50, Back to 1:1

- both grips = the world in Edit whatever the rays hit or the hands are inside (Open Brush / Gravity
  Sketch / ShapesXR); a first-hand grab is TENTATIVE (no selection, nothing sent) until it moves > 2 cm
  or 250 ms pass; an unmoved first grab joined by the second grip goes back to its start pose with no
  undo entry and the world grab starts
- two-hand object scale (+ rotate with the hands' axis) only when the first hand already MOVED the
  object; a second hand INSIDE another small object takes that one instead (33 G4 by touch)
- grip-by-contact skips objects that enclose the headset, are larger than 2 m, are terrain or are
  locked (vrGrip.contactGrabAllowed, smallest containing object wins); terrain never moves by grip
- Settings > VR "Both grips scale the world: Always / Only in empty air" (vrBothGripsWorld)
- detents 1:50 1:10 1:5 1:2 1:1 2x 5x 10x (WORLD_SCALE_MIN 0.05 -> 0.02), readout "1:10" below life size,
  the 53 comfort vignette hears the world gesture's scale/yaw rate, radial root "Back to 1:1" while the
  world is off life size
- suites: vr-universal-world-grab (new, IWER; red on the 51 union in 1/3/4/5), vr-grips-by-mode,
  vr-two-hand-grab, vr-world-grab adapted; unit vrGrip / vrWorldSnap / gameShell

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…iniature too

- Sculpt ring gains Paint ▸ (layers 1-4, terrain only) and Scatter ▸ (paint/erase/rotate/scale/relax + Source ▸)
- Source ▸ wheel: current + in-use sources + primitives; stick Y = density while scatter-painting
- haptic pulse on stroke start; tracked hands are asked to pick up a controller
- brush label reads layer/tool, radius and strength or density; ring scales with the world (miniature)
- wheel picks apply synchronously (primed sculpt module), no late dynamic import
- e2e: vr-brushes-iwer (wheels, strokes, undo, miniature 1:10 placement), vr-brushes-perf (Quest profile, 2k instances)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- several callbacks share one XR frame and most are empty, so per-callback medians read 0 ms
- lane run (--local, Quest profile, 2k instances): paint +0.2 ms, scatter +0.3 ms, rotate +3.0 ms median over idle

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AlexZ005
AlexZ005 merged commit 6ebc630 into feat/52 Oct 23, 2026
7 of 9 checks passed
@AlexZ005
AlexZ005 deleted the feat/52-paint branch October 23, 2026 21:26
@AlexZ005

Copy link
Copy Markdown
Collaborator Author

In main via 52-int: feat/52 was merged into feat/52-int and released in 1.45.0 (#482, tag v1.45.0 e59e96a, live on production).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant