renforge 0.7.2__tar.gz → 0.7.3__tar.gz

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.
Files changed (155) hide show
  1. {renforge-0.7.2 → renforge-0.7.3}/PKG-INFO +1 -1
  2. {renforge-0.7.2 → renforge-0.7.3}/pyproject.toml +1 -1
  3. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/__init__.py +1 -1
  4. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor.rpy +338 -20
  5. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/locales/en.json +3 -0
  6. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/locales/zh-CN.json +3 -0
  7. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/screens/rf_inspector.rpy +2 -2
  8. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor/coordinator.py +110 -50
  9. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor/source.py +282 -46
  10. renforge-0.7.3/src/renforge/editor_add_runner.py +351 -0
  11. renforge-0.7.3/src/renforge/editor_say_who_position_runner.py +282 -0
  12. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/policy.py +15 -0
  13. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_definitions.py +46 -0
  14. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/interaction.py +32 -0
  15. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/project_analysis.py +3 -3
  16. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tools/live.py +216 -0
  17. {renforge-0.7.2 → renforge-0.7.3}/.gitignore +0 -0
  18. {renforge-0.7.2 → renforge-0.7.3}/LICENSE +0 -0
  19. {renforge-0.7.2 → renforge-0.7.3}/README.md +0 -0
  20. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/__main__.py +0 -0
  21. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/activity_log.py +0 -0
  22. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/assets.py +0 -0
  23. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/autopilot.py +0 -0
  24. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/__init__.py +0 -0
  25. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/artifacts.py +0 -0
  26. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/bridge.rpy +0 -0
  27. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/client.py +0 -0
  28. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/control.py +0 -0
  29. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/brand.png +0 -0
  30. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/chip.png +0 -0
  31. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/panel.png +0 -0
  32. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/panel_head.png +0 -0
  33. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/pill.png +0 -0
  34. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/pill_accent.png +0 -0
  35. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/scroll_thumb.png +0 -0
  36. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/scroll_thumb_hover.png +0 -0
  37. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/seg_on.png +0 -0
  38. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/frames/tools.png +0 -0
  39. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/caret.svg +0 -0
  40. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/exit.svg +0 -0
  41. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/eye.svg +0 -0
  42. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/hand.svg +0 -0
  43. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/lock.svg +0 -0
  44. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/measure.svg +0 -0
  45. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/minus.svg +0 -0
  46. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/move.svg +0 -0
  47. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/panel-collapse-left.svg +0 -0
  48. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/panel-collapse-right.svg +0 -0
  49. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/panel-inspector.svg +0 -0
  50. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/panel-style.svg +0 -0
  51. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/panel-tree.svg +0 -0
  52. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/picker.svg +0 -0
  53. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/plus.svg +0 -0
  54. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/redo.svg +0 -0
  55. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/reset.svg +0 -0
  56. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/save.svg +0 -0
  57. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/search.svg +0 -0
  58. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/select.svg +0 -0
  59. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/text.svg +0 -0
  60. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/editor_assets/icons/undo.svg +0 -0
  61. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/hit_sentinel_spike.rpy +0 -0
  62. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/launcher.py +0 -0
  63. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/rotation_spike.rpy +0 -0
  64. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/screens/rf_canvas_decor.rpy +0 -0
  65. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/screens/rf_hud.rpy +0 -0
  66. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/screens/rf_style.rpy +0 -0
  67. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/screens/rf_toolbar.rpy +0 -0
  68. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/bridge/screens/rf_tree.rpy +0 -0
  69. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/build.py +0 -0
  70. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/captures.py +0 -0
  71. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/cli.py +0 -0
  72. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/dashboard_client.py +0 -0
  73. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/docs.py +0 -0
  74. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/dump.py +0 -0
  75. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor/__init__.py +0 -0
  76. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor/constants.py +0 -0
  77. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor/exceptions.py +0 -0
  78. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor/paths.py +0 -0
  79. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor/runtime.py +0 -0
  80. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor/shadow.py +0 -0
  81. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_align_runner.py +0 -0
  82. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_anchor_runner.py +0 -0
  83. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_animated_runner.py +0 -0
  84. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_bar_resize_runner.py +0 -0
  85. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_bar_runner.py +0 -0
  86. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_button_runner.py +0 -0
  87. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_crop_runner.py +0 -0
  88. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_demo_runner.py +0 -0
  89. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_failed_gate_runner.py +0 -0
  90. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_hit_sentinel_runner.py +0 -0
  91. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_imagebutton_runner.py +0 -0
  92. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_live_common.py +0 -0
  93. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_loop_runner.py +0 -0
  94. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_multiline_textbutton_runner.py +0 -0
  95. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_offset_runner.py +0 -0
  96. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_pos_runner.py +0 -0
  97. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_rotation_runner.py +0 -0
  98. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_runner_status.py +0 -0
  99. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_say_what_position_runner.py +0 -0
  100. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_slider_runner.py +0 -0
  101. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_style_color_runner.py +0 -0
  102. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_task0_runner.py +0 -0
  103. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_vbar_runner.py +0 -0
  104. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_viewport_runner.py +0 -0
  105. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/editor_zorder_runner.py +0 -0
  106. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/effect_wait.py +0 -0
  107. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/image_ops.py +0 -0
  108. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/launch_env.py +0 -0
  109. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/lint.py +0 -0
  110. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/navigation.py +0 -0
  111. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/project.py +0 -0
  112. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/scanner.py +0 -0
  113. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/scene_color.py +0 -0
  114. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/scene_diff.py +0 -0
  115. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/scene_measure.py +0 -0
  116. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/scene_wireframe.py +0 -0
  117. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/sdk.py +0 -0
  118. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/server.py +0 -0
  119. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/session_registry.py +0 -0
  120. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/state_compact.py +0 -0
  121. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/symbols.py +0 -0
  122. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/__init__.py +0 -0
  123. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/content_build.py +0 -0
  124. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/inspection.py +0 -0
  125. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/lifecycle.py +0 -0
  126. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/registry.py +0 -0
  127. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/runtime_state.py +0 -0
  128. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/scenarios.py +0 -0
  129. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_registration/wrappers.py +0 -0
  130. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tool_schema.py +0 -0
  131. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tools/__init__.py +0 -0
  132. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tools/project_ops.py +0 -0
  133. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/tools/static.py +0 -0
  134. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/translation.py +0 -0
  135. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/__init__.py +0 -0
  136. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/activity.py +0 -0
  137. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/errors.py +0 -0
  138. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/graph.py +0 -0
  139. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/poller.py +0 -0
  140. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/server.py +0 -0
  141. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/assets/index-BaR-clnO.js +0 -0
  142. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/assets/index-BpBcjffw.css +0 -0
  143. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/brand/renforge-favicon-16.png +0 -0
  144. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/brand/renforge-favicon-180.png +0 -0
  145. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/brand/renforge-favicon-192.png +0 -0
  146. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/brand/renforge-favicon-32.png +0 -0
  147. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/brand/renforge-favicon-48.png +0 -0
  148. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/brand/renforge-mark.png +0 -0
  149. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/brand/renforge-mascot.png +0 -0
  150. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/static/index.html +0 -0
  151. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/ui/ws.py +0 -0
  152. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/util/__init__.py +0 -0
  153. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/util/files.py +0 -0
  154. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/util/frames.py +0 -0
  155. {renforge-0.7.2 → renforge-0.7.3}/src/renforge/util/subprocess.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renforge
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: MCP server, CLI, and web dashboard for Ren'Py visual-novel development
5
5
  Project-URL: Homepage, https://github.com/alex-jordan547/renforge-mcp
