Add fork features: host view sync, grouping, image compress, freistellen

Host can force all clients to their camera; selection Group/Ungroup with
synced groupIds; WebP/PNG preprocess and Tower rembg/white-key freistellen
on insert or via selection actions (save format 0.7 / INFPNT000007).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
copilot 2026-09-21 16:28:47 +02:00
parent df49687f51
commit 042fe8f5a6
19 changed files with 672 additions and 9 deletions

View file

@ -115,6 +115,7 @@ set(sources "src/main.cpp"
"src/CanvasComponents/ImageCanvasComponent.cpp" "src/CanvasComponents/ImageCanvasComponent.cpp"
"src/CanvasComponents/BrushComponentCode.cpp" "src/CanvasComponents/BrushComponentCode.cpp"
"src/CanvasComponents/CanvasComponent.cpp" "src/CanvasComponents/CanvasComponent.cpp"
"src/ImagePreprocess.cpp"
"src/GUIStuff/GUIManager.cpp" "src/GUIStuff/GUIManager.cpp"
"src/GUIStuff/GUIFloatAnimation.cpp" "src/GUIStuff/GUIFloatAnimation.cpp"
"src/GUIStuff/GUIManagerID.cpp" "src/GUIStuff/GUIManagerID.cpp"

33
FORK.md Normal file
View file

@ -0,0 +1,33 @@
# Fork features (copilot/infinipaint)
Upstream: [ErrorAtLine0/infinipaint](https://github.com/ErrorAtLine0/infinipaint)
Forgejo: https://forgejo.fsociety00.cc/copilot/infinipaint
Save format: `INFPNT000007` / version `0.7.0-fork` (not network-compatible with stock 0.6.x peers).
## Bring Everyone Here (host)
In the player list, the lobby host gets **Bring Everyone Here**. Clients receive `CLIENT_FORCE_CAMERA_JUMP` and run the same `smooth_move_to` path as **Jump To**.
## Group / Ungroup
With a multi-selection (images, text boxes, shapes, …), use **Group** / **Ungroup** in the selection panel. Grouped objects auto-expand into the selection so they transform together. Membership syncs via `SET_COMPONENT_GROUP_IDS`.
## Image compression
Settings → **Images**:
- Compress on insert (default on)
- Format: Keep / WebP / PNG
- WebP quality
Selection toolbar: **Compress Images** re-encodes already placed images.
## Freistellen
Settings → **Images**:
- Freistellen on insert
- Modes: White background (always), or rembg **Schnell** (`silueta`) / **Allgemein** (`bria-rmbg`) when Tower rembg is installed (`~/.local/share/tower-rembg` or `rembg` on `PATH`)
Selection: **Freistellen** applies the configured mode to selected images (then compresses).

View file

@ -19,6 +19,8 @@
InfiniPaint is a **collaborative, infinite** canvas note-taking/drawing app. Unlike many other infinite canvas apps, **there is no zoom-in or zoom-out limit** (at least up until the point your computer runs out of memory). This means that this app is very good at things such as drawing sketches of the solar system to scale, or just drawing any massive objects with tiny details. Of course, even though this is a feature, this app is also perfectly well suited for use as a normal canvas. InfiniPaint is a **collaborative, infinite** canvas note-taking/drawing app. Unlike many other infinite canvas apps, **there is no zoom-in or zoom-out limit** (at least up until the point your computer runs out of memory). This means that this app is very good at things such as drawing sketches of the solar system to scale, or just drawing any massive objects with tiny details. Of course, even though this is a feature, this app is also perfectly well suited for use as a normal canvas.
> **This repository is a fork** hosted at [forgejo.fsociety00.cc/copilot/infinipaint](https://forgejo.fsociety00.cc/copilot/infinipaint). Extra features (host view sync, grouping, image compression, freistellen) are documented in [FORK.md](FORK.md).
You can try the web version of this app at [infinipaint.com](https://infinipaint.com) (requires a WebGL2 capable browser). The web version was compiled from C++ to Javascript using Emscripten, and may contain some bugs, so if possible, please consider downloading the native version for a better experience. You can try the web version of this app at [infinipaint.com](https://infinipaint.com) (requires a WebGL2 capable browser). The web version was compiled from C++ to Javascript using Emscripten, and may contain some bugs, so if possible, please consider downloading the native version for a better experience.
This app was inspired by [Lorien](https://github.com/mbrlabs/Lorien), another great infinite canvas program. This app was inspired by [Lorien](https://github.com/mbrlabs/Lorien), another great infinite canvas program.

View file

@ -49,7 +49,7 @@ CanvasComponentContainer::CanvasComponentContainer(NetworkingObjects::NetObjMana
void CanvasComponentContainer::register_class(World& w) { void CanvasComponentContainer::register_class(World& w) {
auto readConstructorFunc = [&w](const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer>& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr<NetServer::ClientData>& c) { auto readConstructorFunc = [&w](const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer>& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr<NetServer::ClientData>& c) {
a(o->coords); a(o->coords, o->groupId);
o->compAllocator = o.get_obj_man()->read_create_message<CanvasComponentAllocator>(a, c); o->compAllocator = o.get_obj_man()->read_create_message<CanvasComponentAllocator>(a, c);
o->compAllocator->comp->compContainer = o.get(); o->compAllocator->comp->compContainer = o.get();
canvas_scale_up_check(*o, w, c); canvas_scale_up_check(*o, w, c);
@ -72,6 +72,7 @@ std::unique_ptr<CanvasComponentContainer::CopyData> CanvasComponentContainer::ge
auto toRet = std::make_unique<CanvasComponentContainer::CopyData>(); auto toRet = std::make_unique<CanvasComponentContainer::CopyData>();
toRet->coords = coords; toRet->coords = coords;
toRet->obj = compAllocator->comp->get_data_copy(); toRet->obj = compAllocator->comp->get_data_copy();
// groupId intentionally not copied — paste creates independent objects
return toRet; return toRet;
} }
@ -84,18 +85,22 @@ void CanvasComponentContainer::set_object_update_lock(DrawingProgram& drawP, boo
} }
void CanvasComponentContainer::write_constructor_func(const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer>& o, cereal::PortableBinaryOutputArchive& a) { void CanvasComponentContainer::write_constructor_func(const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer>& o, cereal::PortableBinaryOutputArchive& a) {
a(o->coords); a(o->coords, o->groupId);
o->compAllocator.write_create_message(a); o->compAllocator.write_create_message(a);
} }
void CanvasComponentContainer::save_file(cereal::PortableBinaryOutputArchive& a) const { void CanvasComponentContainer::save_file(cereal::PortableBinaryOutputArchive& a) const {
a(coords); a(coords, groupId);
compAllocator->save_file(a); compAllocator->save_file(a);
} }
void CanvasComponentContainer::load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version, NetObjManager& objMan) { void CanvasComponentContainer::load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version, NetObjManager& objMan) {
if(version >= VersionNumber(0, 4, 0)) { if(version >= VersionNumber(0, 4, 0)) {
a(coords); a(coords);
if(version >= VersionNumber(0, 7, 0))
a(groupId);
else
groupId = 0;
compAllocator = objMan.make_obj_direct<CanvasComponentAllocator>(); compAllocator = objMan.make_obj_direct<CanvasComponentAllocator>();
compAllocator->load_file(a, version); compAllocator->load_file(a, version);
compAllocator->comp->compContainer = this; compAllocator->comp->compContainer = this;
@ -104,6 +109,7 @@ void CanvasComponentContainer::load_file(cereal::PortableBinaryInputArchive& a,
CanvasComponentType t; CanvasComponentType t;
NetworkingObjects::NetObjID uselessID; NetworkingObjects::NetObjID uselessID;
a(t, uselessID, coords); a(t, uselessID, coords);
groupId = 0;
compAllocator = objMan.make_obj_direct<CanvasComponentAllocator>(t); compAllocator = objMan.make_obj_direct<CanvasComponentAllocator>(t);
compAllocator->comp->load_file(a, version); compAllocator->comp->load_file(a, version);
compAllocator->comp->compContainer = this; compAllocator->comp->compContainer = this;

View file

@ -101,6 +101,8 @@ class CanvasComponentContainer {
std::weak_ptr<DrawingProgramCacheBVHNode> cacheParentBvhNode; std::weak_ptr<DrawingProgramCacheBVHNode> cacheParentBvhNode;
DrawingProgramLayerListItem* parentLayer = nullptr; DrawingProgramLayerListItem* parentLayer = nullptr;
CoordSpaceHelper coords; CoordSpaceHelper coords;
// 0 = ungrouped. Non-zero IDs move/transform together when any member is selected.
uint64_t groupId = 0;
ObjInfoIterator objInfo; ObjInfoIterator objInfo;
private: private:
friend class BrushStrokeCanvasComponent; friend class BrushStrokeCanvasComponent;

View file

@ -28,7 +28,9 @@ enum ServerCommands : MessageCommandType {
SERVER_UPDATE_NETWORK_OBJECT, SERVER_UPDATE_NETWORK_OBJECT,
SERVER_NEW_RESOURCE_ID, SERVER_NEW_RESOURCE_ID,
SERVER_NEW_RESOURCE_DATA, SERVER_NEW_RESOURCE_DATA,
SERVER_TRANSFORM_MANY_COMPONENTS SERVER_TRANSFORM_MANY_COMPONENTS,
SERVER_FORCE_CAMERA_JUMP,
SERVER_SET_COMPONENT_GROUP_IDS
}; };
enum ClientCommands : MessageCommandType { enum ClientCommands : MessageCommandType {
@ -39,5 +41,7 @@ enum ClientCommands : MessageCommandType {
CLIENT_UPDATE_NETWORK_OBJECT, CLIENT_UPDATE_NETWORK_OBJECT,
CLIENT_NEW_RESOURCE_ID, CLIENT_NEW_RESOURCE_ID,
CLIENT_NEW_RESOURCE_DATA, CLIENT_NEW_RESOURCE_DATA,
CLIENT_TRANSFORM_MANY_COMPONENTS CLIENT_TRANSFORM_MANY_COMPONENTS,
CLIENT_FORCE_CAMERA_JUMP,
CLIENT_SET_COMPONENT_GROUP_IDS
}; };

View file

@ -48,6 +48,12 @@
#include <chrono> #include <chrono>
#include "../CanvasComponents/ImageCanvasComponent.hpp" #include "../CanvasComponents/ImageCanvasComponent.hpp"
#include "Layers/DrawingProgramLayer.hpp" #include "Layers/DrawingProgramLayer.hpp"
#include "../ImagePreprocess.hpp"
#include <Helpers/Random.hpp>
#include <cereal/types/utility.hpp>
#ifdef __ANDROID__
#include "../AndroidJNICalls.hpp"
#endif
#include "Layers/DrawingProgramLayerListItem.hpp" #include "Layers/DrawingProgramLayerListItem.hpp"
#include "../ScaleUpCanvas.hpp" #include "../ScaleUpCanvas.hpp"
@ -336,6 +342,22 @@ void DrawingProgram::init_server_callbacks() {
process_transform_message(transforms); process_transform_message(transforms);
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, transforms); world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, transforms);
}); });
world.netServer->add_recv_callback(SERVER_FORCE_CAMERA_JUMP, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
CoordSpaceHelper coords;
Vector2f windowSize;
message(coords, windowSize);
// Only the host may force jumps; ignore client-originated requests.
(void)client;
(void)coords;
(void)windowSize;
});
world.netServer->add_recv_callback(SERVER_SET_COMPONENT_GROUP_IDS, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> groupIds;
message(groupIds);
(void)client;
process_group_id_message(groupIds);
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_SET_COMPONENT_GROUP_IDS, groupIds);
});
} }
void DrawingProgram::init_client_callbacks() { void DrawingProgram::init_client_callbacks() {
@ -344,6 +366,51 @@ void DrawingProgram::init_client_callbacks() {
message(transforms); message(transforms);
process_transform_message(transforms); process_transform_message(transforms);
}); });
world.netClient->add_recv_callback(CLIENT_FORCE_CAMERA_JUMP, [&](cereal::PortableBinaryInputArchive& message) {
CoordSpaceHelper coords;
Vector2f windowSize;
message(coords, windowSize);
process_force_camera_jump(coords, windowSize);
});
world.netClient->add_recv_callback(CLIENT_SET_COMPONENT_GROUP_IDS, [&](cereal::PortableBinaryInputArchive& message) {
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> groupIds;
message(groupIds);
process_group_id_message(groupIds);
});
}
void DrawingProgram::process_force_camera_jump(const CoordSpaceHelper& coords, const Vector2f& windowSize) {
world.drawData.cam.smooth_move_to(world, coords, windowSize);
}
void DrawingProgram::force_clients_to_own_view() {
if(!world.netServer)
return;
CoordSpaceHelper coords = world.drawData.cam.c;
Vector2f windowSize = world.main.window.size.cast<float>().eval();
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_FORCE_CAMERA_JUMP, coords, windowSize);
}
void DrawingProgram::process_group_id_message(const std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>>& groupIds) {
for(auto& [id, groupId] : groupIds) {
auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer>(id);
if(!objPtr)
continue;
objPtr->groupId = groupId;
}
world.set_to_layout_gui_if_focus();
}
void DrawingProgram::send_group_ids_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs) {
if(!world.netObjMan.is_connected() || objs.empty())
return;
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> groupIds;
for(auto& obj : objs)
groupIds.emplace_back(obj->obj.get_net_id(), obj->obj->groupId);
if(world.netObjMan.is_server())
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_SET_COMPONENT_GROUP_IDS, groupIds);
else
world.netClient->send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_SET_COMPONENT_GROUP_IDS, groupIds);
} }
void DrawingProgram::process_transform_message(const std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>>& transforms) { void DrawingProgram::process_transform_message(const std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>>& transforms) {
@ -726,7 +793,23 @@ void DrawingProgram::input_add_file_to_canvas_callback(const CustomEvents::AddFi
void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos) { void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos) {
if(layerMan.is_a_layer_being_edited()) { if(layerMan.is_a_layer_being_edited()) {
NetworkingObjects::NetObjTemporaryPtr<ResourceData> imageTempPtr = world.rMan.add_resource_file(filePath); ResourceData resource;
#ifdef __ANDROID__
resource.name = AndroidJNICalls::getFileNameFromURI(filePath.string());
if(resource.name.empty())
resource.name = "New Resource";
#else
resource.name = std::filesystem::path(filePath).filename().string();
#endif
try {
resource.data = std::make_shared<std::string>(read_file_to_string(filePath));
}
catch(...) {
Logger::get().log(Logger::LogType::INFO, "[DrawingProgram::add_file_to_canvas_by_path] Could not open file " + filePath.string());
return;
}
ImagePreprocess::preprocess_resource(resource.name, resource.data, ImagePreprocess::options_from_config(world.main.conf));
NetworkingObjects::NetObjTemporaryPtr<ResourceData> imageTempPtr = world.rMan.add_resource(resource);
if(imageTempPtr) { if(imageTempPtr) {
NetworkingObjects::NetObjID imageID = imageTempPtr.get_net_id(); NetworkingObjects::NetObjID imageID = imageTempPtr.get_net_id();
ResourceDisplay* display = world.rMan.get_display_data(imageID); ResourceDisplay* display = world.rMan.get_display_data(imageID);
@ -753,6 +836,7 @@ CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(co
ResourceData newResource; ResourceData newResource;
newResource.data = std::make_shared<std::string>(fileBuffer); newResource.data = std::make_shared<std::string>(fileBuffer);
newResource.name = fileName; newResource.name = fileName;
ImagePreprocess::preprocess_resource(newResource.name, newResource.data, ImagePreprocess::options_from_config(world.main.conf));
NetworkingObjects::NetObjID imageID = world.rMan.add_resource(newResource).get_net_id(); NetworkingObjects::NetObjID imageID = world.rMan.add_resource(newResource).get_net_id();
ResourceDisplay* display = world.rMan.get_display_data(imageID); ResourceDisplay* display = world.rMan.get_display_data(imageID);
Vector2f imTrueDim = display->get_dimensions(); Vector2f imTrueDim = display->get_dimensions();
@ -772,6 +856,32 @@ CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(co
return nullptr; return nullptr;
} }
void DrawingProgram::process_selected_images(bool freistellen, bool compress) {
auto selected = selection.get_selection_as_set();
ImagePreprocess::Options opts = ImagePreprocess::options_from_config(world.main.conf);
opts.freistellenOnInsert = freistellen;
opts.compressOnInsert = compress;
if(!freistellen && !compress)
return;
for(auto* objInfo : selected) {
if(objInfo->obj->get_comp().get_type() != CanvasComponentType::IMAGE)
continue;
auto& img = static_cast<ImageCanvasComponent&>(objInfo->obj->get_comp());
auto resource = world.netObjMan.get_obj_temporary_ref_from_id<ResourceData>(img.d.imageID);
if(!resource || !resource->data)
continue;
ResourceData newRes;
newRes.name = resource->name;
newRes.data = std::make_shared<std::string>(*resource->data);
if(!ImagePreprocess::preprocess_resource(newRes.name, newRes.data, opts))
continue;
img.d.imageID = world.rMan.add_resource(newRes).get_net_id();
objInfo->obj->send_comp_update(*this, true);
objInfo->obj->commit_update(*this);
}
world.rMan.clear_display_cache();
}
float DrawingProgram::drag_point_radius() { float DrawingProgram::drag_point_radius() {
return (world.main.g.gui.io.isTouchDevice ? 10.0f : 8.0f) * world.main.g.final_gui_scale(); return (world.main.g.gui.io.isTouchDevice ? 10.0f : 8.0f) * world.main.g.final_gui_scale();
} }

View file

@ -79,6 +79,9 @@ class DrawingProgram {
void invalidate_cache_at_component(CanvasComponentContainer::ObjInfo* objToCheck); void invalidate_cache_at_component(CanvasComponentContainer::ObjInfo* objToCheck);
void preupdate_component(CanvasComponentContainer::ObjInfo* objToCheck); void preupdate_component(CanvasComponentContainer::ObjInfo* objToCheck);
void send_transforms_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objsToSendTransformsFor); void send_transforms_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objsToSendTransformsFor);
void send_group_ids_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs);
void force_clients_to_own_view();
void process_selected_images(bool freistellen, bool compress);
void on_tab_out(); void on_tab_out();
void input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile); void input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile);
@ -105,6 +108,8 @@ class DrawingProgram {
std::unique_ptr<DrawingProgramToolBase> drawTool; std::unique_ptr<DrawingProgramToolBase> drawTool;
private: private:
void process_transform_message(const std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>>& transforms); void process_transform_message(const std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>>& transforms);
void process_group_id_message(const std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>>& groupIds);
void process_force_camera_jump(const CoordSpaceHelper& coords, const Vector2f& windowSize);
void drag_drop_update(); void drag_drop_update();
void check_updateable_components(); void check_updateable_components();

View file

@ -44,6 +44,9 @@
#include "../GUIStuff/ElementHelpers/RadioButtonHelpers.hpp" #include "../GUIStuff/ElementHelpers/RadioButtonHelpers.hpp"
#include "../GUIStuff/ElementHelpers/LayoutHelpers.hpp" #include "../GUIStuff/ElementHelpers/LayoutHelpers.hpp"
#include "../GUIStuff/ElementHelpers/ButtonHelpers.hpp" #include "../GUIStuff/ElementHelpers/ButtonHelpers.hpp"
#include <Helpers/Random.hpp>
#include <limits>
#include <unordered_set>
DrawingProgramSelection::DrawingProgramSelection(DrawingProgram& initDrawP): DrawingProgramSelection::DrawingProgramSelection(DrawingProgram& initDrawP):
drawP(initDrawP) drawP(initDrawP)
@ -75,6 +78,22 @@ void DrawingProgramSelection::selection_gui(Toolbar& t) {
}); });
text_label(gui, "Stroke Color"); text_label(gui, "Stroke Color");
}); });
left_to_right_line_layout(gui, [&]() {
text_button(gui, "group selection", "Group", {
.onClick = [&] { group_selection(); }
});
text_button(gui, "ungroup selection", "Ungroup", {
.onClick = [&] { ungroup_selection(); }
});
});
left_to_right_line_layout(gui, [&]() {
text_button(gui, "compress selection images", "Compress Images", {
.onClick = [&] { drawP.process_selected_images(false, true); }
});
text_button(gui, "freistellen selection images", "Freistellen", {
.onClick = [&] { drawP.process_selected_images(true, true); }
});
});
} }
}); });
} }
@ -327,14 +346,50 @@ std::function<bool(const std::shared_ptr<DrawingProgramCacheBVHNode>&)> DrawingP
return toRet; return toRet;
} }
void DrawingProgramSelection::expand_selection_with_groups() {
std::unordered_set<uint64_t> groupIds;
for(auto& c : selectedSet) {
if(c->obj->groupId != 0)
groupIds.insert(c->obj->groupId);
}
if(groupIds.empty())
return;
std::unordered_set<CanvasComponentContainer::ObjInfo*> already(selectedSet.begin(), selectedSet.end());
for(auto* c : drawP.layerMan.get_flattened_component_list()) {
if(c->obj->groupId != 0 && groupIds.contains(c->obj->groupId) && !already.contains(c)) {
selectedSet.push_back(c);
already.insert(c);
}
}
}
void DrawingProgramSelection::group_selection() {
if(selectedSet.size() < 2)
return;
uint64_t newGroupId = Random::get().int_range<uint64_t>(1, std::numeric_limits<uint64_t>::max());
for(auto& c : selectedSet)
c->obj->groupId = newGroupId;
drawP.send_group_ids_for(selectedSet);
}
void DrawingProgramSelection::ungroup_selection() {
if(selectedSet.empty())
return;
for(auto& c : selectedSet)
c->obj->groupId = 0;
drawP.send_group_ids_for(selectedSet);
}
void DrawingProgramSelection::add_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) { void DrawingProgramSelection::add_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) {
selectedSet.insert(selectedSet.end(), newSelection.begin(), newSelection.end()); selectedSet.insert(selectedSet.end(), newSelection.begin(), newSelection.end());
expand_selection_with_groups();
sort_selection(); sort_selection();
calculate_aabb(); calculate_aabb();
} }
void DrawingProgramSelection::set_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) { void DrawingProgramSelection::set_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) {
selectedSet = newSelection; selectedSet = newSelection;
expand_selection_with_groups();
sort_selection(); sort_selection();
calculate_aabb(); calculate_aabb();
} }

