Ebene-Add Race: SET_VISIBILITY-Roundtrip cancelt APPLY → neuer Eintrag weg

Root-Cause für 'Ebene erscheint kurz und verschwindet wieder, kein
APPLY im PY-Log':

1. User klickt + → addNew. setEbenen(18 Eintraege). state local = 18.
2. visibilityKey aendert sich (ebenen-Aenderung) → applyVisibility
   debounced 30ms.
3. structureKey aendert sich → applyAll debounced 200ms.
4. T=30ms: SET_VISIBILITY landet beim Backend ZUERST.
5. `_apply_visibility` liest e_full (17 alte Eintraege) aus
   doc.Strings, merged Visibility-Flags vom Slim-Payload, schreibt
   die 17 ALTEN zurueck nach doc.Strings (der neue 18. Eintrag ist
   im merged-Loop nicht dabei weil iteriert ueber e_full).
6. broadcast STATE_SYNC mit 17 Eintraegen.
7. React-App empfaengt → setEbenen(17) → neue Ebene weg aus state.
8. structureKey wieder == appliedStructureKey → useEffect's
   clearTimeout cancelt den 200ms-applyAll-Timer.
9. APPLY feuert nie. Backend bleibt auf 17.

Fix in _apply_visibility: detect pending structural change (Payload
hat IDs/Codes die noch nicht in doc.Strings sind) und in dem Fall
das SetString-Save UND den _broadcast_state ueberspringen.
apply_visibility (Rhino-Layer-Visibility-Update) laeuft trotzdem
mit dem merged-state — die noch nicht gespeicherte Ebene hat eh
keinen Rhino-Layer und damit keine Visibility zu setzen.

Sobald der 200ms-applyAll feuert: build_layers + Save bringt alles
in Sync. Daraufhin broadcastet APPLY normal an beide Panels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 00:49:36 +02:00
parent 581f366437
commit fcbc97b608
+21 -1
View File
@@ -350,6 +350,20 @@ class EbenenBridge(panel_base.BaseBridge):
return return
payload_z = p.get("zeichnungsebenen") or [] payload_z = p.get("zeichnungsebenen") or []
payload_e = p.get("ebenen") or [] payload_e = p.get("ebenen") or []
# Strukturelle Aenderung pending? Wenn React-Payload IDs/Codes enthaelt
# die noch nicht in doc.Strings sind (= User hat gerade neue Ebene
# angelegt aber der strukturelle APPLY ist noch in der 200ms-Debounce),
# NICHT speichern. Sonst ueberschreibt die schnellere SET_VISIBILITY
# den geplanten APPLY-Save und die neue Ebene geht in der Race
# verloren.
payload_z_ids = {z.get("id") for z in payload_z if isinstance(z, dict)}
payload_e_codes = {e.get("code") for e in payload_e if isinstance(e, dict)}
existing_z_ids = {z.get("id") for z in z_full if isinstance(z, dict)}
existing_e_codes = {e.get("code") for e in e_full if isinstance(e, dict)}
has_new_structural = (
bool(payload_z_ids - existing_z_ids - {None}) or
bool(payload_e_codes - existing_e_codes - {None})
)
z_state = {z["id"]: z for z in payload_z if isinstance(z, dict) and z.get("id")} z_state = {z["id"]: z for z in payload_z if isinstance(z, dict) and z.get("id")}
e_state = {e["code"]: e for e in payload_e if isinstance(e, dict) and e.get("code")} e_state = {e["code"]: e for e in payload_e if isinstance(e, dict) and e.get("code")}
merged_z = [] merged_z = []
@@ -369,6 +383,9 @@ class EbenenBridge(panel_base.BaseBridge):
m["visible"] = s.get("visible", True) m["visible"] = s.get("visible", True)
m["locked"] = s.get("locked", False) m["locked"] = s.get("locked", False)
merged_e.append(m) merged_e.append(m)
if has_new_structural:
print("[EBENEN] _apply_visibility: structural change pending → skip save (waiting for APPLY)")
else:
doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False)) doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False))
doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False)) doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False))
# zMode + eMode persistieren, damit bei Split-Send (nur eine # zMode + eMode persistieren, damit bei Split-Send (nur eine
@@ -386,7 +403,10 @@ class EbenenBridge(panel_base.BaseBridge):
active_code = p.get("activeCode") or doc.Strings.GetValue("dossier_active_code") active_code = p.get("activeCode") or doc.Strings.GetValue("dossier_active_code")
layer_builder.apply_visibility( layer_builder.apply_visibility(
doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode) doc, merged_z, merged_e, active_z_id, active_code, z_mode, e_mode)
# Cross-Panel-Sync # Cross-Panel-Sync NUR wenn wir nicht in einem structural-pending
# State sind — sonst broadcasten wir die unvollstaendige Liste und
# React ueberschreibt die gerade vom User hinzugefuegte Ebene.
if not has_new_structural:
_broadcast_state(doc) _broadcast_state(doc)
def _set_active_zeichnungsebene(self, z): def _set_active_zeichnungsebene(self, z):