tilemap-editor 4.3.9__tar.gz → 5.0.0__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 (263) hide show
  1. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/CHANGELOG.md +34 -0
  2. {tilemap_editor-4.3.9/src/tilemap_editor.egg-info → tilemap_editor-5.0.0}/PKG-INFO +1 -1
  3. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/pyproject.toml +1 -1
  4. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/editor.py +238 -12
  5. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/node_manager.py +28 -3
  6. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/editor.py +43 -12
  7. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/editor.py +1 -1
  8. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/editor.py +132 -15
  9. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/frame_picker.py +53 -7
  10. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/timeline.py +56 -5
  11. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/commands.py +76 -0
  12. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/document.py +81 -4
  13. tilemap_editor-5.0.0/src/plugins/sprite_editor/editor.py +1258 -0
  14. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/selection.py +8 -0
  15. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/standalone.py +1 -0
  16. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/tools.py +586 -1
  17. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/viewport.py +6 -2
  18. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/collision_painter.py +1 -1
  19. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/standalone_automap.py +5 -2
  20. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/standalone_image_viewer.py +23 -12
  21. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap.py +60 -3
  22. tilemap_editor-5.0.0/src/tilemap_editor/assets/icons/redo.svg +1 -0
  23. tilemap_editor-5.0.0/src/tilemap_editor/assets/icons/text.svg +1 -0
  24. tilemap_editor-5.0.0/src/tilemap_editor/assets/icons/undo.svg +1 -0
  25. tilemap_editor-5.0.0/src/tilemap_editor/cli.py +170 -0
  26. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0/src/tilemap_editor.egg-info}/PKG-INFO +1 -1
  27. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/SOURCES.txt +16 -0
  28. tilemap_editor-5.0.0/src/utils/context_dispatch.py +83 -0
  29. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/font_manager.py +4 -4
  30. tilemap_editor-5.0.0/src/utils/tileset_hierarchy.py +173 -0
  31. tilemap_editor-5.0.0/src/utils/tileset_ops.py +208 -0
  32. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/autotile_template.py +7 -5
  33. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/autotiler.py +60 -38
  34. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/filemanager.py +26 -4
  35. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/layer_selector.py +85 -74
  36. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/map_properties.py +1 -1
  37. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/mapsetup.py +2 -2
  38. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/regex_automap_designer.py +19 -0
  39. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/tile_grid.py +49 -13
  40. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/tile_selector.py +381 -55
  41. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/button.py +31 -7
  42. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/checkbox.py +9 -9
  43. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/collision_layer_sidebar.py +1 -1
  44. tilemap_editor-5.0.0/src/widgets/ui/context_menu.py +161 -0
  45. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/dialog_base.py +2 -2
  46. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/draw_utils.py +4 -2
  47. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/fileinput.py +7 -2
  48. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/menubar.py +89 -33
  49. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/node_editor.py +26 -10
  50. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/node_selector.py +4 -2
  51. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/notification.py +6 -5
  52. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/particle_config_dialog.py +61 -55
  53. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/property_editor.py +83 -23
  54. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/resize_map_dialog.py +1 -1
  55. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/scrollbar.py +8 -6
  56. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/sidebar_container.py +16 -10
  57. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/status_bar.py +4 -5
  58. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/theme.py +111 -38
  59. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/toolbar.py +5 -5
  60. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/tooltip.py +1 -1
  61. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/tree_widget.py +94 -5
  62. tilemap_editor-5.0.0/tests/test_context_dispatch.py +287 -0
  63. tilemap_editor-5.0.0/tests/test_object_properties_context.py +123 -0
  64. tilemap_editor-5.0.0/tests/test_sandbox_mode.py +461 -0
  65. tilemap_editor-5.0.0/tests/test_sprite_animation_gestures.py +375 -0
  66. tilemap_editor-5.0.0/tests/test_sprite_editor_drop.py +497 -0
  67. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_sprite_editor_interaction.py +98 -34
  68. tilemap_editor-5.0.0/tests/test_sprite_editor_stack.py +168 -0
  69. tilemap_editor-5.0.0/tests/test_theme.py +74 -0
  70. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_tile_selector_pick.py +41 -0
  71. tilemap_editor-5.0.0/tests/test_tileset_hierarchy.py +447 -0
  72. tilemap_editor-5.0.0/tests/test_tileset_integrity.py +567 -0
  73. tilemap_editor-4.3.9/src/plugins/sprite_editor/editor.py +0 -658
  74. tilemap_editor-4.3.9/src/tilemap_editor/cli.py +0 -94
  75. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/LICENSE +0 -0
  76. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/MANIFEST.in +0 -0
  77. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/README.md +0 -0
  78. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/jetbrain-fonts/JetBrainsMono-Bold.ttf +0 -0
  79. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/jetbrain-fonts/JetBrainsMono-BoldItalic.ttf +0 -0
  80. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/jetbrain-fonts/JetBrainsMono-Italic.ttf +0 -0
  81. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/jetbrain-fonts/JetBrainsMono-Regular.ttf +0 -0
  82. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/noto/NotoSans-Bold.ttf +0 -0
  83. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/noto/NotoSans-BoldItalic.ttf +0 -0
  84. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/noto/NotoSans-Italic.ttf +0 -0
  85. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  86. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/arrow-down.svg +0 -0
  87. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/check.svg +0 -0
  88. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/checked.svg +0 -0
  89. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/close.svg +0 -0
  90. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/duplicate.svg +0 -0
  91. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/error.svg +0 -0
  92. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/file.svg +0 -0
  93. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/filedead.svg +0 -0
  94. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/fit.svg +0 -0
  95. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/folder.svg +0 -0
  96. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/grid.svg +0 -0
  97. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/image.svg +0 -0
  98. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/info.svg +0 -0
  99. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/load.svg +0 -0
  100. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/loop.svg +0 -0
  101. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/onion.svg +0 -0
  102. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/pan.svg +0 -0
  103. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/pause.svg +0 -0
  104. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/pencil.svg +0 -0
  105. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/play.svg +0 -0
  106. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/plus.svg +0 -0
  107. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/radio.svg +0 -0
  108. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/reset.svg +0 -0
  109. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/save.svg +0 -0
  110. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/skip-back.svg +0 -0
  111. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/skip-forward.svg +0 -0
  112. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/stop.svg +0 -0
  113. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/tilemap.svg +0 -0
  114. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/tileset.svg +0 -0
  115. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/unchecked.svg +0 -0
  116. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/warning.svg +0 -0
  117. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/zoomin.svg +0 -0
  118. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/zoomout.svg +0 -0
  119. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/setup.cfg +0 -0
  120. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/constants.py +0 -0
  121. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/event_map.py +0 -0
  122. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/layers.py +0 -0
  123. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/main.py +0 -0
  124. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/nodes.py +0 -0
  125. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/__init__.py +0 -0
  126. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/__init__.py +0 -0
  127. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/models.py +0 -0
  128. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/protocols.py +0 -0
  129. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/shape_editor.py +0 -0
  130. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/standalone.py +0 -0
  131. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/__init__.py +0 -0
  132. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/models.py +0 -0
  133. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/protocols.py +0 -0
  134. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/standalone.py +0 -0
  135. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/__init__.py +0 -0
  136. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/__main__.py +0 -0
  137. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/clipboard_util.py +0 -0
  138. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/models.py +0 -0
  139. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/preview.py +0 -0
  140. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/protocols.py +0 -0
  141. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/runtime_load.py +0 -0
  142. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/standalone.py +0 -0
  143. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/validation.py +0 -0
  144. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/__init__.py +0 -0
  145. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/camera.py +0 -0
  146. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/clipboard.py +0 -0
  147. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/dialogs.py +0 -0
  148. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/overlays.py +0 -0
  149. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/region_export.py +0 -0
  150. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/__init__.py +0 -0
  151. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/editor.py +0 -0
  152. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/models.py +0 -0
  153. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/protocols.py +0 -0
  154. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/standalone.py +0 -0
  155. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/run.py +0 -0
  156. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/standalone_error_console.py +0 -0
  157. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/standalone_filemanager.py +0 -0
  158. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/__init__.py +0 -0
  159. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/__main__.py +0 -0
  160. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/__init__.py +0 -0
  161. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/jetbrain-fonts/JetBrainsMono-Bold.ttf +0 -0
  162. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/jetbrain-fonts/JetBrainsMono-BoldItalic.ttf +0 -0
  163. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/jetbrain-fonts/JetBrainsMono-Italic.ttf +0 -0
  164. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/jetbrain-fonts/JetBrainsMono-Regular.ttf +0 -0
  165. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/noto/NotoSans-Bold.ttf +0 -0
  166. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/noto/NotoSans-BoldItalic.ttf +0 -0
  167. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/noto/NotoSans-Italic.ttf +0 -0
  168. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  169. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/ToolMove.svg +0 -0
  170. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/arrow-down.svg +0 -0
  171. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/check.svg +0 -0
  172. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/checked.svg +0 -0
  173. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/close.svg +0 -0
  174. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/duplicate.svg +0 -0
  175. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/error.svg +0 -0
  176. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/file.svg +0 -0
  177. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/filedead.svg +0 -0
  178. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/fit.svg +0 -0
  179. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/fold_down_arrow.svg +0 -0
  180. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/folder.svg +0 -0
  181. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/gear.svg +0 -0
  182. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/grid.svg +0 -0
  183. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/image.svg +0 -0
  184. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/info.svg +0 -0
  185. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/load.svg +0 -0
  186. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/loop.svg +0 -0
  187. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/miniobj.svg +0 -0
  188. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/onion.svg +0 -0
  189. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/pan.svg +0 -0
  190. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/pause.svg +0 -0
  191. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/pencil.svg +0 -0
  192. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/play.svg +0 -0
  193. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/plus.svg +0 -0
  194. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/radio.svg +0 -0
  195. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/reset.svg +0 -0
  196. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/save.svg +0 -0
  197. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/skip-back.svg +0 -0
  198. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/skip-forward.svg +0 -0
  199. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/stop.svg +0 -0
  200. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/tilemap.svg +0 -0
  201. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/tileset.svg +0 -0
  202. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/unchecked.svg +0 -0
  203. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/unfold_right_arrow.svg +0 -0
  204. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/warning.svg +0 -0
  205. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/zoomin.svg +0 -0
  206. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/zoomout.svg +0 -0
  207. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/main.py +0 -0
  208. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/settings.py +0 -0
  209. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/dependency_links.txt +0 -0
  210. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/entry_points.txt +0 -0
  211. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/requires.txt +0 -0
  212. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/top_level.txt +0 -0
  213. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/ttypes/__init__.py +0 -0
  214. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/ttypes/tilemap.py +0 -0
  215. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/__init__.py +0 -0
  216. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/editor_preference.py +0 -0
  217. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/error_handler.py +0 -0
  218. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/history.py +0 -0
  219. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/icon_manager.py +0 -0
  220. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/icons_cache.py +0 -0
  221. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/log_capture.py +0 -0
  222. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/natural_sort.py +0 -0
  223. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/project_paths.py +0 -0
  224. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/property_suggestions.py +0 -0
  225. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/serialization.py +0 -0
  226. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/standalone.py +0 -0
  227. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/validation.py +0 -0
  228. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/__init__.py +0 -0
  229. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/automap_models.py +0 -0
  230. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/input.py +0 -0
  231. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/particle_presets.py +0 -0
  232. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/particle_system.py +0 -0
  233. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/collision_layer_mask.py +0 -0
  234. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/confirm_dialog.py +0 -0
  235. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/drag_tracker.py +0 -0
  236. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/label.py +0 -0
  237. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/layer_type_dialog.py +0 -0
  238. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/mode_indicator.py +0 -0
  239. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/region_selector.py +0 -0
  240. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/splitter.py +0 -0
  241. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/tileset_type_dialog.py +0 -0
  242. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/toast.py +0 -0
  243. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/tool_manager.py +0 -0
  244. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/widget_base.py +0 -0
  245. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_animation_library_grid_offset.py +0 -0
  246. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_collision_layer_mask.py +0 -0
  247. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_collision_layer_sidebar.py +0 -0
  248. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_drag_tracker.py +0 -0
  249. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_editor_pan_mode.py +0 -0
  250. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_gid_collision.py +0 -0
  251. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_layers_autotile.py +0 -0
  252. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_map_resize_model.py +0 -0
  253. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_natural_sort.py +0 -0
  254. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_project_paths.py +0 -0
  255. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_render_scale.py +0 -0
  256. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_serialization_regression.py +0 -0
  257. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_sprite_animation_editor_grid.py +0 -0
  258. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_sprite_editor_commands.py +0 -0
  259. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_sprite_editor_models.py +0 -0
  260. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_tile_grid_selection.py +0 -0
  261. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_tilemap_save.py +0 -0
  262. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_timeline_invalidate_cache.py +0 -0
  263. {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_toolbar_tools.py +0 -0
@@ -1,6 +1,40 @@
1
1
  # Changelog
2
2
 
3
3
  <!-- towncrier release notes start -->
4
+ # tilemap_editor 5.0.0 (2026-08-25)
5
+
6
+ ## What's New
7
+
8
+ - **Try maps safely in a sandbox** — open an external map in an isolated workspace, experiment, then export it to your project when ready. Your original project stays untouched if the export has a problem, and images are copied alongside the new map automatically.
9
+ - **Drag & drop images into the sprite editor** — drop files from your desktop or copy a file path and paste (`Cmd/Ctrl+V`). New sheets are added next to your current work without overwriting it, and you can undo. Works with filenames that have spaces and with shared network drives on Windows.
10
+ - **Smarter sprite sheets** — sheets stack neatly (stack vertically or horizontally), filenames sort naturally (`s1, s2, s10` not `s1, s10, s2`), and the canvas grows automatically when you paste outside it. Move, select, and add text directly on the canvas.
11
+ - **Cleaner animation editing** — smoother zoom/pan gestures and clearer controls for frame size (`px` or `cells`).
12
+ - **Organize tilesets** — group tilesets into folders, keep missing images as placeholders so your map doesn't scramble, and pick the draw order with y-sort.
13
+
14
+ ## Fixes & Polish
15
+
16
+ - Saving is now safer — the editor checks for problems before writing and keeps your project intact if something fails.
17
+ - Sprite editor feels smoother: canvas sizing, undo/redo, and switching tools no longer conflict with typing.
18
+ - Better look and feel: updated themes, tidier toolbars, and a draggable splitter between panels.
19
+
20
+ ## Breaking Changes
21
+
22
+ - No intentional breaking changes yet — file format and workflow stay compatible with `4.x`. Any required migration steps will be listed here before `5.0.0` ships.
23
+
24
+ ## Upgrading from 4.x
25
+
26
+ - Just update — no steps needed yet. If you use maps with missing tilesets, they now stay in place as placeholders instead of shifting other tiles.
27
+
28
+ # tilemap_editor 4.4.0 (2026-08-19)
29
+
30
+ ## Features
31
+
32
+ - Property editor: added a remove button for individual properties and object-level property support. Right-clicking a map object, tileset image, or tree row now opens the property editor through a context dispatch system, and object properties can be edited on the placed object directly. (context-property-editor)
33
+
34
+ ## Bug Fixes
35
+
36
+ - Object picking on object layers now hits the visually topmost overlapping object: right-click property open and left-click variant select test objects in reverse draw order. (object-pick-order)
37
+
4
38
  # tilemap_editor 4.3.9 (2026-08-04)
5
39
 
6
40
  ## Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilemap-editor
3
- Version: 4.3.9
3
+ Version: 5.0.0
4
4
  Summary: Pygame tilemap editor with collision, animation, sprite & particle tools
5
5
  Author: tilemap editor contributors
6
6
  License: Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tilemap-editor"
7
- version = "4.3.9"
7
+ version = "5.0.0"
8
8
  description = "Pygame tilemap editor with collision, animation, sprite & particle tools"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -2,6 +2,7 @@ import json
2
2
  import logging
3
3
  import os
4
4
  import queue
5
+ import shutil
5
6
  import subprocess
6
7
  import sys
7
8
  import threading
@@ -21,6 +22,7 @@ from constants import BASE_PATH
21
22
  from node_manager import NodeManager
22
23
  from tilemap import Tilemap
23
24
  from utils import error_context, error_handler
25
+ from utils.context_dispatch import PropertyContextDispatcher
24
26
  from utils.property_suggestions import PropertySuggestionRegistry
25
27
  from utils.standalone import launch_standalone
26
28
  from widgets.autotiler import AutotileRuleDesigner
@@ -157,17 +159,42 @@ def _load_project_config() -> tuple[Path, Path, dict]:
157
159
  _launch_standalone_module = launch_standalone
158
160
 
159
161
 
162
+ def _display_load_path(path: Path) -> Path:
163
+ """Path shown in the SANDBOX load toast: CWD-relative when possible."""
164
+ try:
165
+ return path.resolve().relative_to(Path.cwd())
166
+ except ValueError:
167
+ return path
168
+
169
+
160
170
  class Editor:
161
171
  def __init__(
162
172
  self,
163
173
  size: tuple[int, int] | None = None,
164
174
  fps=60,
165
175
  theme: str | None = None,
176
+ sandbox_dir: Path | None = None,
166
177
  ):
167
178
  self.base_path, self.data_root, self.config = _load_project_config()
168
179
  logger.info(f"Project base_path: {self.base_path}")
169
180
  logger.info(f"Data root: {self.data_root}")
170
181
 
182
+ self.is_sandbox = sandbox_dir is not None
183
+ self._project_data_root = self.data_root
184
+ self._sandbox_root: Path | None = None
185
+ self._sandbox_map_path: Path | None = None
186
+ if sandbox_dir is not None:
187
+ sandbox = Path(sandbox_dir)
188
+ self._sandbox_root = sandbox
189
+ self._sandbox_map_path = sandbox / "map.json"
190
+ self.data_root = sandbox
191
+ log_root = sandbox / "logs"
192
+ log_root.mkdir(parents=True, exist_ok=True)
193
+ from utils.error_handler import init_error_handler
194
+
195
+ init_error_handler(log_root=log_root, config=self.config["error_handler"])
196
+ logger.info(f"Sandbox mode: data_root redirected to {sandbox}")
197
+
171
198
  theme_name = theme or self.config.get("theme", "dark")
172
199
  tm = get_theme_manager()
173
200
  if not tm.set_theme(theme_name):
@@ -175,7 +202,9 @@ class Editor:
175
202
  tm.set_theme("dark")
176
203
 
177
204
  pygame.init()
178
- pygame.display.set_caption("Pure Pygame Editor")
205
+ pygame.display.set_caption(
206
+ "Tilemap Editor - SANDBOX" if self.is_sandbox else "Pure Pygame Editor"
207
+ )
179
208
 
180
209
  self.fps = fps
181
210
  self.running = False
@@ -214,6 +243,7 @@ class Editor:
214
243
  self.notifications = NotificationManager(self)
215
244
  self.tooltip = TooltipManager()
216
245
  self.suggestion_registry = PropertySuggestionRegistry()
246
+ self.context_dispatch = PropertyContextDispatcher()
217
247
  self.error_console_process: subprocess.Popen | None = None
218
248
  self.property_editor: PropertyEditor | None = None
219
249
  self.particle_config_dialog: ParticleConfigDialog | None = None
@@ -448,16 +478,199 @@ class Editor:
448
478
 
449
479
  def do_save_as(self, filename: str):
450
480
  try:
481
+ if self.is_sandbox:
482
+ target = self._project_data_root / filename
483
+ self._sandbox_save_as(target)
484
+ return
451
485
  self.tilemap.save_map(filename)
452
486
  except Exception as e:
453
487
  error_handler.capture(e, context="save_map_as")
454
488
 
455
489
  def on_map_save_selected(self, path: Path):
456
490
  try:
491
+ if self.is_sandbox:
492
+ self._sandbox_save_as(path)
493
+ return
457
494
  self.tilemap.save_map(path)
458
495
  except Exception as e:
459
496
  error_handler.capture(e, context="save_map_selected")
460
497
 
498
+ def _sandbox_save_as(self, path: Path):
499
+ """Save As from sandbox: copy assets into the target map dir first.
500
+
501
+ Assets must land next to the saved map BEFORE save_map runs, and each
502
+ loaded tileset path must be re-pointed at the copied file, so the saved
503
+ JSON gets clean project-relative 'assets/...' paths instead of
504
+ '../sandbox/...' escapes.
505
+ """
506
+ assets_src = self._sandbox_root / "assets"
507
+ if not assets_src.is_dir():
508
+ self.notifications.error("Export failed", "No assets/ directory in sandbox")
509
+ return
510
+ assets_dst = Path(path).parent / "assets"
511
+ conflicts = []
512
+ if assets_dst.is_dir():
513
+ conflicts = [p.name for p in assets_src.iterdir() if p.is_file() and (assets_dst / p.name).exists()]
514
+ if conflicts:
515
+ names = ", ".join(sorted(conflicts)[:5])
516
+ more = f" (+{len(conflicts) - 5} more)" if len(conflicts) > 5 else ""
517
+ self.confirm_dialog.show(
518
+ title="Overwrite Assets?",
519
+ message=(
520
+ f"{len(conflicts)} asset file(s) already exist in {assets_dst.name}/: "
521
+ f"{names}{more}\nThey will be overwritten by the sandbox copies."
522
+ ),
523
+ on_confirm=lambda: self._sandbox_export_map(path, assets_src, assets_dst),
524
+ on_cancel=lambda: None,
525
+ )
526
+ return
527
+ self._sandbox_export_map(path, assets_src, assets_dst)
528
+
529
+ def _sandbox_export_map(self, path: Path, assets_src: Path, assets_dst: Path):
530
+ redirected: list[tuple[object, Path]] = []
531
+ newly_copied: list[Path] = []
532
+ # Snapshot staged outputs before any writes
533
+ map_existed = path.exists()
534
+ try:
535
+ proj_nodes_dir = self._project_data_root / self.config.get(
536
+ "nodes_path", "nodes"
537
+ )
538
+ sidecar_path = proj_nodes_dir / f"{path.stem}.nodes.json"
539
+ sidecar_existed = sidecar_path.exists()
540
+ except Exception:
541
+ sidecar_path = None
542
+ sidecar_existed = False
543
+ try:
544
+ assets_dst.mkdir(parents=True, exist_ok=True)
545
+ for src in assets_src.iterdir():
546
+ if src.is_file():
547
+ dst = assets_dst / src.name
548
+ existed = dst.exists()
549
+ shutil.copy2(src, dst)
550
+ if not existed:
551
+ newly_copied.append(dst)
552
+
553
+ ts_widget = getattr(self, "tileset_widget", None)
554
+ if ts_widget is not None and self._sandbox_root is not None:
555
+ sandbox_root = self._sandbox_root.resolve()
556
+ for ts in ts_widget.tilesets:
557
+ ts_path = getattr(ts, "path", None)
558
+ if ts_path is None:
559
+ continue
560
+ try:
561
+ inside_sandbox = ts_path.resolve().is_relative_to(sandbox_root)
562
+ except OSError:
563
+ inside_sandbox = False
564
+ if inside_sandbox:
565
+ redirected.append((ts, Path(ts_path)))
566
+ ts.path = assets_dst / ts_path.name
567
+
568
+ # Restore real-data context BEFORE saving so the node sidecar is
569
+ # written next to the exported map (NodeManager falls back to
570
+ # <map>.nodes.json once the sandbox pointer is cleared). Prior
571
+ # state is reinstated if the save fails.
572
+ prev_data_root = self.data_root
573
+ prev_is_sandbox = self.is_sandbox
574
+ prev_nodes_dir = (
575
+ getattr(self.node_manager, "_nodes_dir", None)
576
+ if hasattr(self, "node_manager")
577
+ else None
578
+ )
579
+ prev_active_sidecar = getattr(
580
+ self.node_manager, "_active_sidecar", None
581
+ )
582
+ prev_project_path = self.tilemap.active_project_path
583
+ self.data_root = self._project_data_root
584
+ self.is_sandbox = False
585
+ if hasattr(self, "node_manager"):
586
+ self.node_manager.reset_nodes_dir()
587
+
588
+ try:
589
+ ok = self.tilemap.save_map(path)
590
+ if not ok:
591
+ self.data_root = prev_data_root
592
+ self.is_sandbox = prev_is_sandbox
593
+ if hasattr(self, "node_manager"):
594
+ self.node_manager._nodes_dir = prev_nodes_dir
595
+ self.node_manager._active_sidecar = prev_active_sidecar
596
+ self.tilemap.active_project_path = prev_project_path
597
+ for ts, original in redirected:
598
+ try:
599
+ ts.path = original
600
+ except Exception:
601
+ pass
602
+ # Remove staged outputs created for this export
603
+ if not map_existed:
604
+ try:
605
+ path.unlink(missing_ok=True)
606
+ except Exception:
607
+ pass
608
+ if sidecar_path is not None and not sidecar_existed:
609
+ try:
610
+ sidecar_path.unlink(missing_ok=True)
611
+ except Exception:
612
+ pass
613
+ for dst in newly_copied:
614
+ try:
615
+ dst.unlink(missing_ok=True)
616
+ except Exception:
617
+ pass
618
+ return
619
+ except Exception:
620
+ self.data_root = prev_data_root
621
+ self.is_sandbox = prev_is_sandbox
622
+ if hasattr(self, "node_manager"):
623
+ self.node_manager._nodes_dir = prev_nodes_dir
624
+ self.node_manager._active_sidecar = prev_active_sidecar
625
+ self.tilemap.active_project_path = prev_project_path
626
+ if not map_existed:
627
+ try:
628
+ path.unlink(missing_ok=True)
629
+ except Exception:
630
+ pass
631
+ if sidecar_path is not None and not sidecar_existed:
632
+ try:
633
+ sidecar_path.unlink(missing_ok=True)
634
+ except Exception:
635
+ pass
636
+ for dst in newly_copied:
637
+ try:
638
+ dst.unlink(missing_ok=True)
639
+ except Exception:
640
+ pass
641
+ raise
642
+
643
+ self.notifications.success(f"Exported to {path.name}")
644
+ logger.info(f"Sandbox session exported to {path}; data_root restored to {self.data_root}")
645
+ except Exception as e:
646
+ # undo path redirection so the editor keeps working off the sandbox
647
+ for ts, original in redirected:
648
+ try:
649
+ ts.path = original
650
+ except Exception:
651
+ error_handler.capture(e, context="sandbox_restore_paths")
652
+ # Remove staged outputs that were newly created for this export
653
+ for dst in newly_copied:
654
+ try:
655
+ dst.unlink(missing_ok=True)
656
+ except Exception:
657
+ pass
658
+ if not map_existed:
659
+ try:
660
+ path.unlink(missing_ok=True)
661
+ except Exception:
662
+ pass
663
+ if sidecar_path is not None and not sidecar_existed:
664
+ try:
665
+ sidecar_path.unlink(missing_ok=True)
666
+ except Exception:
667
+ pass
668
+ error_handler.capture(e, context="sandbox_export")
669
+ self.notifications.error(
670
+ "Export failed",
671
+ f"{e} Copied asset files may remain in {assets_dst}.",
672
+ )
673
+
461
674
  def export_selection_as_png(self):
462
675
  if self.tile_grid_widget is None:
463
676
  self.notifications.notify("No tile grid available", duration=2.0)
@@ -570,6 +783,10 @@ class Editor:
570
783
  try:
571
784
  self.handle_resize(self.width, self.height)
572
785
  self.tilemap.apply_map_payload(path, payload_or_error)
786
+ if getattr(self, "is_sandbox", False):
787
+ self.notifications.notify(
788
+ f"SANDBOX map loaded: {path.name}", duration=4.0
789
+ )
573
790
  except Exception as e:
574
791
  error_handler.capture(e, context="load_map_apply")
575
792
  import traceback
@@ -613,11 +830,8 @@ class Editor:
613
830
  self.width = width
614
831
  self.height = height
615
832
 
616
- menu_h = 30
617
- toolbar_h = 35
618
-
619
833
  if hasattr(self, "menubar") and self.menubar:
620
- self.menubar.resize(width)
834
+ self.menubar.resize(0, 0, width, self.menubar.rect.height)
621
835
 
622
836
  if hasattr(self, "toolbar") and self.toolbar:
623
837
  self.toolbar.resize(width)
@@ -1263,6 +1477,18 @@ class Editor:
1263
1477
  def run(self):
1264
1478
  self.running = True
1265
1479
  frame_count = 0
1480
+ if self._sandbox_map_path is not None:
1481
+ map_path = self._sandbox_map_path
1482
+ self._sandbox_map_path = None
1483
+ try:
1484
+ if map_path.is_file():
1485
+ self.start_async_load_map(map_path)
1486
+ self.notifications.notify(
1487
+ f"SANDBOX mode: loading {_display_load_path(map_path)}...",
1488
+ duration=4.0,
1489
+ )
1490
+ except Exception as e:
1491
+ logger.warning(f"Sandbox auto-load failed: {e}")
1266
1492
  while self.running:
1267
1493
  self._poll_async_load()
1268
1494
  self._poll_file_manager_result()
@@ -1291,14 +1517,14 @@ class Editor:
1291
1517
  if self.autotiler:
1292
1518
  if self.autotiler.visible:
1293
1519
  overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
1294
- overlay.fill((0, 0, 0, 100))
1520
+ overlay.fill((*COLORS.overlay, 100))
1295
1521
  self.screen.blit(overlay, (0, 0))
1296
1522
  self.autotiler.draw(self.screen)
1297
1523
 
1298
1524
  if self.regex_automap_designer:
1299
1525
  if self.regex_automap_designer.visible:
1300
1526
  overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
1301
- overlay.fill((0, 0, 0, 100))
1527
+ overlay.fill((*COLORS.overlay, 100))
1302
1528
  self.screen.blit(overlay, (0, 0))
1303
1529
  self.regex_automap_designer.draw(self.screen)
1304
1530
 
@@ -1306,7 +1532,7 @@ class Editor:
1306
1532
 
1307
1533
  if self.map_setup_widget and self.map_setup_widget.visible:
1308
1534
  overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
1309
- overlay.fill((0, 0, 0, 150))
1535
+ overlay.fill((*COLORS.overlay, 140))
1310
1536
  self.screen.blit(overlay, (0, 0))
1311
1537
  self.map_setup_widget.draw(self.screen)
1312
1538
 
@@ -1317,25 +1543,25 @@ class Editor:
1317
1543
  self.save_input.draw(self.screen)
1318
1544
  if self.tileset_type_dialog.active:
1319
1545
  overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
1320
- overlay.fill((0, 0, 0, 150))
1546
+ overlay.fill((*COLORS.overlay, 140))
1321
1547
  self.screen.blit(overlay, (0, 0))
1322
1548
  self.tileset_type_dialog.draw(self.screen)
1323
1549
  if self.resize_map_dialog and self.resize_map_dialog.active:
1324
1550
  self.resize_map_dialog.draw(self.screen)
1325
1551
  if self.layer_type_dialog.active:
1326
1552
  overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
1327
- overlay.fill((0, 0, 0, 150))
1553
+ overlay.fill((*COLORS.overlay, 140))
1328
1554
  self.screen.blit(overlay, (0, 0))
1329
1555
  self.layer_type_dialog.draw(self.screen)
1330
1556
  if self.confirm_dialog.active:
1331
1557
  overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
1332
- overlay.fill((0, 0, 0, 150))
1558
+ overlay.fill((*COLORS.overlay, 140))
1333
1559
  self.screen.blit(overlay, (0, 0))
1334
1560
  self.confirm_dialog.draw(self.screen)
1335
1561
 
1336
1562
  if self.loading_state["active"]:
1337
1563
  overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
1338
- overlay.fill((0, 0, 0, 180))
1564
+ overlay.fill((*COLORS.overlay, 170))
1339
1565
  self.screen.blit(overlay, (0, 0))
1340
1566
 
1341
1567
  msg = self.loading_state.get("message", "Loading...")
@@ -25,6 +25,7 @@ class NodeManager:
25
25
  self.groups: list[str] = []
26
26
  self.default_node_type: str = "area"
27
27
  self._nodes_dir: Path | None = None
28
+ self._active_sidecar: Path | None = None
28
29
 
29
30
  @property
30
31
  def nodes_dir(self) -> Path:
@@ -34,6 +35,11 @@ class NodeManager:
34
35
  self._nodes_dir = self.editor.data_root / rel
35
36
  return self._nodes_dir
36
37
 
38
+ def reset_nodes_dir(self) -> None:
39
+ """Drop the cached dir so the next access re-derives from data_root."""
40
+ self._nodes_dir = None
41
+ self._active_sidecar = None
42
+
37
43
  def _sidecar_path_for(self, map_path: Path) -> Path:
38
44
  return self.nodes_dir / f"{map_path.stem}.nodes.json"
39
45
 
@@ -42,9 +48,16 @@ class NodeManager:
42
48
  self.groups.clear()
43
49
  self.active_node_id = None
44
50
  self.active_group_name = None
51
+ self._active_sidecar: Path | None = None
45
52
  sidecar = self._sidecar_path_for(map_path)
46
53
  if not sidecar.is_file():
47
- return
54
+ fallback = map_path.parent / f"{map_path.stem}.nodes.json"
55
+ if fallback.is_file():
56
+ sidecar = fallback
57
+ else:
58
+ self._active_sidecar = None
59
+ return
60
+ self._active_sidecar = sidecar
48
61
  try:
49
62
  raw = json.loads(sidecar.read_text(encoding="utf-8"))
50
63
  if not isinstance(raw, dict):
@@ -65,7 +78,13 @@ class NodeManager:
65
78
  logging.warning(f"Failed to load nodes from {sidecar}")
66
79
 
67
80
  def save(self, map_path: Path) -> None:
68
- sidecar = self._sidecar_path_for(map_path)
81
+ # Trust the loaded sidecar only while it IS the canonical one for the
82
+ # requested map. A diverged pointer (e.g. left over from a sandbox
83
+ # session) must never swallow writes aimed elsewhere: after an export
84
+ # the canonical location wins so the sandbox copy stays frozen.
85
+ fallback = self._sidecar_path_for(map_path)
86
+ active = self._active_sidecar
87
+ sidecar = fallback if (active is None or active != fallback) else active
69
88
  self.nodes_dir.mkdir(parents=True, exist_ok=True)
70
89
  rs = self.editor.tilemap.render_scale
71
90
  orig_areas: dict[str, tuple[int, int]] = {}
@@ -73,14 +92,20 @@ class NodeManager:
73
92
  orig_areas[nid] = (node.area.x, node.area.y)
74
93
  node.area.x = int(node.area.x / rs)
75
94
  node.area.y = int(node.area.y / rs)
95
+ tmp_sidecar = sidecar.with_name(f".{sidecar.name}.tmp")
76
96
  try:
77
97
  data: dict[str, Any] = {
78
98
  "version": SIDECAR_VERSION,
79
99
  "groups": self.groups,
80
100
  "nodes": [node.to_dict() for node in self.nodes.values()],
81
101
  }
82
- sidecar.write_text(json.dumps(data, indent=2), encoding="utf-8")
102
+ tmp_sidecar.write_text(json.dumps(data, indent=2), encoding="utf-8")
103
+ tmp_sidecar.replace(sidecar)
83
104
  finally:
105
+ try:
106
+ tmp_sidecar.unlink(missing_ok=True)
107
+ except Exception:
108
+ pass
84
109
  for nid, (ox, oy) in orig_areas.items():
85
110
  self.nodes[nid].area.x = ox
86
111
  self.nodes[nid].area.y = oy
@@ -1,6 +1,7 @@
1
1
 
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  import re
5
6
  from pathlib import Path
6
7
 
@@ -119,16 +120,41 @@ class CharacterCollisionEditor:
119
120
  x += 100 + 8
120
121
 
121
122
  def _rel_path(self, path: Path) -> str:
123
+ resolved = path.resolve()
122
124
  try:
123
- return str(path.relative_to(self._data_root)) if self._data_root else str(path.resolve())
125
+ if self._data_root:
126
+ rel = os.path.relpath(resolved, self._data_root.resolve())
127
+ # relpath happily emits '../' escapes for paths outside the
128
+ # root — only trust the relative form when it stays inside
129
+ if not rel.split(os.sep)[0].startswith(".."):
130
+ return rel
131
+ return str(resolved)
124
132
  except ValueError:
125
- return str(path.resolve())
133
+ return str(resolved)
126
134
 
127
- def _resolve_image_path(self, image_path: str) -> Path | None:
135
+ def _resolve_image_path(
136
+ self, image_path: str, base: Path | None = None
137
+ ) -> Path | None:
138
+ """Resolve an image reference, mirroring animation runtime_load.
139
+
140
+ Tries, in order: relative to *base* and each of its ancestors (the
141
+ collision file's directory, so ``../../`` refs resolve), relative to
142
+ the data root, then CWD-relative / absolute as-is.
143
+ """
128
144
  p = Path(image_path)
129
- if not p.is_absolute() and self._data_root:
130
- p = self._data_root / p
131
- return p if p.exists() else None
145
+ if p.is_absolute():
146
+ return p if p.exists() else None
147
+ candidates: list[Path] = []
148
+ if base is not None:
149
+ candidates.append(base / p)
150
+ candidates.extend(parent / p for parent in base.parents)
151
+ if self._data_root is not None:
152
+ candidates.append(self._data_root / p)
153
+ candidates.append(p)
154
+ for cand in candidates:
155
+ if cand.exists():
156
+ return cand
157
+ return None
132
158
 
133
159
  def _load_sprite_from_path(self, path: Path) -> bool:
134
160
  try:
@@ -297,8 +323,14 @@ class CharacterCollisionEditor:
297
323
  },