6
6
  Project-URL: Repository, https://github.com/alex-jordan547/renforge-mcp
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "renforge"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "MCP server, CLI, and web dashboard for Ren'Py visual-novel development"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,4 +1,4 @@
1
1
  """RenForge package."""
2
2
 
3
- __version__ = "0.7.2"
3
+ __version__ = "0.7.3"
4
4
  __all__ = ["__version__"]
@@ -381,6 +381,8 @@ init 1090 python:
381
381
  "lock.reason.unsupported": "This source form is not editable yet.",
382
382
  "lock.reason.mismatch": "The live element no longer matches its source.",
383
383
  "lock.reason.ambiguous": "This selection does not resolve to one unique source element.",
384
+ "lock.reason.ambiguous_hit": "Several non-focusable elements overlap here. Move them apart to edit.",
385
+ "lock.reason.statement_kind_mismatch": "This displayable's source form is not a writable add or frame.",
384
386
  "lock.reason.default": "This selection cannot be edited safely.",
385
387
  "tree.title": "SCENE TREE",
386
388
  "tree.items_count": "{count} items",
@@ -458,10 +460,16 @@ init 1090 python:
458
460
  "lock.reason.style_position_variant_unsupported": "Phone/small variant overrides prevent desktop editing.",
459
461
  "inspector.ownership_chain": "OWNERSHIP",
460
462
  "inspector.ownership_style_position": "say.what → style say_dialogue → gui.dialogue_xpos/ypos",
463
+ "inspector.ownership_style_position_who": "say.who → style namebox → gui.name_xpos/ypos",
461
464
  "inspector.global_scope_notice": "⚠ This change affects all standard dialogue lines",
