Modernize CANVAS: data-driven object registry on a Vite + TypeScript editor#106
Open
kalebphipps wants to merge 12 commits into
Open
Modernize CANVAS: data-driven object registry on a Vite + TypeScript editor#106kalebphipps wants to merge 12 commits into
kalebphipps wants to merge 12 commits into
Conversation
…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.
for more information, see https://pre-commit.ci
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




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.
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/:Frontend dev with hot reload (optional): run
npm run dev(Vite dev server) and start Django withDJANGO_VITE_DEV=1set; django‑vite then loads modules from the dev server with HMR. Without that env var, Django serves the built assets fromstatic/dist/via the manifest, so you mustnpm run buildafter pulling.Useful scripts:
npm run typecheck·npm run lint·npm test(vitest) ·npm run build.What changed
A. Performance & reliability (commit
0bfd239,358eab3)requestAnimationFrameloop 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.16384²(~1 GB VRAM, exceeded many GPUs) to4096²(~64 MB).PUTper entity.undefined; failures throw and are logged, and empty204bodies are handled.B. Dependencies (commit
0bfd239,9de08d8,9b18f60)requirements.txtpinned to compatible ranges with upper caps (was fully unpinned). Verified to resolve & install on Python 3.11.h5pyexplicitly (was only transitively present via ARTIST) anddjango-vite(serves the bundle).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.settings.ARTIST_SCENARIO_ENABLED = False.download_viewandcreate_project_formnow importHDF5Managerlazily and short‑circuit when disabled (HTTP 503 for export; a clear validation error for.h5upload).D. Backend rewrite — generic, registry‑driven (commit
2f68d88) (BREAKING)SceneObjectmodel (project,type,name,properties: JSONField) replaces theHeliostat/Receiver/LightSourcetables.Project/Settingsunchanged. Fresh migration0002.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 aSceneObject’sproperties.SceneObjectSerializer+ genericSceneObjectList/SceneObjectDetailviews replace the six per‑type view files.GET /api/object-types/exposes the registry so the frontend renders generically./api/projects/<id>/heliostats/[<pk>/],…/receivers/…,…/light_sources/…/api/projects/<id>/objects/[<pk>/](+/api/object-types/)GET /api/projects/<id>/now returns{ name, objects, settings }.scene_objectsrelation.E. Frontend rewrite — Vite + TypeScript, DI, reactive (commits
9b18f60,b3e440b,f6a050b,2a8a768,6f134f6)typescript-eslint+ Vitest; Three.js/Bootstrap/@preact/signals-coreare bundled. CDN importmap removed.django-viteintegrates the build (dev HMR + prod manifest). Source lives incanvas_editor/frontend/.editor/createEditor.tsconstructs and wires every service — nogetInstance()singletons.objects/SceneObject.ts(reactive signals per field;positionauto‑syncs the mesh),objects/ObjectFactory.ts,objects/ObjectStore.ts, and genericcommands/commands.ts(Create/Delete/Duplicate/UpdateProperty/Rename+UndoRedoService) — replacing the 12 per‑type command classes.@preact/signals-core):ui/Inspector.tsre‑renders only on selection change and patches field inputs in place (focus/caret preserved);ui/Overview.tsrebuilds only on add/remove;ui/Placement.tsshows one “add” button per registered type.rendering/Renderer.ts,rendering/meshFactory.ts): on‑demand renderer + GLB cache/clone and procedural primitives, driven by the registry mesh descriptor.editor/templates/editor/editor.htmlis now a thin mount point; the entire legacystatic/js/editor+static/js/commandstrees and the old editor templates were deleted (net −7,099 lines).F. Editor features restored / modernized (commit
7b90acf)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).ViewHelperintegrated into the renderer as an N/U/E overlay with on‑demand click‑to‑snap animation.ui/Keybindings.ts,?).1..nadd the nth registered type.ui/JobInterface.ts): list/create/poll/delete render jobs against the (mocked)/jobs/API.core/theme.ts+core/csrf.ts(ApiClient now reusesgetCookie, addressing ReusegetCookiefunction and rename variablexsrfCookies#75).G. Project overview page ported (commit
7b90acf)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.htmlnow loads via django‑vite (CDN importmap + legacyprojectOverviewManager.mjsremoved; staleeditor.cssreference removed).H. Tests (commit
4d3e889,7b90acf)autosave_apitests rewritten for the generic endpoint + registry validation;object-typestest added.test_modelscoversSceneObject+ registry defaulting/validation.SceneObject(signals + mesh sync), commands/undo,ObjectStore, and debounced‑autosave coalescing.Breaking changes (explicit)
Heliostat/Receiver/LightSourcetables dropped for oneSceneObject. No data migration — runmigrateon a fresh DB./api/projects/<id>/objects/+/api/object-types/; project detail payload now exposesobjects.npm install && npm run buildbefore running; the CDN importmap is gone.ARTIST_SCENARIO_ENABLEDuntil the integration is rebuilt.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, thetarget_pointincluded in this PR: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), andvite build(two entries) all clean.heliostat.glbfetch for 6 heliostats; 0 idle rAF; created atarget_pointfrom 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.Known limitations / follow‑ups
torchis only used by that path.base.htmlunchanged); only the editor and projects pages are on the bundle.Export projectcommand is wired but currently hits the disabled 503 endpoint.How to review
The 11 commits are layered; suggested order:
0bfd239,358eab3— perf/reliability (small, high‑confidence).9de08d8— dependency pins + ARTIST disable.2f68d88— backend unification (the data model + registry; the conceptual core).9b18f60→6f134f6— frontend toolchain → foundation → objects → UI → page wiring/cleanup.4d3e889— tests.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