Skip to content

feat(points): set the point render order without reordering the data - #277

Merged
Stukova merged 7 commits into
mainfrom
claude/point-render-order-40eed4
Oct 14, 2026
Merged

Stukova merged 7 commits into
mainfrom
claude/point-render-order-40eed4

Conversation

@rokotyan

@rokotyan rokotyan commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Why

Points are always drawn in index order, so a higher index is drawn on top. Until now, the only way to choose what's on top was to reorder every input array, remap the link endpoints and keep a permutation map for callbacks and index-based config. That also changes the layout, because the simulation deals its random values by index. Reordering at runtime is worse: setPointPositions zeroes velocities and re-uploads everything.

What

graph.setPointRenderOrder(order: ArrayLike<number> | null) changes only the drawing order. Point indices, the simulation and every other per-point array keep their meaning.

graph.setPointRenderOrder([7])     // bring point 7 to the front; the rest keep index order underneath
graph.setPointRenderOrder(bySize)  // a full permutation, back to front
graph.setPointRenderOrder(null)    // back to index order
graph.render()
  • Partial lists put the listed points on top, in list order. Every other point stays underneath in index order.
  • A repeated index takes its last position, so appending an index brings that point to the front.
  • Invalid entries are ignored.
  • null, [] or any list that resolves to index order keeps today's unindexed draw path.
  • Hover, click and getSampledPoints follow the render order, so the point on top is the one they report.
  • Highlighting: highlighted points stay above greyed-out ones, and the order applies within each group.

How

  • Order buffer: a uint32 element buffer holds the order. It drives every pass that paints in draw order: the point draw, the picking fill and the label-sampling fill.
  • Occlusion culling: the core pass uses the order reversed. Depth now comes from a per-point renderRank attribute instead of the point index, because depth is the only thing the core and fringe passes agree on. In an indexed draw, gl_VertexID is the point index, not the position in the order.
  • Data resolution: GraphData re-resolves the order only when it's reassigned or the point count changes.

Demo

render-order-demo.mp4

New story: Examples / Points / Render Order.

Testing

All in the browser, since the repo has no unit-test runner:

  • With no order set, output is pixel-identical to main (A/B full-frame compare, 20k points) with culling on and off, translucent points, and highlighting.
  • With a random permutation, output is pixel-identical to main drawing the input arrays physically permuted the same way.
  • Stacked-point checks confirm, for every input shape, that the top pixel, the picked index and the sampled index agree, with no GL errors. They cover culling on/off, highlighting, translucency, the point count growing and shrinking, absent (NaN) points, calls made before the device is ready, and an array edited in place and set again.
  • Cost: a coherent order is free on the GPU. A random permutation of 1M points costs up to ~2× vertex time, because the attribute and texture reads scatter. Numbers are in history/2026/2026-09-30-point-render-order.md.
  • npm run build and eslint pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added control over point drawing order, including partial custom orders. Unlisted points retain their default order; repeated indices use their last position, and invalid indices are ignored.
    • Custom order affects picking and label sampling, persists across position updates, and can be reset to the default order. Changes take effect on the next render, without animation.
    • Added an interactive example with render-order presets, point selection, and demonstrations of how ordering affects overlapping points. Highlighted points retain priority over greyed-out points.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: cosmosgl/graph/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e7bca0a0-e3e5-4f5d-9d15-607db9b4ff6b
📥 Commits

Reviewing files that changed from the base of the PR and between e14de59 and 65bf8e0.

📒 Files selected for processing: 5
  • history/2026/2026-09-30-point-render-order.md
  • src/index.ts
  • src/modules/Points/index.ts
  • src/stories/api-reference.mdx
  • test/point-render-order.test.ts
🚧 Files skipped from review as they are similar to previous changes: 1
  • history/2026/2026-09-30-point-render-order.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

📝 Walkthrough