462
465
  }
463
466
  _RF_UI_STRINGS_READY = []
464
467
 
468
+ def _renforge_editor_ownership_style_position_key(position_mode):
469
+ if position_mode == "style_gui_namebox":
470
+ return "inspector.ownership_style_position_who"
471
+ return "inspector.ownership_style_position"
472
+
465
473
  def _renforge_editor_language():
466
474
  import os
467
475
  return (os.environ.get("RENFORGE_EDITOR_LANG") or "").strip() or "en"
@@ -563,6 +571,26 @@ init 1100 python:
563
571
  import types
564
572
  import uuid
565
573
 
574
+ # Tests exec only this init 1100 body. Keep style-gui helpers here, not in 1090.
575
+ _RF_STYLE_GUI_POSITION_MODES = ("style_gui_dialogue", "style_gui_namebox")
576
+
577
+ def _renforge_editor_is_style_gui_position(source_key):
578
+ return (
579
+ isinstance(source_key, builtins.dict)
580
+ and source_key.get("position_mode") in _RF_STYLE_GUI_POSITION_MODES
581
+ )
582
+
583
+ def _renforge_editor_style_gui_preview_widget_id(target):
584
+ """Who writes style namebox. Preview the namebox window, not the text."""
585
+ widget_id = target.get("widget_id") if isinstance(target, builtins.dict) else None
586
+ source_key = target.get("source_key") if isinstance(target, builtins.dict) else None
587
+ if (
588
+ _renforge_editor_is_style_gui_position(source_key)
589
+ and source_key.get("position_mode") == "style_gui_namebox"
590
+ ):
591
+ return "namebox"
592
+ return widget_id
593
+
566
594
  try:
567
595
  import pygame_sdl2 as pygame
568
596
  except Exception:
@@ -613,6 +641,9 @@ init 1100 python:
613
641
  "VBox",
614
642
  "Drag",
615
643
  "Null",
644
+ "Solid",
645
+ "Image",
646
+ "ImageReference",
616
647
  ]
617
648
  )
618
649
 
@@ -989,6 +1020,7 @@ init 1100 python:
989
1020
  "Grid": ("G", "grid", "layout"),
990
1021
  "Image": ("I", "image", "content"),
991
1022
  "ImageReference": ("I", "image", "content"),
1023
+ "Solid": ("I", "add", "content"),
992
1024
  "Transform": ("X", "transform", "transform"),
993
1025
  }
994
1026
  _RF_TREE_MAX_ROWS = 1000
