Skip to content

Visual Designer V2 - #100

Draft
jonasniesner wants to merge 2 commits into
OpenDisplay:feat/clean-portfrom
jonasniesner:feat/clean-port
Draft

Visual Designer V2#100
jonasniesner wants to merge 2 commits into
OpenDisplay:feat/clean-portfrom
jonasniesner:feat/clean-port

Conversation

@jonasniesner

Copy link
Copy Markdown
Member

Add a visual designer like initially proposed in #44 but based on the work of @schlomo in https://github.com/schlomo/odl-drawcustom-designer

@jonasniesner

Copy link
Copy Markdown
Member Author

It also addresses #97

@schlomo

schlomo commented Aug 15, 2026

Copy link
Copy Markdown

Disclosure: researched and drafted by Schlomo's AI agent (Claude), reviewed and
approved by Schlomo.

Thanks for this PR — we reviewed it in depth and the embedding is textbook: byte-identical
v1.0.2 release artifact, lock/virtual-display semantics exactly as documented, correct
HostCapabilities mapping, proper LICENSE/NOTICE handling. It also became the best
requirements document our embed contract ever got.

What the next releases bring you

We've turned our conversation into the designer's next milestone
(ADR-018):
the designer owns the whole UI, your panel shrinks to a thin data adapter. Target end
state on your side:

import { mount } from 'odl-drawcustom-designer';   // npm package (#103)

