Skip to content

Modernize CANVAS: data-driven object registry on a Vite + TypeScript editor#106

Open
kalebphipps wants to merge 12 commits into
mainfrom
rewrite/modern-editor
Open

Modernize CANVAS: data-driven object registry on a Vite + TypeScript editor#106
kalebphipps wants to merge 12 commits into
mainfrom
rewrite/modern-editor

Conversation

@kalebphipps

Copy link
Copy Markdown
Member

Summary

This PR modernizes CANVAS end‑to‑end: it fixes the worst performance problems, makes dependencies reproducible, disables a broken integration that prevented the app from booting, and then rewrites the editor around a data‑driven object‑type registry on a modern Vite + TypeScript toolchain. The headline outcome: adding a new object type now requires a single registry entry and zero migrations — no new models, serializers, views, commands, or UI.

It is large (11 commits) but each commit is self‑contained and the history reads as a clean progression. See “How to review” at the bottom for a suggested reading order.

⚠️ Breaking: the database schema and the autosave API change shape, the frontend now requires a build step, and the legacy editor JS is deleted. There is no data migration (this was intentional — the app was not deployed). Read “Breaking changes” and “Setup & run” before testing.


Why

The original code base hand‑duplicated every object type (Heliostat / Receiver / LightSource) across ~12 places, used tightly‑coupled getInstance() singletons, rebuilt the inspector DOM on every event, loaded Three.js/Bootstrap from a CDN importmap with no build step, and rendered the 3D scene in a perpetual loop. On top of that, dependencies were unpinned and the bundled ARTIST package’s API had drifted so far that a fresh install could not even start the server.


Setup & run (changed — please read)

The frontend now has a real build step (Vite + TypeScript). Two terminals from canvas_editor/:

# 1) Python deps (now pinned; includes django-vite + h5py)
pip install -r requirements.txt

# 2) Node deps + build the frontend bundle (NEW)
npm install
npm run build            # tsc --noEmit && vite build  → static/dist/ + manifest.json

# 3) Database — schema changed; recreate it (no data to preserve)
python manage.py migrate

# 4) Run the server (serves the built bundle via the Vite manifest)
python manage.py runserver

Frontend dev with hot reload (optional): run npm run dev (Vite dev server) and start Django with DJANGO_VITE_DEV=1 set; django‑vite then loads modules from the dev server with HMR. Without that env var, Django serves the built assets from static/dist/ via the manifest, so you must npm run build after pulling.

Useful scripts: npm run typecheck · npm run lint · npm test (vitest) · npm run build.


What changed

