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.
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/CHANGELOG.md +34 -0
- {tilemap_editor-4.3.9/src/tilemap_editor.egg-info → tilemap_editor-5.0.0}/PKG-INFO +1 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/pyproject.toml +1 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/editor.py +238 -12
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/node_manager.py +28 -3
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/editor.py +43 -12
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/editor.py +1 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/editor.py +132 -15
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/frame_picker.py +53 -7
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/timeline.py +56 -5
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/commands.py +76 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/document.py +81 -4
- tilemap_editor-5.0.0/src/plugins/sprite_editor/editor.py +1258 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/selection.py +8 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/standalone.py +1 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/tools.py +586 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/viewport.py +6 -2
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/collision_painter.py +1 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/standalone_automap.py +5 -2
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/standalone_image_viewer.py +23 -12
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap.py +60 -3
- tilemap_editor-5.0.0/src/tilemap_editor/assets/icons/redo.svg +1 -0
- tilemap_editor-5.0.0/src/tilemap_editor/assets/icons/text.svg +1 -0
- tilemap_editor-5.0.0/src/tilemap_editor/assets/icons/undo.svg +1 -0
- tilemap_editor-5.0.0/src/tilemap_editor/cli.py +170 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0/src/tilemap_editor.egg-info}/PKG-INFO +1 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/SOURCES.txt +16 -0
- tilemap_editor-5.0.0/src/utils/context_dispatch.py +83 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/font_manager.py +4 -4
- tilemap_editor-5.0.0/src/utils/tileset_hierarchy.py +173 -0
- tilemap_editor-5.0.0/src/utils/tileset_ops.py +208 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/autotile_template.py +7 -5
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/autotiler.py +60 -38
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/filemanager.py +26 -4
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/layer_selector.py +85 -74
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/map_properties.py +1 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/mapsetup.py +2 -2
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/regex_automap_designer.py +19 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/tile_grid.py +49 -13
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/tile_selector.py +381 -55
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/button.py +31 -7
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/checkbox.py +9 -9
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/collision_layer_sidebar.py +1 -1
- tilemap_editor-5.0.0/src/widgets/ui/context_menu.py +161 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/dialog_base.py +2 -2
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/draw_utils.py +4 -2
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/fileinput.py +7 -2
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/menubar.py +89 -33
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/node_editor.py +26 -10
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/node_selector.py +4 -2
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/notification.py +6 -5
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/particle_config_dialog.py +61 -55
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/property_editor.py +83 -23
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/resize_map_dialog.py +1 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/scrollbar.py +8 -6
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/sidebar_container.py +16 -10
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/status_bar.py +4 -5
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/theme.py +111 -38
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/toolbar.py +5 -5
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/tooltip.py +1 -1
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/tree_widget.py +94 -5
- tilemap_editor-5.0.0/tests/test_context_dispatch.py +287 -0
- tilemap_editor-5.0.0/tests/test_object_properties_context.py +123 -0
- tilemap_editor-5.0.0/tests/test_sandbox_mode.py +461 -0
- tilemap_editor-5.0.0/tests/test_sprite_animation_gestures.py +375 -0
- tilemap_editor-5.0.0/tests/test_sprite_editor_drop.py +497 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_sprite_editor_interaction.py +98 -34
- tilemap_editor-5.0.0/tests/test_sprite_editor_stack.py +168 -0
- tilemap_editor-5.0.0/tests/test_theme.py +74 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_tile_selector_pick.py +41 -0
- tilemap_editor-5.0.0/tests/test_tileset_hierarchy.py +447 -0
- tilemap_editor-5.0.0/tests/test_tileset_integrity.py +567 -0
- tilemap_editor-4.3.9/src/plugins/sprite_editor/editor.py +0 -658
- tilemap_editor-4.3.9/src/tilemap_editor/cli.py +0 -94
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/LICENSE +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/MANIFEST.in +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/README.md +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/jetbrain-fonts/JetBrainsMono-Bold.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/jetbrain-fonts/JetBrainsMono-BoldItalic.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/jetbrain-fonts/JetBrainsMono-Italic.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/jetbrain-fonts/JetBrainsMono-Regular.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/noto/NotoSans-Bold.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/noto/NotoSans-BoldItalic.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/noto/NotoSans-Italic.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/arrow-down.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/check.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/checked.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/close.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/duplicate.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/error.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/file.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/filedead.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/fit.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/folder.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/grid.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/image.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/info.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/load.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/loop.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/onion.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/pan.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/pause.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/pencil.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/play.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/plus.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/radio.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/reset.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/save.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/skip-back.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/skip-forward.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/stop.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/tilemap.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/tileset.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/unchecked.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/warning.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/zoomin.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/assets/icons/zoomout.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/setup.cfg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/constants.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/event_map.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/layers.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/main.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/nodes.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/models.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/protocols.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/shape_editor.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/character_collision/standalone.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/models.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/protocols.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/object_tileset_collision/standalone.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/__main__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/clipboard_util.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/models.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/preview.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/protocols.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/runtime_load.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/standalone.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_animation/validation.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/camera.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/clipboard.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/dialogs.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/overlays.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/sprite_editor/region_export.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/editor.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/models.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/protocols.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/plugins/tileset_collision/standalone.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/run.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/standalone_error_console.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/standalone_filemanager.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/__main__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/jetbrain-fonts/JetBrainsMono-Bold.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/jetbrain-fonts/JetBrainsMono-BoldItalic.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/jetbrain-fonts/JetBrainsMono-Italic.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/jetbrain-fonts/JetBrainsMono-Regular.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/noto/NotoSans-Bold.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/noto/NotoSans-BoldItalic.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/noto/NotoSans-Italic.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/ToolMove.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/arrow-down.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/check.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/checked.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/close.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/duplicate.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/error.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/file.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/filedead.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/fit.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/fold_down_arrow.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/folder.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/gear.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/grid.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/image.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/info.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/load.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/loop.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/miniobj.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/onion.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/pan.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/pause.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/pencil.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/play.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/plus.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/radio.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/reset.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/save.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/skip-back.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/skip-forward.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/stop.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/tilemap.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/tileset.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/unchecked.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/unfold_right_arrow.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/warning.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/zoomin.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/assets/icons/zoomout.svg +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/main.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor/settings.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/dependency_links.txt +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/entry_points.txt +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/requires.txt +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/tilemap_editor.egg-info/top_level.txt +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/ttypes/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/ttypes/tilemap.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/editor_preference.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/error_handler.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/history.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/icon_manager.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/icons_cache.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/log_capture.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/natural_sort.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/project_paths.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/property_suggestions.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/serialization.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/standalone.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/utils/validation.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/__init__.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/automap_models.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/input.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/particle_presets.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/particle_system.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/collision_layer_mask.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/confirm_dialog.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/drag_tracker.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/label.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/layer_type_dialog.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/mode_indicator.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/region_selector.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/splitter.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/tileset_type_dialog.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/toast.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/ui/tool_manager.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/src/widgets/widget_base.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_animation_library_grid_offset.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_collision_layer_mask.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_collision_layer_sidebar.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_drag_tracker.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_editor_pan_mode.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_gid_collision.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_layers_autotile.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_map_resize_model.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_natural_sort.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_project_paths.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_render_scale.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_serialization_regression.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_sprite_animation_editor_grid.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_sprite_editor_commands.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_sprite_editor_models.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_tile_grid_selection.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_tilemap_save.py +0 -0
- {tilemap_editor-4.3.9 → tilemap_editor-5.0.0}/tests/test_timeline_invalidate_cache.py +0 -0
- {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
|
|
@@ -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(
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
133
|
+
return str(resolved)
|
|
126
134
|
|
|
127
|
-
def _resolve_image_path(
|
|
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
|
|
130
|
-
p
|
|
131
|
-
|
|
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(
|
|
301
|
-
|
|
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
|
|