Skip to content

docs: introduction/tutorial rewrite with runnable examples - #136

Merged
petercorke merged 17 commits into
jhavl:mainfrom
petercorke:docs/tutorial-intro
Aug 22, 2026
Merged

docs: introduction/tutorial rewrite with runnable examples#136
petercorke merged 17 commits into
jhavl:mainfrom
petercorke:docs/tutorial-intro

Conversation

@petercorke

Copy link
Copy Markdown
Collaborator

Summary

  • Rewrites docs/source/intro.rst as a proper Quick Start tutorial: creating/positioning shapes, animating with step() and callbacks, sliders, collision checking, assemblies, and scene graphs — each with a runnable, screenshot-illustrated example
  • Adds a new docs/source/mesh.rst (mesh file formats, vertex colors, scene-graph formats, left-handed/Y-up meshes) and docs/source/swift.rst (playback/viewpoint controls, headless operation, velocity control, snapshots, movies, ending a session, notebook operation, Colab, scene graph internals, design philosophy)
  • New/renamed example scripts backing the tutorial: box1.py, box2.py, box_orbit1.py/box_orbit2.py, busy_scene.py, collision.py, two_link_arm_assembly.py, two_link_arm_scenegraph.py, plus supporting assets (examples/assets/) and figures
  • :example: Sphinx role (via extlinks) added so every referenced example file links straight to its source on GitHub
  • Fact-checked against the actual implementation throughout — caught and fixed several inaccuracies along the way (stale line-number references after edits shifted code blocks, a couple of real syntax bugs in inline code samples, a section that claimed assemblies update poses via .T when they actually bypass it entirely and send poses straight to the browser)