@@ -2125,6 +2157,8 @@ init 1100 python:
2125
2157
  _RF_LOCK_REASON_KEYS = {
2126
2158
  "XPOS_LITERAL_REQUIRED": "lock.reason.xpos_literal_required",
2127
2159
  "YPOS_LITERAL_REQUIRED": "lock.reason.ypos_literal_required",
2160
+ "AMBIGUOUS_HIT": "lock.reason.ambiguous_hit",
2161
+ "STATEMENT_KIND_MISMATCH": "lock.reason.statement_kind_mismatch",
2128
2162
  "STYLE_POSITION_SOURCE_UNRESOLVED": "lock.reason.style_position_source_unresolved",
2129
2163
  "STYLE_POSITION_SOURCE_AMBIGUOUS": "lock.reason.style_position_source_ambiguous",
2130
2164
  "STYLE_POSITION_EXPRESSION_UNSUPPORTED": "lock.reason.style_position_expression_unsupported",
@@ -2345,9 +2379,9 @@ init 1100 python:
2345
2379
  return rect if rect[2] > 0 and rect[3] > 0 else None
2346
2380
 
2347
2381
  class_name = getattr(getattr(node, "__class__", None), "__name__", "unknown")
2348
- # Transform child coordinates require the render matrix, not additive
2349
- # offsets. Keep those text descendants unselectable until proven.
2350
- if class_name == "Transform":
2382
+ # Rotated/zoomed/cropped Transforms need the render matrix. A
2383
+ # position-only Transform is how SL2 places `add`; walk through it.
2384
+ if class_name == "Transform" and not _renforge_editor_transform_is_position_only(node):
2351
2385
  return None
2352
2386
  children = _renforge_editor_children(node)
2353
2387
  if not children:
@@ -2360,7 +2394,7 @@ init 1100 python:
2360
2394
  offset_y = int(offsets[index][1])
2361
2395
  except Exception:
2362
2396
  continue
2363
- elif len(children) == 1 and class_name == "ScreenDisplayable":
2397
+ elif len(children) == 1 and class_name in ("ScreenDisplayable", "Transform"):
2364
2398
  offset_x = offset_y = 0
2365
2399
  else:
2366
2400
  # No rendered placement proof for this branch.
@@ -2919,6 +2953,25 @@ init 1100 python:
2919
2953
  return found
2920
2954
 
2921
2955
 
2956
+ def _renforge_editor_drop_descendant_candidates(raw):
2957
+ """Keep ancestors; drop children even when the child is processed first."""
2958
+ covered = set()
2959
+ for candidate in raw:
2960
+ widget = candidate.get("focused_widget")
2961
+ if widget is None:
2962
+ continue
2963
+ descendants = _renforge_editor_descendant_ids(widget)
2964
+ descendants.discard(id(widget))
2965
+ covered.update(descendants)
2966
+ kept = []
2967
+ for candidate in raw:
2968
+ widget = candidate.get("focused_widget")
2969
+ if widget is not None and id(widget) in covered:
2970
+ continue
2971
+ kept.append(candidate)
2972
+ return kept
2973
+
2974
+
2922
2975
  def _renforge_editor_resolve_widget_id(screen_name, widget):
2923
2976
  if widget is None or not isinstance(screen_name, str):
2924
2977
  return None, None, "MISSING_WIDGET"
@@ -2967,6 +3020,26 @@ init 1100 python:
2967
3020
  return found
2968
3021
 
2969
3022
 
3023
+ def _renforge_editor_transform_is_position_only(node):
3024
+ """True when a Transform only places its child (no rotate/zoom/crop)."""
3025
+ rotate = getattr(node, "rotate", None)
3026
+ zoom = getattr(node, "zoom", None)
3027
+ xzoom = getattr(node, "xzoom", None)
3028
+ yzoom = getattr(node, "yzoom", None)
3029
+ crop = getattr(node, "crop", None)
3030
+ if rotate not in (None, 0, 0.0):
3031
+ return False
3032
+ if zoom not in (None, 1, 1.0):
3033
+ return False
3034
+ if xzoom not in (None, 1, 1.0):
3035
+ return False
3036
+ if yzoom not in (None, 1, 1.0):
3037
+ return False
3038
+ if crop not in (None, False):
3039
+ return False
3040
+ return True
3041
+
3042
+
2970
3043
  def _renforge_editor_transform_crop_is_composite(node):
2971
3044
  """True when crop is combined with non-default rotate/zoom (issue #46)."""
2972
3045
  rotate = getattr(node, "rotate", None)
@@ -3570,9 +3643,247 @@ init 1100 python:
3570
3643
  return candidates
3571
3644
 
3572
3645
 
3646
+ def _renforge_editor_has_text_descendant(widget):
3647
+ try:
3648
+ text_type = renpy.text.text.Text
3649
+ except Exception:
3650
+ text_type = None
3651
+ stack = list(_renforge_editor_children(widget))
3652
+ seen = set([id(widget)])
3653
+ while stack:
3654
+ node = stack.pop()
3655
+ if node is None or id(node) in seen:
3656
+ continue
3657
+ seen.add(id(node))
3658
+ if text_type is not None:
3659
+ try:
3660
+ if isinstance(node, text_type):
3661
+ return True
3662
+ except Exception:
3663
+ pass
3664
+ stack.extend(_renforge_editor_children(node))
3665
+ return False
3666
+
3667
+
3668
+ def _renforge_editor_displayable_statement_kind(widget):
3669
+ name = getattr(getattr(widget, "__class__", None), "__name__", "")
3670
+ if name in ("Frame", "Window"):
3671
+ return "frame"
3672
+ if name in ("Solid", "Image", "ImageReference"):
3673
+ return "add"
3674
+ return None
3675
+
3676
+
3677
+ def _renforge_editor_runtime_key_from_displayable_widget(
3678
+ screen_name,
3679
+ widget,
3680
+ widget_id,
3681
+ ordinal,
3682
+ instances=None,
3683
+ statement_kind="add",
3684
+ ):
3685
+ if not isinstance(screen_name, str) or not screen_name:
3686
+ return None, "MISSING_INVOCATION_PATH", None
3687
+ if not isinstance(widget_id, str) or not widget_id:
3688
+ return None, "INVALID_WIDGET_ID", None
3689
+ if _renforge_editor_displayable_statement_kind(widget) is None:
3690
+ return None, "STATEMENT_KIND_MISMATCH", None
3691
+ screen, widgets = _renforge_editor_widget_map(screen_name)
3692
+ if screen is None:
3693
+ return None, "MISSING_SCREEN", None
3694
+ cache_index = instances.get(screen_name) if instances else None
3695
+ cache_entry = cache_index["entries"].get(id(widget)) if cache_index else None
3696
+ named_widget = None
3697
+ if isinstance(widgets, builtins.dict):
3698
+ named_widget = widgets.get(widget_id)
3699
+ if named_widget is None:
3700
+ named_widget = widget
3701
+ source_location = _renforge_editor_location(named_widget)
3702
+ if source_location is None:
3703
+ source_location = _renforge_editor_location(widget)
3704
+ if source_location is None:
3705
+ return None, "MISSING_SOURCE_LOCATION", None
3706
+ ancestry_nodes = _renforge_editor_find_ancestry(screen, widget)
3707
+ if not ancestry_nodes:
3708
+ ancestry_nodes = _renforge_editor_find_ancestry(screen, named_widget)
3709
+ if not ancestry_nodes:
3710
+ return None, "UNKNOWN_ANCESTRY_TYPE", None
3711
+ ancestry = []
3712
+ for index, node in enumerate(ancestry_nodes):
3713
+ class_name = getattr(getattr(node, "__class__", None), "__name__", "unknown")
3714
+ if class_name not in _ALLOWED_ANCESTRY_TYPES:
3715
+ return None, _renforge_editor_ancestry_lock_code(class_name), None
3716
+ crop_state = _renforge_editor_crop_state(node, focus=None, target=widget)
3717
+ if crop_state not in (
3718
+ "none",
3719
+ "viewport",
3720
+ "crop_displayable",
3721
+ "transform_crop",
3722
+ "transform_crop_composite",
3723
+ "transform_crop_partial",
3724
+ "transform_crop_unproven",
3725
+ "clipping_true",
3726
+ ):
3727
+ return None, "UNKNOWN_CROP_STATE", None
3728
+ editor_owned = bool(
3729
+ screen_name in _EDITOR_SCREENS
3730
+ or getattr(node, "_renforge_editor_owner", None) == _EDITOR_OWNER
3731
+ or getattr(named_widget, "_renforge_editor_owner", None) == _EDITOR_OWNER
3732
+ )
3733
+ style = getattr(node, "style", None)
3734
+ layout = getattr(style, "box_layout", None) if style is not None else None
3735
+ if layout is None:
3736
+ layout = getattr(node, "default_layout", None)
3737
+ layout_name = layout if isinstance(layout, builtins.str) else None
3738
+ ancestry.append(
3739
+ {
3740
+ "index": int(index),
3741
+ "type": class_name,
3742
+ "source_location": _renforge_editor_location(node),
3743
+ "screen_owner": _EDITOR_OWNER if editor_owned else "game",
3744
+ "crop_state": crop_state,
3745
+ "editor_owned": editor_owned,
3746
+ "layout": layout_name,
3747
+ }
3748
+ )
3749
+ discriminator = {"kind": "static", "instance_count": 1}
3750
+ if cache_entry is not None:
3751
+ measured = _renforge_editor_instance_discriminator(
3752
+ cache_entry,
3753
+ cache_index["statement_siblings"],
3754
+ )
3755
+ if measured is not None:
3756
+ discriminator = measured
3757
+ discriminator["ordinal"] = int(ordinal)
3758
+ key = {
3759
+ "screen": screen_name,
3760
+ "invocation_path": screen_name,
3761
+ "widget_id": widget_id,
3762
+ "source_location": source_location,
3763
+ "locator": {
3764
+ "kind": "source",
3765
+ "source_location": list(source_location),
3766
+ "statement_kind": statement_kind,
3767
+ },
3768
+ "instance_discriminator": discriminator,
3769
+ "ancestry": ancestry,
3770
+ }
3771
+ return key, None, named_widget
3772
+
3773
+
3774
+ def _renforge_editor_displayable_candidates(focus_candidates=None):
3775
+ """Discover source-backed add/frame targets from the scene walk."""
3776
+ candidates = []
3777
+ instances = {}
3778
+ ordinal = 200000
3779
+ if focus_candidates is None:
3780
+ focus_candidates = _renforge_editor_focus_candidates()
3781
+ focus_covered_ids = set()
3782
+ for candidate in focus_candidates or []:
3783
+ focused_widget = candidate.get("focused_widget")
3784
+ if focused_widget is not None:
3785
+ focus_covered_ids.update(_renforge_editor_descendant_ids(focused_widget))
3786
+ raw = []
3787
+ for screen_name in _renforge_editor_active_game_screens():
3788
+ screen, widgets = _renforge_editor_widget_map(screen_name)
3789
+ if screen is None or not isinstance(widgets, builtins.dict):
3790
+ continue
3791
+ screen_instances = _renforge_editor_screen_instances(screen_name, instances)
3792
+ widget_ids = {}
3793
+ for widget_id, widget in list(widgets.items()):
3794
+ if isinstance(widget_id, str) and widget_id:
3795
+ widget_ids[id(widget)] = widget_id
3796
+ runtime_widgets = []
3797
+ seen_widgets = set()
3798
+ for widget in list(widgets.values()) + [
3799
+ entry.get("displayable")
3800
+ for entry in screen_instances.get("entries", {}).values()
3801
+ if isinstance(entry, builtins.dict)
3802
+ ]:
3803
+ if widget is None or id(widget) in seen_widgets:
3804
+ continue
3805
+ seen_widgets.add(id(widget))
3806
+ statement_kind = _renforge_editor_displayable_statement_kind(widget)
3807
+ if statement_kind is None:
3808
+ continue
3809
+ if id(widget) in focus_covered_ids:
3810
+ continue
3811
+ if statement_kind == "frame" and _renforge_editor_has_text_descendant(widget):
3812
+ continue
3813
+ runtime_widgets.append((widget, statement_kind))
3814
+ for widget, statement_kind in runtime_widgets:
3815
+ widget_id = widget_ids.get(id(widget))
3816
+ if not isinstance(widget_id, str) or not widget_id:
3817
+ continue
3818
+ rect = _renforge_editor_measure_text_rect(screen, widget)
3819
+ if rect is None:
3820
+ continue
3821
+ runtime_key, resolve_error, named_widget = _renforge_editor_runtime_key_from_displayable_widget(
3822
+ screen_name,
3823
+ widget,
3824
+ widget_id,
3825
+ ordinal,
3826
+ instances,
3827
+ statement_kind,
3828
+ )
3829
+ ordinal += 1
3830
+ owner_hit = bool(screen_name in _EDITOR_SCREENS)
3831
+ if runtime_key is not None:
3832
+ for node in runtime_key.get("ancestry", []):
3833
+ if bool(node.get("editor_owned")):
3834
+ owner_hit = True
3835
+ break
3836
+ raw.append(
3837
+ {
3838
+ "focus": None,
3839
+ "rect": rect,
3840
+ "ordinal": int(ordinal - 1),
3841
+ "runtime_key": runtime_key,
3842
+ "resolve_error": resolve_error,
3843
+ "named_widget": named_widget,
3844
+ "focused_widget": widget,
3845
+ "editor_owned": owner_hit,
3846
+ "measurement_method": "scene_tree_displayable",
3847
+ }
3848
+ )
3849
+ for candidate in _renforge_editor_drop_descendant_candidates(raw):
3850
+ candidates.append(candidate)
3851
+ counts = {}
3852
+ for candidate in candidates:
3853
+ key = candidate.get("runtime_key")
3854
+ if not isinstance(key, builtins.dict):
3855
+ continue
3856
+ signature = (
3857
+ key.get("screen"),
3858
+ key.get("widget_id"),
3859
+ tuple(key.get("source_location") or []),
3860
+ )
3861
+ counts[signature] = counts.get(signature, 0) + 1
3862
+ for candidate in candidates:
3863
+ key = candidate.get("runtime_key")
3864
+ if not isinstance(key, builtins.dict):
3865
+ continue
3866
+ signature = (
3867
+ key.get("screen"),
3868
+ key.get("widget_id"),
3869
+ tuple(key.get("source_location") or []),
3870
+ )
3871
+ if counts.get(signature, 0) <= 1:
3872
+ continue
3873
+ discriminator = key.get("instance_discriminator") or {}
3874
+ if discriminator.get("kind") in ("loop", "use"):
3875
+ continue
3876
+ candidate["resolve_error"] = "MULTI_INSTANCE_UNSUPPORTED"
3877
+ return candidates
3878
+
3879
+
3573
3880
  def _renforge_editor_all_candidates():
3574
3881
  focus_candidates = list(_renforge_editor_focus_candidates())
3575
- return focus_candidates + list(_renforge_editor_text_candidates(focus_candidates))
3882
+ return (
3883
+ focus_candidates
3884
+ + list(_renforge_editor_text_candidates(focus_candidates))
3885
+ + list(_renforge_editor_displayable_candidates(focus_candidates))
3886
+ )
3576
3887
 
3577
3888
 
3578
3889
  def _renforge_editor_barrier():
@@ -3733,7 +4044,7 @@ init 1100 python:
3733
4044
  for target in state.targets.values():
3734
4045
  if target.get("screen") != screen:
3735
4046
  continue
3736
- widget_id = target.get("widget_id")
4047
+ widget_id = _renforge_editor_style_gui_preview_widget_id(target)
3737
4048
  if not widget_id:
3738
4049
  continue
3739
4050
  props = {}
@@ -3919,7 +4230,7 @@ init 1100 python:
3919
4230
  return default_x, default_y
3920
4231
 
3921
4232
  screen_name = target.get("screen")
3922
- widget_id = target.get("widget_id")
4233
+ widget_id = _renforge_editor_style_gui_preview_widget_id(target)
3923
4234
  widget = None
3924
4235
  if isinstance(screen_name, str) and isinstance(widget_id, str):
3925
4236
  widget = _renforge_editor_find_widget_by_id(screen_name, widget_id)
@@ -4062,9 +4373,9 @@ init 1100 python:
4062
4373
  ]
