screengraft 0.41.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screengraft",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
4
4
  "description": "Put a UI screenshot or screen recording onto a photographed device screen with the perspective exactly right \u2014 a homography you confirm by hand, not a generative guess.",
5
5
  "keywords": [
6
6
  "mockup",
package/scripts/ui.py CHANGED
@@ -702,6 +702,10 @@ def _adopt(role, path):
702
702
  return {"path": real, "size": [im.shape[1], im.shape[0]], **meta}
703
703
 
704
704
 
705
+ class RenderBusy(Exception):
706
+ """A request that must not run while a render is in flight."""
707
+
708
+
705
709
  def _clear(role):
706
710
  """Un-choose a source. The mirror of _adopt(), and the only way to start
707
711
  over: picking a different file was the one exit, and a reload restores the
@@ -719,6 +723,13 @@ def _clear(role):
719
723
  """
720
724
  if role not in ROLES:
721
725
  raise ValueError(f"role must be one of {', '.join(ROLES)}")
726
+ # A render in flight would publish its output and sidecar AFTER this
727
+ # cleared them, from a source that is no longer loaded -- the exact stale
728
+ # artefact this exists to prevent. Refused, the way the render route
729
+ # refuses a second render; the page shows the message.
730
+ with RENDER_LOCK:
731
+ if RENDER["state"] == "running":
732
+ raise RenderBusy("a render is running — wait for it, then remove the source")
722
733
  if role == "photo":
723
734
  SESSION.update(photo=None, corners=None, output=None)
724
735
  else:
@@ -897,7 +908,10 @@ class Handler(BaseHTTPRequestHandler):
897
908
  return self._json(_adopt(b["role"], b["path"]))
898
909
 
899
910
  if u.path == "/api/clear":
900
- return self._json(_clear(b.get("role")))
911
+ try:
912
+ return self._json(_clear(b.get("role")))
913
+ except RenderBusy as exc:
914
+ return self._json({"error": str(exc)}, 409)
901
915
 
902
916
  if u.path == "/api/figma":
903
917
  return self._json(SESSION.enqueue({
package/ui/index.html CHANGED
@@ -1490,6 +1490,11 @@ async function clearSource(role){
1490
1490
  // Nothing to draw on: the canvas goes back to its empty state rather
1491
1491
  // than showing the last photograph with no way to act on it.
1492
1492
  const c = $('#c'); c.getContext('2d').clearRect(0, 0, c.width, c.height);
1493
+ // ... and the edge view, which would otherwise keep showing a strip of a
1494
+ // photograph that is no longer here.
1495
+ for (const id of ['strip', 'stripF']) {
1496
+ const k = $('#' + id); if (k) k.getContext('2d').clearRect(0, 0, k.width, k.height);
1497
+ }
1493
1498
  $('#cvbar').hidden = true;
1494
1499
  $('#detSt').className = 'status';
1495
1500
  $('#detSt').textContent = 'Photo removed. A fit you saved for it comes back when you pick it again.';
@@ -1501,6 +1506,10 @@ async function clearSource(role){
1501
1506
  : 'Screenshot removed.';
1502
1507
  }
1503
1508
  markStale();
1509
+ // The control that had focus has just hidden itself; without this, focus
1510
+ // drops to <body> and a keyboard user is nowhere. The empty chip is the
1511
+ // natural next action.
1512
+ chip.focus();
1504
1513
  }
1505
1514
  document.querySelectorAll('.chipclear').forEach(b => { b.onclick = () => clearSource(b.dataset.role); });
1506
1515