Skip to content

feat(stage): composable 3D stage for robot arms and CNC machines - #18

Merged
gabrielfrasantos merged 10 commits into
mainfrom
claude/epic-bardeen-tvkqbj
Sep 24, 2026
Merged

gabrielfrasantos merged 10 commits into
mainfrom
claude/epic-bardeen-tvkqbj

Conversation

@gabrielfrasantos

@gabrielfrasantos gabrielfrasantos commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Adds ui/stage, a Tier 1 3D view: a stage (camera, lights, grid) plus parts you hang off a joint tree. It draws through the existing 2D Canvas using the painter's algorithm, so there is no Qt, no GPU and no Canvas change, and it is tested with RecordingCanvas.

What's in it

ui/scene (extended)

  • Matrix3 and Transform3: rigid transforms with axis-angle, RPY, DH, AlignZ and a 3×4 row-major import.
  • ClipPolygonNear / ClipSegmentNear clip geometry at the camera's near plane.
  • ViewFrame gains ToView, ProjectView and ProjectWithDepth.
  • OrbitCamera gains pan and Frame(centre, radius).
  • The ground grid is now clipped at the near plane instead of clamped, so it no longer smears across the screen when the eye is close to it. The existing tests are unchanged.

ui/stage (new)

  • Mesh, with hard-edge masks so outlines skip facet seams.

  • Unit primitives: box, cylinder, cone, sphere, capsule, plane.

  • ParseStl / LoadStlFile: binary and ASCII, welding, scale and a triangle cap. Floats are parsed by hand because from_chars<float> is missing from older libc++.

  • SceneGraph: fixed, revolute and prismatic joints with limits, and SetJointValues(q) in joint-creation order.

  • Material with presets (Themed, Matte, Plastic, Metal, Aluminium, Steel, Translucent, Wireframe) and Lighting.

  • Stage holds parts, fixed-size labels, ring-buffer trails and frame markers. StageBuilder adds AddBox, AddCylinder, AddLink, AddJointHousing and friends.

  • StageRenderer renders each face in turn:

    1. culls back faces and clips at the near plane
    2. flat-shades with a key light, a fill from the eye and a highlight
    3. sorts far to near and draws one DrawPolygon per face

    It also emits trails as depth-sorted chunks, then frame markers and labels. Clicks are picked against the last painted frame.

  • StageView : PaintedView:

    • Orbit with left-drag; pan with right-, middle- or shift-drag; zoom with the wheel.
    • Click to pick and select; handlers are registered with SetOnPick and SetOnSelectionChanged.
    • Double-click to frame everything; Escape clears the selection; Home resets the camera.

Other changes

  • Six new theme roles in both palettes: SceneSurface, SceneJoint, SceneEdge, SceneSelection, SceneTrail, SceneLabel.
  • doc/scene3d.md covers usage, the allocation rules and the limitations. ui/stage is added to the Tier 1 and allocation-free lists in AGENTS.md and doc/portability.md.
  • examples/stage is a Qt demo of a six-axis arm and a gantry mill with a toolpath. It builds behind the new UI_BUILD_EXAMPLES option, which is on in the host-qt preset so CI keeps it compiling.
  • sonar-project.properties adds cpp:S6022 ("use std::byte for byte-oriented memory access") to the project-wide ignored rules. Viewing raw bytes as characters, as the ASCII STL reader does, is an ordinary use of char.

Allocation contract

Adding nodes, parts, meshes or trails allocates, and belongs to setup. Moving joints, pushing trail points, relabelling, recolouring and resizing never allocate. The renderer's scratch buffers grow only on the first paint after the stage has grown. ui.stage_allocation_test replaces operator new with a counter, paints 200 frames that change all of those things, and requires zero allocations.

Known limitations

These are documented in doc/scene3d.md:

  • There is no depth buffer, so parts that interpenetrate can sort wrongly.
  • Shading is flat: no textures, no shadows.
  • Translucency is sorted, not order-independent.
  • The default OrbitLimits suit a scene about a metre across.

Testing

  • CI passes on Ubuntu with Qt, macOS and Windows (MSVC), along with Linting, validate-docs and the Tier 1 check.
  • Locally, the host and host-qt presets build and all tests pass: 12 and 13 suites respectively. That includes 92 stage tests and a Qt test that renders a StageView offscreen and selects a part through QtPaintedWidget.
  • scripts/validate-docs.py passes. The guardrail greps are clean: no Qt in Tier 1, no I prefix, no std::format in ui/scene or ui/stage.
  • clang-format is clean.
  • I rendered the demo offscreen and looked at the output.

🤖 Generated with Claude Code

https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo

Matrix3 and Transform3 (axis-angle, RPY, DH, AlignZ, row-major import),
ClipPolygonNear/ClipSegmentNear, ViewFrame::ToView/ProjectView/
ProjectWithDepth, and OrbitCamera pan and Frame. The ground grid now
clips at the near plane instead of smearing once the eye can hover over
it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
A Mesh of shared vertices and tri/quad faces with per-face normals and
hard-edge masks. Primitive builders for a box, cylinder, cone, sphere,
capsule and plane, lathed from one profile helper. ParseStl reads binary
and ASCII STL from a byte span with welding, scale and a triangle cap.
Floats are parsed by hand because from_chars<float> is missing from
older libc++.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
… model