4063
4374
  _renforge_editor_set_label(state.pointer[0], state.pointer[1])
4064
4375
 
4065
- # Track preview method for say.what style position
4376
+ # Track preview method for style-backed say position
4066
4377
  source_key = target.get("source_key")
4067
- if isinstance(source_key, builtins.dict) and source_key.get("position_mode") == "style_gui_dialogue":
4378
+ if _renforge_editor_is_style_gui_position(source_key):
4068
4379
  state.last_preview_method = "style_widget_override"
4069
4380
 
4070
4381
  return {"ok": True, "x": next_position[0], "y": next_position[1]}
@@ -4904,7 +5215,7 @@ init 1100 python:
4904
5215
  target = state.targets.get(state.selected_target_key)
4905
5216
  if isinstance(target, builtins.dict):
4906
5217
  source_key = target.get("source_key")
4907
- if isinstance(source_key, builtins.dict) and source_key.get("position_mode") == "style_gui_dialogue":
5218
+ if _renforge_editor_is_style_gui_position(source_key):
4908
5219
  preview_method = "style_widget_override"
4909
5220
 
4910
5221
  state.last_preview_method = preview_method
@@ -4941,8 +5252,8 @@ init 1100 python:
4941
5252
  if isinstance(source_key, builtins.dict):