298
324
  )
299
325
 
300
- def load_collision_data(self, data: CharacterCollisionData) -> None:
301
- """Load collision data + auto-resolve sprite from image_path"""
326
+ def load_collision_data(
327
+ self, data: CharacterCollisionData, resolve_base: Path | None = None
328
+ ) -> None:
329
+ """Load collision data + auto-resolve sprite from image_path.
330
+
331
+ resolve_base: directory the image_path is relative to (the loaded
332
+ collision file's parent); falls back to the data root.
333
+ """
302
334
  self.character_name = data.name
303
335
  self._name_input.text = data.name
304
336
  self._name_input.cursor_pos = len(data.name)
@@ -309,9 +341,10 @@ class CharacterCollisionEditor:
309
341
 
310
342
  self._image_path = data.image_path
311
343
  if self._image_path:
312
- resolved = self._resolve_image_path(self._image_path)
344
+ resolved = self._resolve_image_path(self._image_path, base=resolve_base)
313
345
  if resolved:
314
346
  self._load_sprite_from_path(resolved)
347
+ self._image_path = self._rel_path(resolved)
315
348
  self._toast_manager.success(f"Sprite loaded: {resolved.name}")
316
349
 
317
350
  self.shape_editor.load_shape_data(data.shape.to_dict())
@@ -392,7 +425,7 @@ class CharacterCollisionEditor:
392
425
  data_dict = json.load(f)
393
426
  self._validate_collision_data(data_dict)
394
427
  data = CharacterCollisionData.from_dict(data_dict)
395
- self.load_collision_data(data)
428
+ self.load_collision_data(data, resolve_base=path.parent)
396
429
  except json.JSONDecodeError:
397
430
  raise ValueError("Invalid collision file: not valid JSON") from None
398
431
  except KeyError as e:
@@ -423,8 +456,6 @@ class CharacterCollisionEditor:
423
456
  return True
424
457
  return True
425
458
 
426
- mouse = pygame.mouse.get_pos()
427
-
428
459
  if self.layer_sidebar.handle_event(event):
429
460
  return True
430
461
 
@@ -729,7 +729,7 @@ class ObjectTilesetCollisionEditor:
729
729
  return
730
730
 
731
731
  overlay = Surface(screen.get_size(), pygame.SRCALPHA)
732
- overlay.fill((0, 0, 0, 180))
732
+ overlay.fill((*COLORS.overlay, 170))
733
733
  screen.blit(overlay, (0, 0))
734
734
 
735
735
  panel_w = 500