A. Performance & reliability (commit 0bfd239, 358eab3)

  • On‑demand rendering: the perpetual requestAnimationFrame loop is gone; the scene draws only on camera/gizmo interaction, data changes, or async asset loads (coalesced to one frame). Idle GPU usage drops to ~0.
  • Shadow map reduced from 16384² (~1 GB VRAM, exceeded many GPUs) to 4096² (~64 MB).
  • GLB caching: each model is fetched/parsed once and cloned per object (shared geometry/materials) instead of re‑downloaded per object.
  • Debounced autosave: rapid edits/drags coalesce into a single PUT per entity.
  • Error surfacing (fixes Potential bug in JSON handling in saveAndLoadHanlder.makeApiCall #101): the autosave client no longer swallows errors and returns undefined; failures throw and are logged, and empty 204 bodies are handled.

B. Dependencies (commit 0bfd239, 9de08d8, 9b18f60)

  • requirements.txt pinned to compatible ranges with upper caps (was fully unpinned). Verified to resolve & install on Python 3.11.
  • Added h5py explicitly (was only transitively present via ARTIST) and django-vite (serves the bundle).
  • Documented that the ARTIST git dependency should be pinned to a tag/SHA.

C. ARTIST HDF5 integration disabled (commit 9de08d8)

The HDF5 scenario export/import (hdf5_management/) was written against an old ARTIST API that no longer exists, and it was imported at URLconf‑load time, so the app couldn’t boot.

  • Added settings.ARTIST_SCENARIO_ENABLED = False.
  • download_view and create_project_form now import HDF5Manager lazily and short‑circuit when disabled (HTTP 503 for export; a clear validation error for .h5 upload).
  • The three HDF5 tests are skipped with a clear reason; a new test asserts the 503.

D. Backend rewrite — generic, registry‑driven (commit 2f68d88) (BREAKING)

  • One SceneObject model (project, type, name, properties: JSONField) replaces the Heliostat/Receiver/LightSource tables. Project/Settings unchanged. Fresh migration 0002.
  • project_management/object_types.py — the single source of truth: each type declares display info, a mesh descriptor, and typed fields (with defaults/limits/options). It validates & defaults a SceneObject’s properties.
  • One SceneObjectSerializer + generic SceneObjectList/SceneObjectDetail views replace the six per‑type view files.
  • New endpoint GET /api/object-types/ exposes the registry so the frontend renders generically.
  • API surface changed:
    • Before: /api/projects/<id>/heliostats/[<pk>/], …/receivers/…, …/light_sources/…
    • After: /api/projects/<id>/objects/[<pk>/] (+ /api/object-types/)
    • GET /api/projects/<id>/ now returns { name, objects, settings }.
  • Project duplicate/share views updated to copy the generic scene_objects relation.

E. Frontend rewrite — Vite + TypeScript, DI, reactive (commits 9b18f60, b3e440b, f6a050b, 2a8a768, 6f134f6)

  • Toolchain: Vite + TypeScript (strict) + typescript-eslint + Vitest; Three.js/Bootstrap/@preact/signals-core are bundled. CDN importmap removed. django-vite integrates the build (dev HMR + prod manifest). Source lives in canvas_editor/frontend/.
  • Dependency injection: editor/createEditor.ts constructs and wires every service — no getInstance() singletons.
  • One generic object path: objects/SceneObject.ts (reactive signals per field; position auto‑syncs the mesh), objects/ObjectFactory.ts, objects/ObjectStore.ts, and generic commands/commands.ts (Create/Delete/Duplicate/UpdateProperty/Rename + UndoRedoService) — replacing the 12 per‑type command classes.
  • Reactive UI (@preact/signals-core): ui/Inspector.ts re‑renders only on selection change and patches field inputs in place (focus/caret preserved); ui/Overview.ts rebuilds only on add/remove; ui/Placement.ts shows one “add” button per registered type.
  • Rendering (rendering/Renderer.ts, rendering/meshFactory.ts): on‑demand renderer + GLB cache/clone and procedural primitives, driven by the registry mesh descriptor.
  • Page shell: editor/templates/editor/editor.html is now a thin mount point; the entire legacy static/js/editor + static/js/commands trees and the old editor templates were deleted (net −7,099 lines).

F. Editor features restored / modernized (commit 7b90acf)

  • Command palette (ui/CommandPrompt.ts): Ctrl+Space, fuzzy filter + highlight, keyboard nav; actions are plain objects (theme, add‑each‑type, undo/redo, fullscreen, export, jobs, shortcuts, logout).
  • Compass: Three’s ViewHelper integrated into the renderer as an N/U/E overlay with on‑demand click‑to‑snap animation.
  • Keybindings modal (ui/Keybindings.ts, ?).
  • Quick selector: number keys 1..n add the nth registered type.
  • Job interface (ui/JobInterface.ts): list/create/poll/delete render jobs against the (mocked) /jobs/ API.
  • Shared core/theme.ts + core/csrf.ts (ApiClient now reuses getCookie, addressing Reuse getCookie function and rename variable xsrfCookies #75).

G. Project overview page ported (commit 7b90acf)

  • New second Vite entry frontend/src/projects.ts + ui/ProjectOverview.ts (favorite toggle + “only favorites” filter). Fixes the favorite‑filter bug (Filter by favorite doen´t work properly anymore #88) — it compared "true" against Django’s "True".
  • projects.html now loads via django‑vite (CDN importmap + legacy projectOverviewManager.mjs removed; stale editor.css reference removed).

H. Tests (commit 4d3e889, 7b90acf)

  • autosave_api tests rewritten for the generic endpoint + registry validation; object-types test added.
  • test_models covers SceneObject + registry defaulting/validation.
  • Frontend Vitest suite: registry, SceneObject (signals + mesh sync), commands/undo, ObjectStore, and debounced‑autosave coalescing.

Breaking changes (explicit)

  1. DB schema: Heliostat/Receiver/LightSource tables dropped for one SceneObject. No data migration — run migrate on a fresh DB.
  2. API: per‑type endpoints replaced by /api/projects/<id>/objects/ + /api/object-types/; project detail payload now exposes objects.
  3. Frontend build required: npm install && npm run build before running; the CDN importmap is gone.
  4. ARTIST HDF5 export/import is disabled behind ARTIST_SCENARIO_ENABLED until the integration is rebuilt.
  5. Removed files: all static/js/editor/**, static/js/commands/**, static/js/projectOverviewManager.mjs, the old editor templates, jsconfig.json, .eslintrc.js, static/css/editor.css.

How to add a new object type (the payoff)

Add one entry to project_management/object_types.py — for example, the target_point included in this PR:

"target_point": ObjectTypeSpec(
    type="target_point",
    label="Target point",
    icon="bi-bullseye",
    mesh={"kind": "primitive", "shape": "octahedron", "size": 2, "color": "#ff4477"},
    fields=(
        _vec("position", "Position", [0.0, 10.0, 0.0]),
        FieldSpec(name="power", label="Power", kind=SLIDER, default=1.0, min=0.0, max=10.0, step=0.1),
    ),
),

That alone gives you: a placement button, a number‑key shortcut, a schema‑driven inspector, validated persistence, undo/redo, and a scene mesh — no model, migration, serializer, view, command, or per‑type frontend code.


Verification performed

In a clean Python 3.11 venv with the full pinned requirements, plus a headless browser (Preview):

  • tsc --noEmit, eslint, vitest (7 passing), and vite build (two entries) all clean.
  • Django suite green: 150 tests, 3 skipped (HDF5).
  • Editor (browser): project loads from the API; objects render with one heliostat.glb fetch for 6 heliostats; 0 idle rAF; created a target_point from config alone (POST 201 → schema‑driven inspector); rapid edits → 1 debounced PUT (200), persisted; undo reverts reactively; command palette, compass, keybindings, jobs (create), and number‑key quick‑add all work. No console errors.
  • Projects page (browser): runs fully off the bundle (no CDN); favorite toggle + filter work.

Known limitations / follow‑ups

  • HDF5 export/import stays disabled — needs a rebuild against current ARTIST (and the ARTIST dep should be pinned to a tag/SHA). torch is only used by that path.
  • Account/login pages still use the CDN (base.html unchanged); only the editor and projects pages are on the bundle.
  • Bundle is ~740 KB (Three.js); code‑splitting is an easy later win.
  • Export project command is wired but currently hits the disabled 503 endpoint.
  • The compass click‑to‑snap is wired and renders; an exhaustive pixel‑level axis‑click test wasn’t done.
  • Coordinate mapping (N/U/E ↔ x/y/z) was carried over by inspection — worth a domain check against ARTIST conventions.

How to review

The 11 commits are layered; suggested order:

  1. 0bfd239, 358eab3 — perf/reliability (small, high‑confidence).
  2. 9de08d8 — dependency pins + ARTIST disable.
  3. 2f68d88backend unification (the data model + registry; the conceptual core).
  4. 9b18f606f134f6 — frontend toolchain → foundation → objects → UI → page wiring/cleanup.
  5. 4d3e889 — tests.
  6. 7b90acf — editor features + projects‑page port.

If a single PR this size is unwieldy, the first three commits (perf + deps + ARTIST disable) are independent and could be split into a precursor PR.

🤖 Generated with Claude Code

kalebphipps and others added 12 commits June 1, 2026 00:22
…ave, pinned deps

Render-on-demand (editor.mjs):
- Replace the perpetual requestAnimationFrame loop with a coalesced
  requestRender() driven by camera/gizmo interaction, selection and data
  events, plus async mesh/skybox loads. The GPU now idles when nothing
  changes instead of redrawing 60x/s.
- Shrink the directional-light shadow map from 16384^2 (~1 GB VRAM, beyond
  many GPUs' limits) to 4096^2 (~64 MB) which still looks crisp here.

GLB caching (canvasObject.mjs):
- loadGltf now fetches and parses each model at most once and clones it per
  object (sharing geometry/materials), instead of re-downloading and
  re-parsing the file for every heliostat/receiver. Dispatches a render
  request once the mesh is attached.

Autosave (saveAndLoadHandler.mjs):
- Debounce per-entity update PUTs (400 ms trailing) so typing in a field or
  dragging an object sends one request instead of one per change.
- Fix #makeApiCall silently swallowing errors and returning undefined (which
  led to `undefined["id"]`): it now surfaces network/HTTP failures and
  handles empty 204 bodies. Fixes issue #101.

Dependencies (requirements.txt):
- Pin previously unpinned packages to compatible version ranges with upper
  bounds so installs cannot silently pull a breaking major; document how to
  generate a hash-locked lock file and the need to pin the ARTIST git dep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Use a bound method reference instead of an assigned arrow function in
#setUpRenderTriggers so eslint (jsdoc/require-jsdoc) reports zero problems.
No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The HDF5 scenario export/import in hdf5_management/ was written against an
older ARTIST API (e.g. `artist.data_parser`) that no longer exists on the
current ARTIST release. Because that module was imported at URLconf/form
load time, a fresh install could not even boot the app.

Until the ARTIST integration is rebuilt:
- Add settings.ARTIST_SCENARIO_ENABLED (default False) as a feature gate.
- editor/views/download_view.py: import HDF5Manager lazily and return HTTP
  503 with a clear message when the feature is disabled, so the app boots
  without a working ARTIST.
- project_management/forms/create_project_form.py: import HDF5Manager lazily;
  reject .h5 uploads with a clear validation error while disabled.
- requirements.txt: add h5py explicitly (was only pulled in transitively via
  ARTIST) and clearly flag that the ARTIST git dep / feature is broken and
  gated off pending a rebuild.

Tests:
- Move the ARTIST/h5py imports off the top of test_download_view so the module
  collects without ARTIST; skip test_download and the two HDF5-import project
  tests with a clear "pending ARTIST rebuild" reason; add test_download_disabled
  asserting the 503.

Verified in a clean Python 3.11 venv with the full pinned requirements: the
app boots with no ARTIST shim and the suite is green (150 tests, 3 skipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffold the modern frontend build for the editor rewrite:
- package.json: Vite + TypeScript + vitest + typescript-eslint; three,
  bootstrap, bootstrap-icons, @preact/signals-core as runtime deps. Drops the
  CDN importmap approach (deps are now bundled).
- vite.config.ts: builds frontend/src into static/dist with a manifest;
  vitest configured under jsdom. tsconfig.json (strict). eslint flat config
  migrated to typescript-eslint.
- frontend/src/main.ts entry stub + a smoke vitest.
- settings.py: register django_vite + DJANGO_VITE config (dev HMR via
  DJANGO_VITE_DEV=1, else served from the built manifest).
- requirements.txt: add django-vite.

Verified: npm install, tsc --noEmit, vite build (emits manifest), eslint, and
vitest all pass; manage.py check is clean with django_vite registered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the three hand-duplicated object models/serializers/views with one
data-driven path:
- New project_management/object_types.py registry: per-type schema (typed
  fields + defaults + limits) plus presentation (icon, mesh). Single source of
  truth; validates/*defaults* a SceneObject's properties.
- models.py: drop Heliostat/Receiver/LightSource for one SceneObject
  (project, type, name, properties JSONField). Keep Project/Settings. Fresh
  migration (no data to preserve).
- autosave_api: one SceneObjectSerializer (validates properties against the
  registry by type) + generic SceneObjectList/Detail views; routes collapse to
  /api/projects/<id>/objects/[<pk>/]. New /api/object-types/ endpoint exposes
  the registry. ProjectDetailSerializer now returns objects+settings. Per-type
  view files removed.
- Adds a brand-new 'target_point' type purely as a registry entry (proof that
  new types need no model/migration/serializer/view).

Verified: makemigrations/migrate/check clean; shell smoke confirms defaulting,
validation, bad-type rejection, and the project-detail payload. ruff clean.
NOTE: object-related tests are intentionally red until Phase 6 rewrites them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ory)

- core/types.ts: shared types mirroring /api/object-types/ and the project payload.
- api/ApiClient.ts: typed autosave client; one generic create/update/delete for
  every type; per-entity debounced updates; surfaced errors + 204 handling
  (carries over the perf-branch fixes).
- rendering/Renderer.ts: scene/camera/controls with strict on-demand rendering
  (coalesced rAF) and a 4096^2 shadow map.
- rendering/meshFactory.ts: GLB cache+clone and procedural primitives, driven by
  the registry mesh descriptor.

tsc --noEmit and eslint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- objects/registry.ts: typed lookup over the fetched object-type registry.
- objects/SceneObject.ts: one generic object (no per-type subclasses); reactive
  signals per field + name; position field auto-syncs the mesh transform.
- objects/ObjectFactory.ts: create/fromData/duplicate from the registry.
- objects/ObjectStore.ts: scene + backend sync; reactive  signal.
- commands/commands.ts: UndoRedoService + generic Create/Delete/Duplicate/
  UpdateProperty/Rename commands (replacing the 12 per-type command classes).

tsc + eslint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…placement)

- editor/Selection.ts: click-to-pick (drag/orbit aware) driving the transform
  gizmo + selection box; reactive `selected` signal.
- ui/fieldControls.ts: schema kind -> control (number/integer/slider/select/
  vector3), bound to the object's signal; never overwrites a focused input.
- ui/Inspector.ts: re-renders only on selection change; field edits patch in
  place (focus preserved) and dispatch generic UpdateProperty/Rename/Duplicate/
  Delete commands.
- ui/Overview.ts: reactive object list; rebuilt only on add/remove, names track
  signals in place, active row follows selection.
- ui/Placement.ts: one "add" button per registered type (new types appear
  automatically).

tsc + eslint clean.
- editor/createEditor.ts: composition root (DI) that constructs and wires every
  service, loads the project from the API, applies settings, and installs gizmo
  drag-commit + keyboard shortcuts (undo/redo/delete/duplicate). No singletons.
- main.ts mounts the app from #editor-root (data-project-id) into the template
  containers; styles/editor.css handles layout.
- editor.html rewritten as a thin mount point: django-vite tags load the bundle,
  a {% csrf_token %} seeds the cookie for autosave, sidebar tabs host the
  inspector/overview and a placement bar hosts the registry-driven add buttons.

Cleanup (big-bang): delete the entire legacy editor frontend
(static/js/editor, static/js/commands, message_dict.js, path_dict.js), the now
-unused editor templates (navbar/sidebar/modals/messageSystem/quickSettings/
modeSelector) and stale tooling configs (jsconfig.json, .eslintrc.js, old
editor.css).

tsc + vite build + eslint clean; manage.py check clean.
- autosave_api tests rewritten for /api/object-types/ and the generic
  /api/projects/<id>/objects/ endpoint: defaulting, validation (bad type,
  bad/unknown property), CRUD, settings, auth.
- test_models: SceneObject + registry defaulting/validation (replacing the
  three per-type model tests); Project/Settings kept.
- Update duplicate/share project views + their tests to copy via the generic
  `scene_objects` relation instead of the removed per-type accessors.
- Fix remaining test setups (download, project page, job interface) to create
  SceneObjects.
- frontend vitest: registry, SceneObject (signals + mesh sync), command/undo,
  ObjectStore, and debounced-autosave coalescing. eslint: honor _-prefixed
  unused args.

Full Django suite green (148 tests, 3 skipped); vitest 7/7; tsc + eslint clean.
…+ port projects page

Re-implement the editor features that weren't carried into the rewrite, plus
move the project list page onto the Vite/TS pipeline.

Editor (frontend/src):
- rendering/Renderer: integrate three's ViewHelper as an N/U/E orientation
  compass (overlay render + on-demand click-to-snap animation); Selection
  defers canvas clicks to the compass.
- ui/CommandPrompt: fuzzy command palette (Ctrl+Space) driven by plain action
  objects; ui/Keybindings: shortcuts modal ("?"); ui/JobInterface: list/create/
  poll/delete render jobs (mocked backend).
- Quick selector: number keys 1..n add the nth registered type.
- createEditor wires these + theme commands + export/logout; editor.html gains
  Commands/Jobs/? nav buttons. core/theme + core/csrf shared helpers (ApiClient
  now reuses getCookie).

Project overview page:
- New second Vite entry frontend/src/projects.ts + ui/ProjectOverview (favorite
  toggle + "only favorites" filter; fixes the True/true filter bug, #88).
- vite.config builds editor + projects entries; projects.html served via
  django-vite (drops the CDN importmap + legacy projectOverviewManager.mjs);
  removed the stale editor.css reference.

Verified (browser, Preview): palette opens/filters/runs; compass renders and
snaps; keybindings + jobs modals work (job create hits the mocked API);
number-key quick-add works; projects page runs fully off the bundle (no CDN)
with working favorite toggle/filter. tsc + eslint + vitest + vite build clean;
Django suite green.
@sonarqubecloud

sonarqubecloud Bot commented Jun 5, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
E Security Rating on New Code (required ≥ A)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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.

Potential bug in JSON handling in saveAndLoadHanlder.makeApiCall

1 participant