SceneGraph holds parent-before-child nodes with fixed, revolute and
prismatic joints (clamped to limits, settable from a q vector) and
settles world transforms in one lazy pass. Stage owns shared unit
meshes, materials with presets, parts, fixed-size labels, ring-buffer
trails and frame markers; StageBuilder adds boxes, cylinders, links and
joint housings. Six scene colour roles join both palettes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
…picking

StageRenderer transforms each visible part once into view space, culls
back faces (kept for translucent and double-sided materials), clips at
the near plane, flat-shades with a key light, eye fill and Blinn-Phong
highlight, and sorts far to near before one DrawPolygon per face. Hard
edges outline without facet seams, trails emit as depth-sorted 16-segment
polylines, and frame markers and labels draw on top. Pick() reuses the
last frame's sorted polygons. Scratch buffers grow only when the stage
does; a dedicated suite counts operator new to prove steady-state paint
is allocation-free.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
Owns the Stage, renderer and orbit camera. Left-drag orbits; right-,
middle- or shift-drag pans; the wheel zooms; a click inside the slop
picks and selects (onPick, onSelectionChanged); double-click frames the
scene; Escape clears the selection and Home resets the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
doc/scene3d.md covers building a scene, interaction, the rendering
pipeline, the allocation contract and the painter's-algorithm limits.
ui/stage joins the Tier 1 and allocation-free lists. A Qt test renders
a StageView offscreen and selects a part through the Qt adapter.
examples/stage (UI_BUILD_EXAMPLES, on in the host-qt preset so CI keeps
it compiling) shows a six-axis arm and a gantry mill with a toolpath.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

✅⚠️MegaLinter analysis: Success with warnings

Descriptor Linter Files Fixed Errors Max errors Warnings Elapsed time
✅ ACTION actionlint 7 0 0 0.69s
✅ CPP clang-format 158 0 0 0 2.03s
✅ CPP cppcheck 158 0 0 2.03s
✅ DOCKERFILE hadolint 1 0 0 0.39s
✅ JSON jsonlint 8 0 0 0.11s
✅ JSON prettier 8 2 0 0 0.34s
✅ MARKDOWN markdownlint 14 0 0 0 0.91s
✅ MARKDOWN markdown-table-formatter 14 0 0 0 0.18s
⚠️ SPELL lychee 60 1 0 1.63s
✅ YAML prettier 11 0 0 0 0.48s
✅ YAML yamllint 11 0 0 0.59s

Detailed Issues

⚠️ SPELL / lychee - 1 error
📝 Summary
---------------------
🔍 Total...........23
🔗 Unique...........9
✅ Successful......15
⏳ Timeouts.........0
🔀 Redirected.......1
👻 Excluded.........7
❓ Unknown..........0
🚫 Errors...........1
⛔ Unsupported......1

Errors in .github/workflows/static-analysis.yml
[403] https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$ (at 37:21) | Rejected status code: 403 Forbidden

Hint: Followed 1 redirect. You might want to consider replacing redirecting URLs with the resolved URLs. Use verbose mode (`-v`/`-vv`) to see redirection details.
Hint: You can configure accepted/rejected response codes with `-a` or `--accept`

Notices

⚠️ Your configuration references items that have been removed from MegaLinter and are ignored: REPOSITORY_GITLEAKS, REPOSITORY_KICS. See Removed linters to find their replacements.

See detailed reports in MegaLinter artifacts

Your project could benefit from a custom flavor, which would allow you to run only the linters you need, and thus improve runtime performances. (Skip this info by defining FLAVOR_SUGGESTIONS: false)

  • Documentation: Custom Flavors
  • Command: npx mega-linter-runner@10.0.0 --custom-flavor-setup --custom-flavor-linters ACTION_ACTIONLINT,CPP_CPPCHECK,CPP_CLANG_FORMAT,DOCKERFILE_HADOLINT,JSON_JSONLINT,JSON_PRETTIER,MARKDOWN_MARKDOWNLINT,MARKDOWN_MARKDOWN_TABLE_FORMATTER,SPELL_LYCHEE,YAML_PRETTIER,YAML_YAMLLINT

MegaLinter is provided by OX Security
Show us your support by starring ⭐ the repository

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
Explicit has_value() on optionals, hidden-friend operators for Matrix3
and Transform3, std::byte for the hard-edge mask, emplace_back for scene
nodes, a template instead of a function pointer for shared meshes,
std::ranges::sort, std::size on generic containers, a const Refresh(),
using enum for mouse buttons, and a plain while loop in the ASCII STL
reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
StageView's pick and selection callbacks become private members set
through SetOnPick and SetOnSelectionChanged, so the class no longer
mixes public and private data. cpp:S6022 joins the project-wide Sonar
ignores: viewing bytes as text and small bit masks don't need std::byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVVrF6XXfUvyzubDXtGVo
@sonarqubecloud

Copy link
Copy Markdown

@gabrielfrasantos
gabrielfrasantos merged commit 1340663 into main Sep 24, 2026
10 checks passed
@gabrielfrasantos
gabrielfrasantos deleted the claude/epic-bardeen-tvkqbj branch September 24, 2026 08:56
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