View file

@ -82,6 +82,9 @@ class DrawingProgramSelection {
void set_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection); void set_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection);
void add_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection); void add_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection);
void expand_selection_with_groups();
void group_selection();
void ungroup_selection();
void calculate_aabb(); void calculate_aabb();
void reset_all(); void reset_all();
void reset_transform_data(); void reset_transform_data();

View file

@ -69,6 +69,12 @@ nlohmann::json GlobalConfig::get_config_json(const InputManager& input) const {
toRet["flipZoomToolDirection"] = flipZoomToolDirection; toRet["flipZoomToolDirection"] = flipZoomToolDirection;
toRet["realTimeEraser"] = realTimeEraser; toRet["realTimeEraser"] = realTimeEraser;
toRet["disableTouchForDrawing"] = disableTouchForDrawing; toRet["disableTouchForDrawing"] = disableTouchForDrawing;
toRet["imageCompressOnInsert"] = imageCompressOnInsert;
toRet["imageCompressFormat"] = imageCompressFormat;
toRet["imageWebpQuality"] = imageWebpQuality;
toRet["imageFreistellenOnInsert"] = imageFreistellenOnInsert;
toRet["imageFreistellenMode"] = imageFreistellenMode;
toRet["imageWhiteKeyThreshold"] = imageWhiteKeyThreshold;
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
toRet["checkForUpdates"] = checkForUpdates; toRet["checkForUpdates"] = checkForUpdates;
#endif #endif
@ -147,6 +153,12 @@ void GlobalConfig::set_config_json(InputManager& input, const nlohmann::json& j,
try{j.at("defaultCanvasBackgroundColor").get_to(defaultCanvasBackgroundColor);} catch(...) {} try{j.at("defaultCanvasBackgroundColor").get_to(defaultCanvasBackgroundColor);} catch(...) {}
try{j.at("flipZoomToolDirection").get_to(flipZoomToolDirection);} catch(...) {} try{j.at("flipZoomToolDirection").get_to(flipZoomToolDirection);} catch(...) {}
try{j.at("realTimeEraser").get_to(realTimeEraser);} catch(...) {} try{j.at("realTimeEraser").get_to(realTimeEraser);} catch(...) {}
try{j.at("imageCompressOnInsert").get_to(imageCompressOnInsert);} catch(...) {}
try{j.at("imageCompressFormat").get_to(imageCompressFormat);} catch(...) {}
try{j.at("imageWebpQuality").get_to(imageWebpQuality);} catch(...) {}
try{j.at("imageFreistellenOnInsert").get_to(imageFreistellenOnInsert);} catch(...) {}
try{j.at("imageFreistellenMode").get_to(imageFreistellenMode);} catch(...) {}
try{j.at("imageWhiteKeyThreshold").get_to(imageWhiteKeyThreshold);} catch(...) {}
#ifndef __EMSCRIPTEN__ #ifndef __EMSCRIPTEN__
try{j.at("checkForUpdates").get_to(checkForUpdates);} catch(...) {} try{j.at("checkForUpdates").get_to(checkForUpdates);} catch(...) {}
#endif #endif

View file

@ -21,6 +21,7 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <Helpers/VersionNumber.hpp> #include <Helpers/VersionNumber.hpp>
#include "InputManager.hpp" #include "InputManager.hpp"
#include "ImagePreprocess.hpp"
#define DEFAULT_CANVAS_BACKGROUND_COLOR Vector3f{0.07f, 0.07f, 0.07f} #define DEFAULT_CANVAS_BACKGROUND_COLOR Vector3f{0.07f, 0.07f, 0.07f}
@ -112,6 +113,14 @@ class GlobalConfig {
std::string displayName; std::string displayName;
bool flipZoomToolDirection = false; bool flipZoomToolDirection = false;
// Fork: image insert options
bool imageCompressOnInsert = true;
ImagePreprocess::CompressFormat imageCompressFormat = ImagePreprocess::CompressFormat::WEBP;
int imageWebpQuality = 80;
bool imageFreistellenOnInsert = false;
ImagePreprocess::FreistellenMode imageFreistellenMode = ImagePreprocess::FreistellenMode::WHITE_KEY;
int imageWhiteKeyThreshold = 245;
bool disableGraphicsDriverWorkarounds = false; bool disableGraphicsDriverWorkarounds = false;
int vsyncValue = 0; int vsyncValue = 0;
@ -135,3 +144,16 @@ NLOHMANN_JSON_SERIALIZE_ENUM(GlobalConfig::AntiAliasing, {
{GlobalConfig::AntiAliasing::SKIA, "Skia"}, {GlobalConfig::AntiAliasing::SKIA, "Skia"},
{GlobalConfig::AntiAliasing::DYNAMIC_MSAA, "Dynamic MSAA"}, {GlobalConfig::AntiAliasing::DYNAMIC_MSAA, "Dynamic MSAA"},
}) })
NLOHMANN_JSON_SERIALIZE_ENUM(ImagePreprocess::CompressFormat, {
{ImagePreprocess::CompressFormat::KEEP, "Keep"},
{ImagePreprocess::CompressFormat::WEBP, "WebP"},
{ImagePreprocess::CompressFormat::PNG, "PNG"},
})
NLOHMANN_JSON_SERIALIZE_ENUM(ImagePreprocess::FreistellenMode, {
{ImagePreprocess::FreistellenMode::OFF, "Off"},
{ImagePreprocess::FreistellenMode::WHITE_KEY, "WhiteKey"},
{ImagePreprocess::FreistellenMode::REMBG_SCHNELL, "RembgSchnell"},
{ImagePreprocess::FreistellenMode::REMBG_ALLGEMEIN, "RembgAllgemein"},
})

315
src/ImagePreprocess.cpp Normal file
View file

@ -0,0 +1,315 @@
/*
* InfiniPaint (fork)
* Image insert preprocessing: freistellen + compression options.
*/
#include "ImagePreprocess.hpp"
#include "GlobalConfig.hpp"
#include <Helpers/Logger.hpp>
#include <include/codec/SkBmpDecoder.h>
#include <include/codec/SkCodec.h>
#include <include/codec/SkGifDecoder.h>
#include <include/codec/SkIcoDecoder.h>
#include <include/codec/SkJpegDecoder.h>
#include <include/codec/SkPngDecoder.h>
#include <include/codec/SkWbmpDecoder.h>
#include <include/codec/SkWebpDecoder.h>
#include <include/core/SkBitmap.h>
#include <include/core/SkData.h>
#include <include/core/SkPixmap.h>
#include <include/core/SkStream.h>
#include <include/encode/SkPngEncoder.h>
#include <include/encode/SkWebpEncoder.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
#include <random>
#include <vector>
#ifndef __EMSCRIPTEN__
#include <cstdlib>
#include <array>
#endif
namespace ImagePreprocess {
namespace {
std::array<const SkCodecs::Decoder, 7> imageDecoders = {
SkBmpDecoder::Decoder(),
SkGifDecoder::Decoder(),
SkIcoDecoder::Decoder(),
SkJpegDecoder::Decoder(),
SkPngDecoder::Decoder(),
SkWbmpDecoder::Decoder(),
SkWebpDecoder::Decoder()
};
std::string to_lower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
std::string replace_extension(std::string name, const char* newExt) {
auto slash = name.find_last_of("/\\");
auto baseStart = slash == std::string::npos ? 0 : slash + 1;
auto dot = name.find_last_of('.');
if(dot != std::string::npos && dot > baseStart)
name = name.substr(0, dot);
name += newExt;
return name;
}
bool decode_first_frame(const std::string& data, SkBitmap& outBitmap) {
sk_sp<SkData> skData = SkData::MakeWithoutCopy(data.data(), data.size());
std::unique_ptr<SkCodec> codec = SkCodec::MakeFromData(skData, imageDecoders);
if(!codec)
return false;
SkImageInfo info = codec->getInfo().makeColorType(kRGBA_8888_SkColorType).makeAlphaType(kUnpremul_SkAlphaType);
if(!outBitmap.tryAllocPixels(info))
return false;
SkCodec::Options opts;
opts.fFrameIndex = 0;
if(codec->getPixels(info, outBitmap.getPixels(), outBitmap.rowBytes(), &opts) != SkCodec::Result::kSuccess)
return false;
return true;
}
void apply_white_key(SkBitmap& bitmap, int threshold) {
threshold = std::clamp(threshold, 0, 255);
SkPixmap pm;
if(!bitmap.peekPixels(&pm))
return;
const int w = pm.width();
const int h = pm.height();
for(int y = 0; y < h; y++) {
auto* row = reinterpret_cast<uint8_t*>(pm.writable_addr(0, y));
for(int x = 0; x < w; x++) {
uint8_t* p = row + x * 4;
if(p[0] >= threshold && p[1] >= threshold && p[2] >= threshold) {
p[3] = 0;
}
else if(p[0] > threshold - 25 && p[1] > threshold - 25 && p[2] > threshold - 25) {
// Soft edge: fade alpha near threshold
int minC = std::min({p[0], p[1], p[2]});
int fade = threshold - minC;
int alpha = (fade * 255) / 25;
p[3] = static_cast<uint8_t>(std::clamp(std::min<int>(p[3], alpha), 0, 255));
}
}
}
}
std::optional<std::string> encode_bitmap(const SkBitmap& bitmap, CompressFormat format, int webpQuality) {
SkPixmap pm;
if(!bitmap.peekPixels(&pm))
return std::nullopt;
SkDynamicMemoryWStream out;
bool ok = false;
if(format == CompressFormat::WEBP) {
SkWebpEncoder::Options opts;
opts.fCompression = SkWebpEncoder::Compression::kLossy;
opts.fQuality = static_cast<float>(std::clamp(webpQuality, 0, 100));
ok = SkWebpEncoder::Encode(&out, pm, opts);
}
else {
ok = SkPngEncoder::Encode(&out, pm, {});
}
if(!ok)
return std::nullopt;
out.flush();
auto skData = out.detachAsData();
if(!skData)
return std::nullopt;
return std::string(reinterpret_cast<const char*>(skData->bytes()), skData->size());
}
#ifndef __EMSCRIPTEN__
std::optional<std::string> find_rembg_bin() {
const char* envBin = std::getenv("TOWER_REMBG_BIN");
if(envBin && envBin[0] && std::filesystem::is_regular_file(envBin))
return std::string(envBin);
const char* home = std::getenv("HOME");
if(home) {
std::filesystem::path venv = std::filesystem::path(home) / ".local/share/tower-rembg/bin/rembg";
if(std::filesystem::is_regular_file(venv))
return venv.string();
std::filesystem::path local = std::filesystem::path(home) / ".local/bin/rembg";
if(std::filesystem::is_regular_file(local))
return local.string();
}
// PATH lookup via `command -v`
FILE* pipe = popen("command -v rembg 2>/dev/null", "r");
if(!pipe)
return std::nullopt;
char buf[512];
std::string path;
while(fgets(buf, sizeof(buf), pipe))
path += buf;
pclose(pipe);
while(!path.empty() && (path.back() == '\n' || path.back() == '\r'))
path.pop_back();
if(!path.empty() && std::filesystem::is_regular_file(path))
return path;
return std::nullopt;
}
std::optional<std::string> run_rembg(const std::string& inputData, const std::string& model) {
auto rembg = find_rembg_bin();
if(!rembg) {
Logger::get().log(Logger::LogType::INFO, "[ImagePreprocess] rembg not found — falling back to white-key");
return std::nullopt;
}
std::error_code ec;
auto tmpDir = std::filesystem::temp_directory_path(ec);
if(ec)
return std::nullopt;
auto inPath = tmpDir / ("infinipaint-rembg-in-" + std::to_string(std::random_device{}()) + ".png");
auto outPath = tmpDir / ("infinipaint-rembg-out-" + std::to_string(std::random_device{}()) + ".png");
{
std::ofstream out(inPath, std::ios::binary);
if(!out)
return std::nullopt;
out.write(inputData.data(), static_cast<std::streamsize>(inputData.size()));
}
std::string cmd = "\"" + *rembg + "\" i -m " + model + " \"" + inPath.string() + "\" \"" + outPath.string() + "\" 2>/dev/null";
int rc = std::system(cmd.c_str());
std::optional<std::string> result;
if(rc == 0 && std::filesystem::is_regular_file(outPath)) {
std::ifstream in(outPath, std::ios::binary);
if(in) {
result = std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
if(result->empty())
result.reset();
}
}
std::filesystem::remove(inPath, ec);
std::filesystem::remove(outPath, ec);
return result;
}
#endif
} // namespace
bool looks_like_raster_image(std::string_view name, std::string_view data) {
std::string lower = to_lower(std::string(name));
auto hasExt = [&](const char* ext) {
return lower.size() >= std::strlen(ext) && lower.compare(lower.size() - std::strlen(ext), std::strlen(ext), ext) == 0;
};
if(hasExt(".png") || hasExt(".jpg") || hasExt(".jpeg") || hasExt(".webp") || hasExt(".bmp") || hasExt(".gif") || hasExt(".ico"))
return true;
if(data.size() >= 8) {
// PNG / JPEG / RIFF WEBP / GIF / BMP magic
const auto* b = reinterpret_cast<const unsigned char*>(data.data());
if(b[0] == 0x89 && b[1] == 'P' && b[2] == 'N' && b[3] == 'G')
return true;
if(b[0] == 0xFF && b[1] == 0xD8)
return true;
if(b[0] == 'R' && b[1] == 'I' && b[2] == 'F' && b[3] == 'F')
return true;
if(b[0] == 'G' && b[1] == 'I' && b[2] == 'F')
return true;
if(b[0] == 'B' && b[1] == 'M')
return true;
}
return false;
}
Options options_from_config(const GlobalConfig& conf) {
Options o;
o.compressOnInsert = conf.imageCompressOnInsert;
o.compressFormat = conf.imageCompressFormat;
o.webpQuality = conf.imageWebpQuality;
o.freistellenOnInsert = conf.imageFreistellenOnInsert;
o.freistellenMode = conf.imageFreistellenMode;
o.whiteKeyThreshold = conf.imageWhiteKeyThreshold;
return o;
}
bool preprocess_resource(std::string& name, std::shared_ptr<std::string>& data, const Options& options) {
if(!data || data->empty())
return false;
if(!looks_like_raster_image(name, *data))
return false;
bool wantFreistellen = options.freistellenOnInsert && options.freistellenMode != FreistellenMode::OFF;
bool wantCompress = options.compressOnInsert && options.compressFormat != CompressFormat::KEEP;
if(!wantFreistellen && !wantCompress)
return false;
std::string working = *data;
bool changed = false;
if(wantFreistellen) {
#ifndef __EMSCRIPTEN__
if(options.freistellenMode == FreistellenMode::REMBG_SCHNELL || options.freistellenMode == FreistellenMode::REMBG_ALLGEMEIN) {
const char* model = options.freistellenMode == FreistellenMode::REMBG_SCHNELL ? "silueta" : "bria-rmbg";
// rembg wants an image file; feed current bytes (encode as PNG first if needed)
SkBitmap bmp;
std::string rembgInput = working;
if(decode_first_frame(working, bmp)) {
auto png = encode_bitmap(bmp, CompressFormat::PNG, 100);
if(png)
rembgInput = *png;
}
if(auto rembgOut = run_rembg(rembgInput, model)) {
working = *rembgOut;
changed = true;
name = replace_extension(name, ".png");
}
else {
// Fallback white-key
if(decode_first_frame(working, bmp)) {
apply_white_key(bmp, options.whiteKeyThreshold);
if(auto enc = encode_bitmap(bmp, CompressFormat::PNG, 100)) {
working = *enc;
changed = true;
name = replace_extension(name, ".png");
}
}
}
}
else
#endif
{
SkBitmap bmp;
if(decode_first_frame(working, bmp)) {
apply_white_key(bmp, options.whiteKeyThreshold);
if(auto enc = encode_bitmap(bmp, CompressFormat::PNG, 100)) {
working = *enc;
changed = true;
name = replace_extension(name, ".png");
}
}
}
}
if(wantCompress) {
SkBitmap bmp;
if(decode_first_frame(working, bmp)) {
CompressFormat fmt = options.compressFormat;
if(auto enc = encode_bitmap(bmp, fmt, options.webpQuality)) {
// Keep result if smaller, or if freistellen required alpha-friendly format
bool preferNew = enc->size() < working.size() || wantFreistellen || fmt == CompressFormat::WEBP;
if(preferNew) {
working = *enc;
changed = true;
name = replace_extension(name, fmt == CompressFormat::WEBP ? ".webp" : ".png");
}
}
}
}
if(changed)
data = std::make_shared<std::string>(std::move(working));
return changed;
}
}

45
src/ImagePreprocess.hpp Normal file
View file

@ -0,0 +1,45 @@
/*
* InfiniPaint (fork)
* Image insert preprocessing: freistellen + compression options.
*/
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
class GlobalConfig;
namespace ImagePreprocess {
enum class CompressFormat : uint8_t {
KEEP = 0,
WEBP,
PNG
};
enum class FreistellenMode : uint8_t {
OFF = 0,
WHITE_KEY, // Near-white → transparent (always available)
REMBG_SCHNELL, // rembg -m silueta (Tower freistellen/schnell)
REMBG_ALLGEMEIN // rembg -m bria-rmbg (Tower freistellen/allgemein)
};
struct Options {
bool compressOnInsert = true;
CompressFormat compressFormat = CompressFormat::WEBP;
int webpQuality = 80; // 0–100, lossy
bool freistellenOnInsert = false;
FreistellenMode freistellenMode = FreistellenMode::WHITE_KEY;
int whiteKeyThreshold = 245; // 0–255, RGB channels all >= threshold → transparent
};
// Mutates name + data in place when preprocessing applies. Returns true if changed.
bool preprocess_resource(std::string& name, std::shared_ptr<std::string>& data, const Options& options);
Options options_from_config(const GlobalConfig& conf);
bool looks_like_raster_image(std::string_view name, std::string_view data);
}

View file

@ -346,6 +346,14 @@ void PhoneDrawingProgramScreen::top_toolbar_settings_popup() {
.floating = {.offset = {.x = 0, .y = static_cast<float>(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} .floating = {.offset = {.x = 0, .y = static_cast<float>(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT}
}) { }) {
text_label_centered(gui, "Player List"); text_label_centered(gui, "Player List");
if(main.world->netServer) {
text_button(gui, "force all jump to host phone", "Bring Everyone Here", {
.wide = true,
.onClick = [&] {
main.world->drawProg.force_clients_to_own_view();
}
});
}
left_to_right_line_layout(gui, [&]() { left_to_right_line_layout(gui, [&]() {
CLAY_AUTO_ID({ CLAY_AUTO_ID({
.layout = { .layout = {

View file

@ -1211,6 +1211,14 @@ void Toolbar::player_list() {
gui.new_id("client list", [&] { gui.new_id("client list", [&] {
text_label_centered(gui, "Player List"); text_label_centered(gui, "Player List");
if(!main.world->clientStillConnecting) { if(!main.world->clientStillConnecting) {
if(main.world->netServer) {
text_button(gui, "force all jump to host", "Bring Everyone Here", {
.wide = true,
.onClick = [&] {
main.world->drawProg.force_clients_to_own_view();
}
});
}
left_to_right_line_layout(gui, [&]() { left_to_right_line_layout(gui, [&]() {
CLAY_AUTO_ID({ CLAY_AUTO_ID({
.layout = { .layout = {
@ -1463,6 +1471,7 @@ void Toolbar::general_settings_inner_gui() {
}); });
}; };
category_button("Generalbutton", "General", GSETTINGS_GENERAL); category_button("Generalbutton", "General", GSETTINGS_GENERAL);
category_button("Imagesbutton", "Images", GSETTINGS_IMAGES);
category_button("Graphicsbutton", "Graphics", GSETTINGS_GRAPHICS); category_button("Graphicsbutton", "Graphics", GSETTINGS_GRAPHICS);
category_button("Tabletbutton", "Tablet", GSETTINGS_TABLET); category_button("Tabletbutton", "Tablet", GSETTINGS_TABLET);
category_button("Themebutton", "Theme", GSETTINGS_THEME); category_button("Themebutton", "Theme", GSETTINGS_THEME);
@ -1524,6 +1533,35 @@ void Toolbar::general_settings_inner_gui() {
}); });
break; break;
} }
case GSETTINGS_IMAGES: {
general_scroll_area("image settings", [&] {
text_label(gui, "On insert / paste:");
checkbox_boolean_field(gui, "compress images on insert", "Compress images automatically", &main.conf.imageCompressOnInsert);
text_label(gui, "Compression format:");
radio_button_selector(gui, "image compress format", &main.conf.imageCompressFormat, {
{"Keep original", ImagePreprocess::CompressFormat::KEEP},
{"WebP", ImagePreprocess::CompressFormat::WEBP},
{"PNG", ImagePreprocess::CompressFormat::PNG}
});
slider_scalar_field(gui, "webp quality", "WebP quality", &main.conf.imageWebpQuality, 1, 100);
checkbox_boolean_field(gui, "freistellen on insert", "Freistellen / remove background on insert", &main.conf.imageFreistellenOnInsert);
text_label(gui, "Freistellen mode:");
radio_button_selector(gui, "freistellen mode", &main.conf.imageFreistellenMode, {
{"Off", ImagePreprocess::FreistellenMode::OFF},
{"White background", ImagePreprocess::FreistellenMode::WHITE_KEY},
#ifndef __EMSCRIPTEN__
{"Schnell (rembg silueta)", ImagePreprocess::FreistellenMode::REMBG_SCHNELL},
{"Allgemein (rembg bria)", ImagePreprocess::FreistellenMode::REMBG_ALLGEMEIN},
#endif
});
slider_scalar_field(gui, "white key threshold", "White-key threshold", &main.conf.imageWhiteKeyThreshold, 200, 255);
text_label_light(gui, "Selection toolbar: Compress Images / Freistellen also work afterwards.");
#ifndef __EMSCRIPTEN__
text_label_light(gui, "rembg uses Tower venv (~/.local/share/tower-rembg) when present.");
#endif
});
break;
}
case GSETTINGS_GRAPHICS: { case GSETTINGS_GRAPHICS: {
general_scroll_area("graphics settings", [&] { general_scroll_area("graphics settings", [&] {
input_scalar_field(gui, "Max GUI Scale", "Max GUI Scale", &main.conf.guiScale, 0.5f, 5.0f, { input_scalar_field(gui, "Max GUI Scale", "Max GUI Scale", &main.conf.guiScale, 0.5f, 5.0f, {

View file

@ -169,6 +169,7 @@ class Toolbar {
} optionsMenuType; } optionsMenuType;
enum GeneralSettingsOptions { enum GeneralSettingsOptions {
GSETTINGS_GENERAL = 0, GSETTINGS_GENERAL = 0,
GSETTINGS_IMAGES,
GSETTINGS_GRAPHICS, GSETTINGS_GRAPHICS,
GSETTINGS_TABLET, GSETTINGS_TABLET,
GSETTINGS_THEME, GSETTINGS_THEME,

View file

@ -30,6 +30,7 @@ VersionNumber header_to_version_number(const std::string& header) {
m["INFPNT000004"] = VersionNumber(0, 3, 0); m["INFPNT000004"] = VersionNumber(0, 3, 0);
m["INFPNT000005"] = VersionNumber(0, 4, 0); m["INFPNT000005"] = VersionNumber(0, 4, 0);
m["INFPNT000006"] = VersionNumber(0, 6, 0); m["INFPNT000006"] = VersionNumber(0, 6, 0);
m["INFPNT000007"] = VersionNumber(0, 7, 0);
} }
auto it = m.find(header); auto it = m.find(header);
if(it == m.end()) if(it == m.end())

View file

@ -28,7 +28,7 @@ namespace VersionConstants {
VersionNumber header_to_version_number(const std::string& header); VersionNumber header_to_version_number(const std::string& header);
constexpr int SAVEFILE_HEADER_LEN = 12; // DO NOT CHANGE THIS HEADER LENGTH constexpr int SAVEFILE_HEADER_LEN = 12; // DO NOT CHANGE THIS HEADER LENGTH
const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000006"; // Change whenever the save file is incompatible with the previous version const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000007"; // Change whenever the save file is incompatible with the previous version
const std::string CURRENT_VERSION_STRING = "0.6.1"; const std::string CURRENT_VERSION_STRING = "0.7.0-fork";
constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 6, 1); constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 7, 0);
} }