The graph now accepts a point render order and resolves it against the current point count. Drawing, picking, label sampling, and the occlusion core pass use the resolved order. API documentation and an interactive Storybook example describe and demonstrate the feature.

Changes: Point render ordering

Layer / File(s) Summary
Order input and resolution
src/index.ts, src/modules/GraphData/index.ts
Adds setPointRenderOrder and resolves partial orders, duplicate indices, invalid indices, and index-order fallback. Order resolution runs during graph data updates and graph creation.
Render-order buffers and draw passes
src/modules/Points/index.ts, src/modules/Points/draw-points.vert, test/point-render-order.test.ts
Builds render-rank and index buffers and applies them to drawing, picking, label sampling, and the occlusion core pass. The vertex shader uses render rank for depth. Tests cover order resolution, GPU rendering, and host picking.
API documentation and interactive example
src/stories/api-reference.mdx, history/2026/2026-09-30-point-render-order.md, src/stories/points.stories.ts, src/stories/points/render-order/index.ts
Documents the API rules and adds a Storybook example with order presets, click and hover interactions, and controls for culling, translucency, and highlighting.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Graph
  participant GraphData
  participant Points
  participant GPU
  Caller->>Graph: Set point render order
  Graph->>GraphData: Store and resolve input order
  Graph->>Points: Update render-order buffers
  Points->>GPU: Bind buffers for draw, picking, and sampling
  GPU->>GPU: Render points using render ranks
Loading

Suggested reviewers: stukova

Merge Risk: ⚪ Minimal · up to 65bf8

No actionable issue is established; the change is mergeable after normal checks.

Pre-merge checks: Passed 5
Passed Passed checks: 5 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the primary change: adding point render-order control without reordering point data.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 6…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed due to a network error.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cosmosgl cosmosgl deleted a comment from coderabbitai Bot Oct 1, 2026
@Stukova
Stukova force-pushed the claude/point-render-order-40eed4 branch from b4f2a5d to 7f72f36 Compare October 7, 2026 11:29
@Stukova
Stukova marked this pull request as ready for review October 7, 2026 13:34
@Stukova

Stukova commented Oct 7, 2026

Copy link
Copy Markdown
Member

Update — 5baacdc: rebase, tests, and two corrections to the description

Rebased onto main at ce88a55 (3.5.0), up from d68aa2a: commits are now 50ebdb4 and 7f72f36 (were f4e2457 and b4f2a5d). git range-diff shows only context lines changed; the patches are the same.

New test/point-render-order.test.ts, 12 tests in pnpm test (real WebGL 2 in headless Chromium). The repo does have a browser test runner; the Testing section's "no unit-test runner" was wrong.

  • Resolver (GraphData directly): partial lists, repeated and invalid indices, orders that collapse to index order (unindexed draw kept), an out-of-range index as the count grows and shrinks, an array edited in place.
  • GPU: every point stacked in one sampling cell, so getSampledPoints reports the point drawn last. Covers the default order, set and null, an order set before the device is ready, count changes, and an order set right before setConfigPartial({ spaceSize }).
  • That last test fails without 7f72f36 (expected 3 to be +0); the others pin behavior that already holds.
  • Full suite on the rebased branch: 104 passed.

Correction to What: getSampledPoints follows the render order but, as on main, gives highlighted points no priority. Only hover and click report the point on top while highlighting is on. The JSDoc already said so; its @note is rewrapped in the same commit.

🤖 Generated with Claude Code

@Stukova
Stukova self-requested a review October 7, 2026 14:13
rokotyan and others added 5 commits October 14, 2026 16:43
Points were always drawn in index order, so the only way to choose what
ends up on top was to reorder every input array, remap the links and
keep a permutation map for callbacks. That also changes the layout,
since the simulation deals its random values by index, and reordering
at runtime zeroes velocities through setPointPositions.

setPointRenderOrder(order) changes only the drawing order. Indices,
the simulation and every other per-point array keep their meaning.