4942
5253
  position_mode = source_key.get("position_mode")
4943
5254
 
4944
- # style_gui_dialogue: restore by mutating style, not rebuilding say screen
4945
- if position_mode == "style_gui_dialogue":
5255
+ # style-backed say position: restore by mutating style, not rebuilding say
5256
+ if position_mode in _RF_STYLE_GUI_POSITION_MODES:
4946
5257
  baseline = list(target.get("runtime_baseline") or [])
4947
5258
  if len(baseline) == 2:
4948
5259
  try:
@@ -5268,21 +5579,25 @@ init 1100 python:
5268
5579
 
5269
5580
  def _renforge_editor_hit_candidates(x, y, screen_x=None, screen_y=None):
5270
5581
  """Return focus hits first; only fall back to text when no focusable covers the point."""
5582
+ focus_candidates = _renforge_editor_focus_candidates()
5271
5583
  focus_hits = []
5272
- for candidate in reversed(_renforge_editor_focus_candidates()):
5584
+ for candidate in reversed(focus_candidates):
5273
5585
  if candidate.get("editor_owned"):
5274
5586
  continue
5275
5587
  if _renforge_editor_candidate_hit(candidate, x, y, screen_x, screen_y):
5276
5588
  focus_hits.append(candidate)
5277
5589
  if focus_hits:
5278
5590
  return focus_hits
5279
- text_hits = []
5280
- for candidate in reversed(_renforge_editor_text_candidates()):
5591
+ scene_hits = []
5592
+ nonfocus = list(_renforge_editor_text_candidates(focus_candidates)) + list(
5593
+ _renforge_editor_displayable_candidates(focus_candidates)
5594
+ )
5595
+ for candidate in reversed(nonfocus):
5281
5596
  if candidate.get("editor_owned"):
5282
5597
  continue
5283
5598
  if _renforge_editor_candidate_hit(candidate, x, y):
5284
- text_hits.append(candidate)
5285
- return text_hits
5599
+ scene_hits.append(candidate)
5600
+ return scene_hits
5286
5601
 
5287
5602
 
5288
5603
  def _renforge_editor_select(x, y, requested_candidate=None):
@@ -5299,10 +5614,13 @@ init 1100 python:
5299
5614
  if not hits:
5300
5615
  _renforge_editor_clear_selection()
5301
5616
  return {"ok": False, "error": "NO_FOCUSABLE_TARGET"}
5302
- # Fail closed when multiple non-focusable text targets cover the same point.
5617
+ # Fail closed when multiple non-focusable scene-walk targets cover the same point.
5303
5618
  if (
5304
5619
  len(hits) > 1
5305
- and all(c.get("measurement_method") == "scene_tree_text" for c in hits)
5620
+ and all(
5621
+ c.get("measurement_method") in ("scene_tree_text", "scene_tree_displayable")
5622
+ for c in hits
5623
+ )
5306
5624
  ):