const handle = mount(container, {
  targets,          // [{id, label, capabilities}] — display picker in the designer (#106)
  states,           // live states with HA friendly names (#107)
  actions: [{ id: 'send', label: 'Send to display' }],   // your buttons, our chrome (#108)
  onAction: (id, payload, targetId) => hass.callService(...),
  renderPreview: (payload, targetId) => ,   // drawcustom dry-run → overlay (#109)
});
handle.getPayload();   // read the design directly — no Save-button scraping (#104)
  • Your toolbar, _clickSave(), and the hand-maintained cache token all become
    unnecessary: entity autocomplete + missing-entity highlighting and a
    referenced-states panel land with
    #107 (the State
    Simulator switches off in host-fed mode),
    #104 exposes the
    payload, and the npm import
    (#103) lets your
    build emit a content-hashed filename — cache invalidation solved properly.
  • "Send to display" becomes your explicitly-registered action
    (#108), decoupled
    from Save, with auth/admin semantics on your side. Service options
    (background/dither/rotate/ttl) travel with the design
    (#105).
  • Your dry-run overlay idea is the preview seam
    (#109) — and our
    pixel-parity reference at the same time. High-frequency hass pushes get diffed on
    our side (#110).

We'll sequence around what unblocks you first — #103 (npm) and #104 (getPayload) look
like the natural openers. Tell us what you'd pull first.

schlomo added a commit to schlomo/odl-drawcustom-designer that referenced this pull request Aug 16, 2026
…frequency setStates

Upstream PR OpenDisplay/Home_Assistant_Integration#100 pushes the full HA
entity registry via setStates up to 4x/s, even on ticks where nothing
changed. useProjectState.applyStates previously replaced mockStates/
mockAttributes wholesale on every push, re-evaluating templates and
re-rendering the canvas continuously.

- hostStatesEqual (src/embed/hostContract.ts): cheap structural equality
  over the raw host states payload — linear scan, short-circuits on the
  first difference, no intermediate allocation. An identical push now
  returns before any conversion, setState, or re-render happens.
- mockStatesEqual / mergeMockAttributes: for a push that does change
  something, bail per-part via the setState functional-updater form (React
  skips re-rendering a part whose updater returns the same reference), and
  reuse each unaffected entity's attribute object so any future per-entity
  memoization (e.g. a referenced-states panel row, ADR-018) can skip work.
- useProjectState.ts: a new lastHostStatesRef, set synchronously inside
  applyStates itself (the ref-paired-with-setter convention used elsewhere
  in this file), gates the fast path.

Investigated interaction safety (issue #110 requirement 3): a host push
during an active canvas drag does not disrupt it. DesignerCanvas's drag
state is React state mirrored into a ref for the hot path; the drag-start
element snapshot and pointer-move math never read live elements/props;
the rendered element stack is frozen for the drag's duration
(frozenElements). A mockStates/mockAttributes push cannot reach any of
that — proven with a test rather than adding deferral machinery for a
non-problem.

Tests (red-first): tests/embed/host-states.test.ts gains direct unit
coverage for the three new pure helpers (verified failing on pre-fix code
via git stash). tests/embed/host-states-push-diff.test.ts exercises
useProjectState directly (renderHook + a stub DesignerHost capturing the
push target, same pattern as tests/ui/hooks/use-project-state-history.test.ts):
an identical push causes zero additional renders and preserves
previewElements/mockContext identity (red on pre-fix code); a
changed-subset push updates only the changed entity and preserves the
unaffected entity's attribute object identity (red on pre-fix code); a
push mid-coalesced-edit does not disrupt the drag (already green
pre-fix — proves existing resilience, not a new fix).

Full gate green: npm test (1503 tests), npm run lint, npm run build,
npm run build:lib, npm run test:e2e (49 tests).

Closes #110

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
schlomo added a commit to schlomo/odl-drawcustom-designer that referenced this pull request Aug 16, 2026
…frequency setStates (#114)

* perf(embed): diff host state pushes; bound re-render cost under high-frequency setStates

Upstream PR OpenDisplay/Home_Assistant_Integration#100 pushes the full HA
entity registry via setStates up to 4x/s, even on ticks where nothing
changed. useProjectState.applyStates previously replaced mockStates/
mockAttributes wholesale on every push, re-evaluating templates and
re-rendering the canvas continuously.

- hostStatesEqual (src/embed/hostContract.ts): cheap structural equality
  over the raw host states payload — linear scan, short-circuits on the
  first difference, no intermediate allocation. An identical push now
  returns before any conversion, setState, or re-render happens.
- mockStatesEqual / mergeMockAttributes: for a push that does change
  something, bail per-part via the setState functional-updater form (React
  skips re-rendering a part whose updater returns the same reference), and
  reuse each unaffected entity's attribute object so any future per-entity
  memoization (e.g. a referenced-states panel row, ADR-018) can skip work.
- useProjectState.ts: a new lastHostStatesRef, set synchronously inside
  applyStates itself (the ref-paired-with-setter convention used elsewhere
  in this file), gates the fast path.

Investigated interaction safety (issue #110 requirement 3): a host push
during an active canvas drag does not disrupt it. DesignerCanvas's drag
state is React state mirrored into a ref for the hot path; the drag-start
element snapshot and pointer-move math never read live elements/props;
the rendered element stack is frozen for the drag's duration
(frozenElements). A mockStates/mockAttributes push cannot reach any of
that — proven with a test rather than adding deferral machinery for a
non-problem.

Tests (red-first): tests/embed/host-states.test.ts gains direct unit
coverage for the three new pure helpers (verified failing on pre-fix code
via git stash). tests/embed/host-states-push-diff.test.ts exercises
useProjectState directly (renderHook + a stub DesignerHost capturing the
push target, same pattern as tests/ui/hooks/use-project-state-history.test.ts):
an identical push causes zero additional renders and preserves
previewElements/mockContext identity (red on pre-fix code); a
changed-subset push updates only the changed entity and preserves the
unaffected entity's attribute object identity (red on pre-fix code); a
push mid-coalesced-edit does not disrupt the drag (already green
pre-fix — proves existing resilience, not a new fix).

Full gate green: npm test (1503 tests), npm run lint, npm run build,
npm run build:lib, npm run test:e2e (49 tests).

Closes #110

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

* fix(embed): invalidate host-push cache on local Simulator mock writes

Adjudicated review finding on PR #114: `lastHostStatesRef` (issue #110)
made an identical host push a no-op, but local Simulator writers
(setMockState, addMockEntity, removeMockEntity, setMockAttribute,
renameMockAttribute, removeMockAttribute, clearElements, loadDemo) never
invalidated that cache. A Simulator edit landing between two otherwise-
identical host pushes was then never reconciled back to host truth
(pre-#110 behavior: every push overwrote deterministically).

Each local mock-mutation path now nulls lastHostStatesRef synchronously
in the same callback as its setState call, matching the file's existing
ref-paired-with-setter convention.

Added a red-first behavior test (host push -> Simulator edit -> identical
host push -> mock state reconciles back to host value) with a pointer
that issue #107 will disable the Simulator under host-fed states, making
this a transition-period guarantee.

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

* test(embed): real-browser mid-drag proof for host state pushes; document setStates aliasing contract

Adjudicated Copilot findings on PR #114:

- The hook-level test in host-states-push-diff.test.ts proved state
  isolation (applyStates never touches elements/selectedIndices/history)
  but called the hook's coalescing methods directly, with no real
  DesignerCanvas drag session, frozen-elements overlay, or pointer
  capture. Add tests/e2e/embed-host-push-mid-drag.spec.ts: drives a real
  pointer drag on the demo host page's canvas, pushes setStates() through
  window.designerHandle mid-gesture (pointer still down, DesignerCanvas's
  own cursor:grabbing state confirms the drag session is live), then
  finishes the drag and asserts the element lands at its true final
  position with the pushed state applied afterward. Reworded the
  hook-level test's name/comments to claim exactly what it proves.

- lastHostStatesRef (and the diff) alias the caller's pushed object
  rather than cloning it, so a host that mutates the same object in
  place and re-pushes gets a false "unchanged". Document the ownership
  contract instead of cloning (which would cost what the diff saves):
  docs/embedding.md's `states` section, JSDoc on HostStates/setStates in
  src/embed/types.ts, and a code comment on the ref itself.

Red-first validation for the new e2e spec: confirmed it fails when the
expected post-drag coordinates are inverted to the pre-drag values, and
confirmed it fails when DesignerCanvas is deliberately remounted mid-push
(temporary sabotage key tied to pushed state, reverted after observing
red) — proving the spec genuinely detects a canvas losing its drag
session mid-gesture, not just asserting a tautology.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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