This branch is deliberately docs/examples-only — no src/swift/* changes — kept separate from three other small open PRs (#133, #134, #135) so review isn't tangled up with unrelated feature work.

Test plan

  • pytest tests/ — no regressions (one pre-existing, unrelated failure: test_add_path_sends_points_radius_and_linewidth, caused by the installed spatialgeometry PyPI release lagging behind an unreleased Polyline rename)
  • Clean Sphinx build (sphinx-build -E) — zero warnings, every :ref:/:meth:/:class:/:example: cross-reference resolves

🤖 Generated with Claude Code

petercorke and others added 16 commits August 18, 2026 15:46
_sync_legacy() only pulled robot.q/qd -> handle.q/qd (detecting the
deprecated direct-mutation style and adopting it), but never pushed
Swift's own per-step integration (velocity-mode step_v()) back into
robot.q/qd. A control loop that reads robot.q back mid-loop -- the
normal pattern, e.g. RTB's own README p_servo example -- saw a
permanently stale configuration: fkine()/jacobe() never advanced, so
the computed qd never changed either, and the robot just ran the same
joint velocity forever without ever converging or stopping.

Reproduced RTB's exact README example headlessly: without this fix it
hits a 500-step cap never arriving, with panda.q frozen at the initial
value the entire time; with the fix, converges in 43 steps.

Add _push_legacy(), the mirror of _sync_legacy(): once a handle is
confirmed to be in legacy mode (_warned), write handle.q/qd back into
robot._q/_qd after each step_v() integration, keeping the snapshot in
sync so this doesn't retrigger the warning. Gated on _warned (not
unconditional) so a handle never driven the legacy way -- the intended
case, including several handles sharing one plain robot model -- is
untouched, preserving the actual design goal of the refactor.
serve()'s producer/wait_closed race only guards the send side, at the
top of each loop iteration -- built specifically for the idle hold()
case (nothing ever queued, so producer() alone would never notice a
disconnect). expect_message()'s websocket.recv(), where the server
actually spends nearly all its time during an active step() loop
(waiting for the browser's reply to the last message), had no such
race. A disconnect right there -- the common case, e.g. killing the
tab mid-RRMC-loop -- fell through entirely to Swift._send_socket()'s
own _REPLY_TIMEOUT fallback, up to 15s later instead of near-instant.

Worse: even the *existing* idle-disconnect fast path never actually
sped up a concurrently-blocked _send_socket() call -- it only pushes a
throwaway value onto outq (to stop producer()'s own background thread
leaking), never signals inq. So detecting the disconnect faster on the
server's asyncio side alone wouldn't have been visible to a caller
already blocked in inq.get(timeout=_REPLY_TIMEOUT).

Fix, two parts:
- Race expect_message()'s recv() against wait_closed() too, the same
  pattern already used at the top of serve()'s loop.
- Add a threading.Event, set by SwiftSocket the instant either race
  detects a disconnect, and change _send_socket() to poll in short
  (0.05s) slices against it instead of one blocking
  inq.get(timeout=_REPLY_TIMEOUT). Deliberately not a sentinel value
  pushed onto inq itself -- inq persists across a close()/launch()
  reconnect (Swift.__init__ creates it once; launch() never recreates
  it), so a disconnect detected while nothing is waiting on inq (e.g.
  mid-hold()) would otherwise leave a stale value to be wrongly
  consumed by the next session's handshake.

Also reworded the TimeoutError message to state actual elapsed time
rather than a fixed "_REPLY_TIMEOUT" figure, now that it can legitimately
fire well before that.

New test test_disconnect_while_waiting_for_a_reply_is_noticed_quickly
exercises a real client over a real websocket that vanishes mid-reply-
wait -- confirmed it fails (times out at ~15s, disconnect never
detected) against the pre-fix expect_message().
.label-div's default margin (1cm top, 0.5cm bottom) and font-size
(1.3em, bold) are sized for an occasional standalone heading, not
several Labels stacked close together -- e.g. a multi-line live
readout next to a slider panel, where the current spacing looks
disproportionately spacious and pushes other elements off-screen.

No existing example uses Label at all, so nothing currently depends on
today's spacing -- but editing the shared .label-div CSS class directly
would still silently change the look for any future/external caller
that does want the current spacious heading style. compact=True is
opt-in per-instance instead: applied as an inline style override in
the JS constructor, default False leaves the shared class and every
other Label untouched.
Adds two new intro.rst sections after "Playback controls": ending a
session (hold()/run()/close(), all interrupt-safe since ^C is treated
as the normal way to end an interactive session), and notebook
operation (browser="notebook", close(clear_cell=True)) plus a Colab
section summarizing tech-debt.md's investigation for anyone who hits
the same wall without reading the full writeup.

Also points docs/notebooks/swift.ipynb's install cell at jhavl/swift's
future branch instead of the now-merged feat/browser-lifecycle fork
branch it was temporarily pinned to.
Setting aside to unblock the future->main merge for the coordinated
SG/swift/RTB release. To be split into proper doc:/feat: commits later.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Quick-start section now walks through a minimal box example matching
examples/box1.py, with a screenshot. examples/box.py becomes the
screenshot-generating variant (adds step()/screenshot() before hold());
box1.py stays the clean, copy-pasteable version the docs embed.
…mmit

The previous commit's intro.rst was copied wholesale from a different,
older/shorter version of this file on another branch, silently dropping
the "Displaying shapes"/mouse-controls/animating-shapes sections this
branch already had. Restored those, keeping only the actual intended
edit (Quick start section + box1.png).
# Conflicts:
#	examples/box_sliders.py
#	examples/panda_ik_sliders.py
#	examples/two_link_arm.py
#	src/swift/SwiftElement.py
#	tests/test_swift_element.py
…scene

These were a stopgap to capture doc screenshots and were never meant
to stay in the example scripts. env.screenshot() also has no headless
guard (unlike set_lights()), so it hangs 15s and raises TimeoutError
under SWIFT_HEADLESS -- discovered while building an examples smoke
test on a sibling branch.
@petercorke
petercorke force-pushed the docs/tutorial-intro branch from b92058a to 492adb1 Compare August 22, 2026 05:58
@petercorke
petercorke merged commit f137e0b into jhavl:main Aug 22, 2026
2 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.

1 participant