Skip to content

Qt-removal R1: canvas core - React Flow scene on the WS backend#100

Merged
dovvnloading merged 4 commits into
mainfrom
qt-removal/r1-canvas-core
Jul 22, 2026
Merged

Qt-removal R1: canvas core - React Flow scene on the WS backend#100
dovvnloading merged 4 commits into
mainfrom
qt-removal/r1-canvas-core

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Phase R1 of the Qt/PySide6 full-removal plan

Stacked on #99 (R0 foundations) — merge #99 first and this diff auto-shrinks to the R1 changes. (GitHub rejected the stacked-base PR with its intermittent 'no commits between' error, same as PR #98; base is main as the workaround.)

The QGraphicsScene successor: a React Flow canvas in the SPA, driven by a backend-held scene document over the WebSocket bus.

Backend

  • backend/canvas.pySceneDocument (nodes/edges/pins/settings) sourced from the surviving Qt-free models: GridViewSettings republished on the grid-control topic with intent names 1:1 with GridControlBridge's @Slots (making the R2 control-island port a transport swap), and NavigationPinStore reused verbatim for pins. Full intent surface; edges die with either endpoint; duplicate connects are idempotent. 11 new tests.
  • graphlink_scene_payload.py + codegen entry — scene wire contract → generated TS type/validator, topics.ts regenerated.

Frontend (web_ui/src/app/canvas)

  • SceneStore — framework-free, validator-guarded snapshot store + intent surface (9 tests, incl. the scaleDragPosition drag-speed contract).
  • SceneCanvas — React Flow: model-driven grid background (size/style/color/opacity), snap grid, drag-speed scaling from drag start, connect via node handles, selection + Delete/Backspace, minimap, LOD collapse threshold, pins panel (pin view / jump / remove), double-click create.

Live acceptance (native pywebview window) — all per plan R1

  • Created 2 nodes by double-click ✅ · moved one (backend persisted the position) ✅ · connected them by handle drag ✅ · pinned the view, jumped to it, removed it ✅ · minimap tracked throughout ✅
  • Round-trip proof: a fresh WS client received the complete backend-held scene (nodes/edge/pin) — stronger than a page reload ✅
  • Found + fixed during the drive: RF's default dblclick-zoom swallowed the create gesture; placeholder nodes initially lacked connection handles.

Verification (exact CI commands, locally)

  • npm run check exit 0 — 52 files / 553 vitest ✅
  • Full offscreen pytest: 1794 passed

Burn-down: 168 of 168 remaining (R1 adds only Qt-free code; Qt deletion begins as R2+ cuts surfaces over).

dovvnloading and others added 4 commits July 22, 2026 11:15
…down gate

Per doc/QT_REMOVAL_PLAN.md phase R0 (terminal goal: ZERO PySide6/Qt):

- backend/: FastAPI app + session-scoped WebSocket event bus carrying the
  same versioned full-state-snapshot + named-intent wire semantics as the
  IslandBridge layer it succeeds; static serving of the SPA build;
  /api/health; 14 pytest cases (bus + WS endpoint).
- web_ui/src/app: the single-SPA target consolidating the islands
  (GRAPHLINK_ISLAND=app builds to dist/app). WS transport module with
  auto-reconnect/re-subscribe (9 tests), token-driven shell layout, live
  system panel.
- graphlink_island_codegen.py: emits web_ui/src/lib/api-contract/topics.ts,
  the SPA's topic->type/validator registry derived from the same schema
  registry (--check enforced).
- graphlink_desktop.py: NATIVE pywebview/WebView2 window (not a browser,
  zero Qt) that boots the backend and opens the SPA.
- tests/test_no_qt_anywhere.py + qt_burndown.json: THE permanent gate.
  Pin = 168 files importing Qt (90 source + 78 tests); any increase fails,
  any decrease must lower the pin in-commit; flips to zero-mode assertions
  (package uninstalled, undeclared) when the pin reaches 0. backend/,
  web_ui/, and graphlink_desktop.py are held to zero Qt from day one.

Acceptance drive PASSED: native window, SPA served by the Python backend,
live WS ping round-trip 4.1 ms. 17 pytest + 544 vitest green.
Burn-down: 168 of 168 remaining (R0 adds only Qt-free code by design).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… SPA target

- App.tsx: drop the vestigial transportRef (ref write during render -
  react-hooks/refs error); the memoized transport was already the only
  consumer.
- transport.test.ts: comma-expression assertion rewritten as a real
  assertion (no-unused-expressions error).
- test_gl_vars_dev_css.py: src/app is a documented carve-out from the
  dev-only token rule - the SPA has no Qt host injecting :root tokens, so
  the shipped file IS its token source until R2's backend-served theme.
  Islands remain fully guarded.

Local CI parity: npm run check exit 0; full offscreen pytest 1783 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per doc/QT_REMOVAL_PLAN.md phase R1:

- backend/canvas.py: SceneDocument (nodes/edges/pins/settings) sourced from
  the surviving Qt-free models - GridViewSettings republished 1:1 on the
  grid-control topic (intent names match GridControlBridge's @Slots for the
  R2 island port), NavigationPinStore reused verbatim for pins. Full intent
  surface (add/move/remove/connect nodes+edges, pins CRUD, snap, drag
  factor), edges die with either endpoint, duplicate-connect idempotent.
  11 new pytest cases.
- graphlink_scene_payload.py + codegen entry: scene topic wire contract ->
  generated TS type + validator; topics.ts regenerated (20 artifact sets).
- web_ui/src/app/canvas: SceneStore (validated snapshots, framework-free,
  9 vitest cases incl. the scaleDragPosition drag-speed contract) +
  SceneCanvas (React Flow): model-driven grid background, snap grid,
  drag-speed scaling from drag start, connect via node Handles, selection +
  delete keys, minimap, LOD collapse threshold, pins panel (add view pin /
  jump / remove), double-click create (RF dblclick-zoom disabled - it
  swallowed the gesture; found in the live drive).

Live acceptance PASSED in the native pywebview window: created 2 nodes,
moved one (backend persisted 523,215), connected n0->n1 by handle drag,
pinned the view + jumped + removed, minimap tracked throughout; a FRESH WS
client then received the complete backend-held scene - the round-trip
criterion, proven with a second client rather than a reload.

CI parity verified locally: npm run check exit 0 (52 files / 553 vitest),
full offscreen pytest 1794 passed.
Burn-down: 168 of 168 remaining (R1 adds only Qt-free code; deletion starts
as R2+ cuts surfaces over).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e R1 superset

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit 386bb96 into main Jul 22, 2026
2 checks passed
@dovvnloading
dovvnloading deleted the qt-removal/r1-canvas-core branch July 22, 2026 15:46
dovvnloading added a commit that referenced this pull request Jul 22, 2026
…ng the R2 superset

# Conflicts:
#	backend/app.py
#	backend/canvas.py
#	backend/tests/test_app_ws.py
#	backend/tests/test_canvas.py
#	graphlink_app/graphlink_island_codegen.py
#	graphlink_app/graphlink_scene_payload.py
#	web_ui/src/app/App.tsx
#	web_ui/src/app/canvas/SceneCanvas.tsx
#	web_ui/src/app/canvas/sceneStore.test.ts
#	web_ui/src/app/canvas/sceneStore.ts
#	web_ui/src/app/styles.css
#	web_ui/src/lib/bridge-core/generated/scene-state.schema.json
#	web_ui/src/lib/bridge-core/generated/scene-state.ts
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