- The order lists indices back to front. A partial list puts the
  listed points on top and keeps the rest underneath in index order;
  a repeated index takes its last position, so appending one brings it
  to the front. Invalid entries are ignored; null, [] or anything that
  resolves to index order keeps the unindexed draw path.
- A uint32 element buffer drives every pass that paints in draw order:
  the point draw, the picking fill and the label-sampling fill. So the
  point on top is the one hover, click and getSampledPoints report.
- The occlusion core pass draws the order reversed, and depth now
  comes from a per-point rank attribute instead of the point index.
  In an indexed draw gl_VertexID is the element value, not the draw
  position, and depth is the only thing the core and fringe passes
  agree on.
- GraphData resolves the order only when it is reassigned or the
  point count changes, since render() runs update() every time.

With no order set, output is pixel-identical to the previous commit.
With an order, it is pixel-identical to the old code drawing the input
physically permuted the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Nikita Rokotyan <nikita@rokotyan.com>
…uns create() first

setConfigPartial({ spaceSize }) and turning enableSimulation on call
create() without first letting GraphData resolve pending input. The
render-order flag was cleared there against the old order, so the next
render() resolved the new order but never uploaded it.

- create() now calls updateRenderOrder() unconditionally. It is a
  reference and size check unless the resolved order changed, so it is
  free on every other create(), and the flag is gone.
- Docs: label sampling follows the render order but, as before, gives
  highlighted points no priority. Only picking reports the point on top
  while highlighting.
- createAtlas gets its JSDoc back, and the occlusion comments say
  "render order" where they said "index order".
- Story: per-point translucency doesn't switch culling off, so the
  readout no longer says it does, and a repeat click moves a point to
  the front instead of counting it twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Nikita Rokotyan <nikita@rokotyan.com>
…p its JSDoc note

Render order had no tests in the suite, so the resolver — plain CPU
code, and the part most likely to regress — and the path where a
config change runs create() before the order is resolved could both
break unnoticed.

- Resolver tests drive GraphData directly: partial lists, repeated and
  invalid entries, orders that collapse to index order (and so keep the
  unindexed draw), an out-of-range index that applies once the point
  count grows and drops when it shrinks, an array edited in place.
- GPU tests stack every point on one spot inside a single sampling
  cell, so getSampledPoints reports the point drawn last: the render
  order read back from the GPU, through the public API.
- One test sets an order right before setConfigPartial({ spaceSize })
  and expects it drawn after the next render(); the rest pin behavior
  that already holds.
- The setPointRenderOrder @note had a sentence spliced onto one line;
  it is rewrapped with the wording unchanged.

Every resolver rule and the create()-before-update path now has a
test that fails if it stops holding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
… picks the point on top

With a render order set, the visible draw, the engine's own picking fill and the
label-sampling fill all paint through the order buffer, but the host picking pass
(`drawToRenderPass` with `picking`, added for deck.gl after this branch was cut) drew
unindexed. A host pick at a pixel covered by several points then reported the
highest index, not the point the visible draw put on top.

The rule: every pass that paints points in draw order binds the order buffer and the
render-rank attribute. The host picking model now gets both at creation and sits in
the `updateRenderOrder()` fan-out, so it is rebound when the order changes and
unbound before the buffer goes.

- `getDrawCommandAttributes()` includes `renderRank`, so the picking model's per-pick
  rebind carries it like every other draw attribute.