5307
5625
  state.pointer = [int(x), int(y)]
5308
5626
  state.selected_runtime_key = None
@@ -6276,7 +6594,7 @@ init 1100 python:
6276
6594
  and intent.get("x") is not None
6277
6595
  and intent.get("y") is not None
6278
6596
  and isinstance(intent.get("source_key"), builtins.dict)
6279
- and intent["source_key"].get("position_mode") == "style_gui_dialogue"
6597
+ and intent["source_key"].get("position_mode") in _RF_STYLE_GUI_POSITION_MODES
6280
6598
  for intent in intents
6281
6599
  )
6282
6600
  )
@@ -21,6 +21,8 @@
21
21
  "lock.reason.unsupported": "This source form is not editable yet.",
22
22
  "lock.reason.mismatch": "The live element no longer matches its source.",
23
23
  "lock.reason.ambiguous": "This selection does not resolve to one unique source element.",
24
+ "lock.reason.ambiguous_hit": "Several non-focusable elements overlap here. Move them apart to edit.",
25
+ "lock.reason.statement_kind_mismatch": "This displayable's source form is not a writable add or frame.",
24
26
  "lock.reason.default": "This selection cannot be edited safely.",
25
27
  "tree.title": "SCENE TREE",
26
28
  "tree.items_count": "{count} items",
@@ -98,5 +100,6 @@
98
100
  "lock.reason.style_position_variant_unsupported": "Phone/small variant overrides prevent desktop editing.",
99
101
  "inspector.ownership_chain": "OWNERSHIP",
100
102
  "inspector.ownership_style_position": "say.what → style say_dialogue → gui.dialogue_xpos/ypos",
103
+ "inspector.ownership_style_position_who": "say.who → style namebox → gui.name_xpos/ypos",
101
104
  "inspector.global_scope_notice": "⚠ This change affects all standard dialogue lines"
102
105
  }
@@ -21,6 +21,8 @@
21
21
  "lock.reason.unsupported": "尚不支持编辑此源码形式。",
22
22
  "lock.reason.mismatch": "实时元素与其源码不再匹配。",
23
23
  "lock.reason.ambiguous": "此选择无法解析为唯一的源码元素。",
24
+ "lock.reason.ambiguous_hit": "多个不可聚焦元素在此重叠。请先将它们分开再编辑。",
25
+ "lock.reason.statement_kind_mismatch": "此显示对象的源码形式不是可写入的 add 或 frame。",
24
26
  "lock.reason.default": "无法安全编辑此选择。",
25
27
  "tree.title": "场景树",
26
28
  "tree.items_count": "{count} 项",
@@ -98,5 +100,6 @@
98
100
  "lock.reason.style_position_variant_unsupported": "手机/小屏幕变体覆盖阻止桌面编辑。",
99
101
  "inspector.ownership_chain": "所有权",
100
102
  "inspector.ownership_style_position": "say.what → 样式 say_dialogue → gui.dialogue_xpos/ypos",
103
+ "inspector.ownership_style_position_who": "say.who → 样式 namebox → gui.name_xpos/ypos",
101
104
  "inspector.global_scope_notice": "⚠ 此更改影响所有标准对话行"
102
105
  }
@@ -225,13 +225,13 @@ screen _rf_editor_inspector_panel():
225
225
  use _rf_editor_field("xpos", str(_rf_facts["rect"]["x"]))
226
226
  use _rf_editor_field("ypos", str(_rf_facts["rect"]["y"]))
227
227
 
228
- if _rf_facts.get("position_mode") == "style_gui_dialogue":
228
+ if _rf_facts.get("position_mode") in ("style_gui_dialogue", "style_gui_namebox"):
229
229
  text _renforge_editor_t("inspector.ownership_chain"):
230
230
  color _renforge_editor_ui_color("meta")
231
231
  font _renforge_editor_ui_font()
232
232
  size _renforge_editor_ui_px(14)
233
233
  yoffset _renforge_editor_ui_px(6)
234
- text _renforge_editor_t("inspector.ownership_style_position"):
234
+ text _renforge_editor_t(_renforge_editor_ownership_style_position_key(_rf_facts.get("position_mode"))):
235
235
  id "rf_inspector_ownership_style_position"
236
236
  color _renforge_editor_ui_color("surface")
237
237
  font _renforge_editor_ui_font()