- The new test stacks four points under a host pick pass and reads the red byte: it
  fails without this change (`expected 3 to be 1`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
…e host pick pass

The entry was drafted before the branch had tests or a host picking pass to cover, and
still carried the commit placeholder. It now names the four commits, says that a host's
pick pass follows the order like label sampling does, and points at the vitest file.

The note that `draw-points.vert` no longer reads `pointsTextureSize` goes — the host
picking varying reads it again since the rebase.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
@Stukova
Stukova force-pushed the claude/point-render-order-40eed4 branch from 5baacdc to e14de59 Compare October 14, 2026 11:54
@Stukova

Stukova commented Oct 14, 2026

Copy link
Copy Markdown
Member

Update — e14de59: rebase onto 3.5.1, and the host pick pass follows the order

Rebased onto main at ca6d366 (3.5.1), up from ce88a55: commits are now 5d16e38, adb19b9 and b69e398 (were 50ebdb4, 7f72f36 and 5baacdc). One conflict, in draw-points.vert: main's host picking varying and this branch's render-rank depth touch the same lines; both are kept. git range-diff shows only that context change; the patches are the same.

New: 7e558f5 — the host picking pass draws by the render order. Main gained a host picking pass (drawToRenderPass with picking, for deck.gl) after this branch was cut. It is its own model over the draw shader, so it had no order buffer and no renderRank, and drew unindexed: with an order set, a deck host picking a pixel covered by several points got the highest index, not the point the visible draw put on top. The picking model now takes the order buffer and the rank attribute at creation and sits in the updateRenderOrder() fan-out with the other three order-driven passes.

  • New test in test/point-render-order.test.ts: four stacked points under a host pick pass, reading the red byte with no order, [1], then null. It fails without 7e558f5 (expected 3 to be 1).
  • Full suite on the rebased branch: 124 passed. Lint and typecheck clean.

e14de59 fills in the history entry's commit list, mentions the host pick pass, and drops the note that the vertex shader no longer reads pointsTextureSize — the picking varying reads it again since the rebase.

🤖 Generated with Claude Code

Stukova and others added 2 commits October 14, 2026 17:41
… host picks the point it sees

While `highlightedPointIndices` is set, the visible draw and the engine's own picking
fill paint greyed points first and highlighted points on top, so a highlighted point
wins the pixel whatever its place in the draw order. The host picking pass drew every
point in one pass, so a greyed point later in the order won a host's pick over a
highlighted point it was drawn under: deck's hover and click named the point visibly
underneath.

The rule: every picking pass layers the way the visible draw does. The host pass now
draws the same two layers through the same skip uniforms, one pass when nothing is
highlighted; the render order still applies within each layer.

- The host pick test highlights two of four stacked points and reads the pick byte,
  with and without an order that puts a greyed point last: it fails without this
  change (`expected 3 to be 2`).
- The JSDoc and the API reference name the host's pick pass among the picks that
  follow the order and the highlight layering.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
…t layering

The note said a host's pick pass follows the render order only, like label sampling.
Since `704fde4` it layers highlighted over greyed like the visible draw, so the note
says that, and why the one-pass draw was wrong. The commit list gains the fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Stukova Olya <stukova.o@gmail.com>
@Stukova

Stukova commented Oct 14, 2026

Copy link
Copy Markdown
Member

Update — 65bf8e0: the host pick pass layers highlighted over greyed

A fast-forward on e14de59, no rebase.

704fde4 — the host picking pass draws the visible draw's two layers. While highlightedPointIndices is set, the visible draw and the engine's own picking fill paint greyed points first and highlighted points on top, so a highlighted point wins the pixel whatever the order says. The host pick pass drew every point in one pass, so a greyed point later in the order won a deck host's pick over a highlighted point it was drawn under. This predates the branch — main has it with index order — but the branch routes the pass through the order, so it belongs here. The pass now draws the same two layers through the same skip uniforms, one pass when nothing is highlighted; the order applies within each layer.

  • The host pick test highlights two of four stacked points and reads the pick byte, with and without an order that puts a greyed point last. It fails without 704fde4 (expected 3 to be 2).
  • The setPointRenderOrder JSDoc and the API reference name the host's pick pass among the picks that follow the order and the highlight layering.
  • Full suite: 124 passed. Lint and typecheck clean.

65bf8e0 updates the history entry's note and commit list.

🤖 Generated with Claude Code

@Stukova
Stukova merged commit 68f1803 into main Oct 14, 2026
6 checks passed
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.

2 participants