Ship 0.8 fog/freistellen and fix Windows CI for friends
Some checks are pending
Windows Release / windows-x86_64 (push) Waiting to run
Some checks are pending
Windows Release / windows-x86_64 (push) Waiting to run
Publish the portable zip to public Forgejo from the private GitHub builder, and force Ninja so MSVC 195 deps build without CMake 4.2.
This commit is contained in:
parent
97b136de3b
commit
1c1d63f376
46 changed files with 1748 additions and 156 deletions
|
|
@ -51,6 +51,7 @@
|
|||
#include "../ImagePreprocess.hpp"
|
||||
#include <Helpers/Random.hpp>
|
||||
#include <cereal/types/utility.hpp>
|
||||
#include <unordered_map>
|
||||
#ifdef __ANDROID__
|
||||
#include "../AndroidJNICalls.hpp"
|
||||
#endif
|
||||
|
|
@ -112,6 +113,8 @@ void DrawingProgram::input_drop_text_callback(const InputManager::DropCallbackAr
|
|||
img.d.p2 = drop.pos + imDim;
|
||||
img.d.imageID = {0, 0};
|
||||
auto newObjInfo = layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!newObjInfo)
|
||||
return;
|
||||
layerMan.add_undo_place_component(newObjInfo);
|
||||
droppedDownloadingFiles.emplace_back(newObjInfo, world.main.window.size.cast<float>(), FileDownloader::download_data_from_url(drop.data));
|
||||
}
|
||||
|
|
@ -338,9 +341,29 @@ void DrawingProgram::init_server_callbacks() {
|
|||
for(auto& [netID, coord] : transforms)
|
||||
coord.scale_about(WorldVec{0, 0}, scaleUpAmount, true);
|
||||
}
|
||||
|
||||
process_transform_message(transforms);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, transforms);
|
||||
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>> allowed;
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>> rejectedRestore;
|
||||
allowed.reserve(transforms.size());
|
||||
for(auto& [netID, coord] : transforms) {
|
||||
auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer>(netID);
|
||||
if(!objPtr)
|
||||
continue;
|
||||
if(!layerMan.client_may_modify_component(objPtr.get())) {
|
||||
rejectedRestore.emplace_back(netID, objPtr->coords);
|
||||
continue;
|
||||
}
|
||||
allowed.emplace_back(netID, coord);
|
||||
}
|
||||
if(!allowed.empty()) {
|
||||
process_transform_message(allowed);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, allowed);
|
||||
// Moving content or fog changes coverage — unload/reload on clients.
|
||||
layerMan.refresh_fog_client_projection();
|
||||
}
|
||||
// Snap the offending client (and peers) back if they tried to move fog/locked objects.
|
||||
if(!rejectedRestore.empty())
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, rejectedRestore);
|
||||
});
|
||||
world.netServer->add_recv_callback(SERVER_FORCE_CAMERA_JUMP, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
CoordSpaceHelper coords;
|
||||
|
|
@ -355,8 +378,18 @@ void DrawingProgram::init_server_callbacks() {
|
|||
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);
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> allowed;
|
||||
allowed.reserve(groupIds.size());
|
||||
for(auto& [netID, groupId] : groupIds) {
|
||||
auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer>(netID);
|
||||
if(!objPtr || !layerMan.client_may_modify_component(objPtr.get()))
|
||||
continue;
|
||||
allowed.emplace_back(netID, groupId);
|
||||
}
|
||||
if(allowed.empty())
|
||||
return;
|
||||
process_group_id_message(allowed);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_SET_COMPONENT_GROUP_IDS, allowed);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -430,10 +463,20 @@ void DrawingProgram::process_transform_message(const std::vector<std::pair<Netwo
|
|||
void DrawingProgram::send_transforms_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objsToSendTransformsFor) {
|
||||
if(world.netObjMan.is_connected()) {
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>> transforms;
|
||||
for(auto& obj : objsToSendTransformsFor)
|
||||
for(auto& obj : objsToSendTransformsFor) {
|
||||
if(!obj || !obj->obj)
|
||||
continue;
|
||||
if(!world.netObjMan.is_server() && !layerMan.client_may_modify_component(obj->obj.get()))
|
||||
continue;
|
||||
transforms.emplace_back(obj->obj.get_net_id(), obj->obj->coords);
|
||||
if(world.netObjMan.is_server())
|
||||
}
|
||||
if(transforms.empty())
|
||||
return;
|
||||
if(world.netObjMan.is_server()) {
|
||||
// Fog coords first, then coverage erase/reveal (same reliable channel order).
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, transforms);
|
||||
layerMan.refresh_fog_client_projection();
|
||||
}
|
||||
else
|
||||
world.netClient->send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_TRANSFORM_MANY_COMPONENTS, transforms);
|
||||
}
|
||||
|
|
@ -497,8 +540,12 @@ void DrawingProgram::toolbar_gui(Toolbar& t) {
|
|||
}
|
||||
|
||||
void DrawingProgram::right_click_popup_gui(Toolbar& t) {
|
||||
if(rightClickPopupLocation.has_value())
|
||||
drawTool->right_click_popup_gui(t, rightClickPopupLocation.value());
|
||||
if(rightClickPopupLocation.has_value()) {
|
||||
if(showingImagePasteChooser)
|
||||
image_paste_choice_menu(rightClickPopupLocation.value());
|
||||
else
|
||||
drawTool->right_click_popup_gui(t, rightClickPopupLocation.value());
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgram::set_right_click_popup_location(const Vector2f& newLoc) {
|
||||
|
|
@ -509,8 +556,9 @@ void DrawingProgram::set_right_click_popup_location(const Vector2f& newLoc) {
|
|||
}
|
||||
|
||||
void DrawingProgram::clear_right_click_popup() {
|
||||
if(rightClickPopupLocation.has_value()) {
|
||||
if(rightClickPopupLocation.has_value() || showingImagePasteChooser) {
|
||||
rightClickPopupLocation = std::nullopt;
|
||||
showingImagePasteChooser = false;
|
||||
world.main.g.gui.set_to_layout();
|
||||
}
|
||||
}
|
||||
|
|
@ -550,11 +598,43 @@ void DrawingProgram::selection_action_menu(Vector2f popupPos) {
|
|||
});
|
||||
popup_menu_action_button("Paste Image", "Paste Image", [&, popupPos] {
|
||||
selection.deselect_all();
|
||||
world.main.input.call_paste(CustomEvents::PasteEvent::DataType::IMAGE, {
|
||||
.pastePosition = popupPos * world.main.g.final_gui_scale()
|
||||
});
|
||||
request_clipboard_image_paste(popupPos * world.main.g.final_gui_scale(), false);
|
||||
});
|
||||
popup_menu_action_button("Paste Image Freistellen", "Paste Image + Freistellen", [&, popupPos] {
|
||||
selection.deselect_all();
|
||||
request_clipboard_image_paste(popupPos * world.main.g.final_gui_scale(), true);
|
||||
});
|
||||
CanvasComponentContainer::ObjInfo* imageUnderCursor = nullptr;
|
||||
{
|
||||
Vector2f screenPos = popupPos * world.main.g.final_gui_scale();
|
||||
SkPath clickPath = SkPath::Rect(SkRect::MakeLTRB(screenPos.x() - 0.5f, screenPos.y() - 0.5f, screenPos.x() + 0.5f, screenPos.y() + 0.5f));
|
||||
auto* hit = selection.get_front_object_colliding_with_in_editing_layer(clickPath);
|
||||
if(hit && hit->obj && hit->obj->get_comp().get_type() == CanvasComponentType::IMAGE)
|
||||
imageUnderCursor = hit;
|
||||
}
|
||||
if(imageUnderCursor && !selection.is_something_selected()) {
|
||||
popup_menu_action_button("Freistellen", "Freistellen", [&, imageUnderCursor] {
|
||||
process_images_on({imageUnderCursor}, true, true);
|
||||
});
|
||||
}
|
||||
if(selection.is_something_selected()) {
|
||||
bool hasImage = false;
|
||||
for(auto* c : selection.get_selection_as_set()) {
|
||||
if(c && c->obj && c->obj->get_comp().get_type() == CanvasComponentType::IMAGE) {
|
||||
hasImage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(hasImage) {
|
||||
popup_menu_action_button("Freistellen", "Freistellen", [&] {
|
||||
process_selected_images(true, true);
|
||||
});
|
||||
}
|
||||
else if(imageUnderCursor) {
|
||||
popup_menu_action_button("Freistellen", "Freistellen", [&, imageUnderCursor] {
|
||||
process_images_on({imageUnderCursor}, true, true);
|
||||
});
|
||||
}
|
||||
popup_menu_action_button("Copy", "Copy", [&] {
|
||||
selection.selection_to_clipboard();
|
||||
});
|
||||
|
|
@ -575,6 +655,31 @@ void DrawingProgram::selection_action_menu(Vector2f popupPos) {
|
|||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::request_clipboard_image_paste(Vector2f screenPos, bool applyFreistellen) {
|
||||
world.main.input.call_paste(CustomEvents::PasteEvent::DataType::IMAGE, {
|
||||
.pastePosition = screenPos,
|
||||
.applyFreistellen = applyFreistellen
|
||||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::image_paste_choice_menu(Vector2f popupPos) {
|
||||
using namespace GUIStuff;
|
||||
using namespace ElementHelpers;
|
||||
|
||||
GUIStuff::GUIManager& gui = world.main.g.gui;
|
||||
right_click_action_menu(popupPos, [&] {
|
||||
text_label_light(gui, "Paste image");
|
||||
popup_menu_action_button("Paste Image as-is", "Paste as-is", [&, popupPos] {
|
||||
selection.deselect_all();
|
||||
request_clipboard_image_paste(popupPos * world.main.g.final_gui_scale(), false);
|
||||
});
|
||||
popup_menu_action_button("Paste Image Freistellen", "Freistellen then paste", [&, popupPos] {
|
||||
selection.deselect_all();
|
||||
request_clipboard_image_paste(popupPos * world.main.g.final_gui_scale(), true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::right_click_action_menu(Vector2f popupPos, const std::function<void()>& innerContent) {
|
||||
using namespace GUIStuff;
|
||||
using namespace ElementHelpers;
|
||||
|
|
@ -642,6 +747,9 @@ void DrawingProgram::update() {
|
|||
update_downloading_dropped_files();
|
||||
check_updateable_components();
|
||||
|
||||
// After tools/transforms this frame: hide/reveal client content under moved fog.
|
||||
layerMan.flush_fog_client_projection_if_dirty();
|
||||
|
||||
if(drawCache.check_rebuild_needed_from_framerate() || drawCache.should_rebuild())
|
||||
rebuild_cache();
|
||||
}
|
||||
|
|
@ -791,7 +899,7 @@ 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, bool applyFreistellen) {
|
||||
if(layerMan.is_a_layer_being_edited()) {
|
||||
ResourceData resource;
|
||||
#ifdef __ANDROID__
|
||||
|
|
@ -808,7 +916,7 @@ void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& fil
|
|||
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));
|
||||
ImagePreprocess::preprocess_resource(resource.name, resource.data, ImagePreprocess::options_for_place(world.main.conf, applyFreistellen));
|
||||
NetworkingObjects::NetObjTemporaryPtr<ResourceData> imageTempPtr = world.rMan.add_resource(resource);
|
||||
if(imageTempPtr) {
|
||||
NetworkingObjects::NetObjID imageID = imageTempPtr.get_net_id();
|
||||
|
|
@ -826,17 +934,18 @@ void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& fil
|
|||
img.d.p2 = dropPos + imDim;
|
||||
img.d.imageID = imageID;
|
||||
auto newObjInfo = layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
layerMan.add_undo_place_component(newObjInfo);
|
||||
if(newObjInfo)
|
||||
layerMan.add_undo_place_component(newObjInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos) {
|
||||
CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos, bool applyFreistellen) {
|
||||
if(layerMan.is_a_layer_being_edited()) {
|
||||
ResourceData newResource;
|
||||
newResource.data = std::make_shared<std::string>(fileBuffer);
|
||||
newResource.name = fileName;
|
||||
ImagePreprocess::preprocess_resource(newResource.name, newResource.data, ImagePreprocess::options_from_config(world.main.conf));
|
||||
ImagePreprocess::preprocess_resource(newResource.name, newResource.data, ImagePreprocess::options_for_place(world.main.conf, applyFreistellen));
|
||||
NetworkingObjects::NetObjID imageID = world.rMan.add_resource(newResource).get_net_id();
|
||||
ResourceDisplay* display = world.rMan.get_display_data(imageID);
|
||||
Vector2f imTrueDim = display->get_dimensions();
|
||||
|
|
@ -850,6 +959,8 @@ CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(co
|
|||
img.d.p2 = dropPos + imDim;
|
||||
img.d.imageID = imageID;
|
||||
CanvasComponentContainer::ObjInfo* newObjInfo = layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!newObjInfo)
|
||||
return nullptr;
|
||||
layerMan.add_undo_place_component(newObjInfo);
|
||||
return newObjInfo;
|
||||
}
|
||||
|
|
@ -858,28 +969,183 @@ CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(co
|
|||
|
||||
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;
|
||||
std::vector<CanvasComponentContainer::ObjInfo*> objs(selected.begin(), selected.end());
|
||||
process_images_on(objs, freistellen, compress);
|
||||
}
|
||||
|
||||
void DrawingProgram::process_all_canvas_images(bool freistellen, bool compress) {
|
||||
process_images_on(layerMan.get_flattened_component_list(), freistellen, compress);
|
||||
}
|
||||
|
||||
void DrawingProgram::process_images_on(const std::vector<CanvasComponentContainer::ObjInfo*>& objs, bool freistellen, bool compress) {
|
||||
ImagePreprocess::Options opts = ImagePreprocess::options_for_place(world.main.conf, freistellen);
|
||||
opts.compressOnInsert = compress;
|
||||
if(compress && opts.compressFormat == ImagePreprocess::CompressFormat::KEEP)
|
||||
opts.compressFormat = ImagePreprocess::CompressFormat::WEBP;
|
||||
if(!freistellen && !compress)
|
||||
return;
|
||||
for(auto* objInfo : selected) {
|
||||
|
||||
std::unordered_map<NetworkingObjects::NetObjID, NetworkingObjects::NetObjID> remapped;
|
||||
int changedUnique = 0;
|
||||
int changedInstances = 0;
|
||||
|
||||
for(auto* objInfo : objs) {
|
||||
if(!objInfo || !objInfo->obj)
|
||||
continue;
|
||||
if(objInfo->obj->get_comp().get_type() != CanvasComponentType::IMAGE)
|
||||
continue;
|
||||
auto& img = static_cast<ImageCanvasComponent&>(objInfo->obj->get_comp());
|
||||
auto oldId = img.d.imageID;
|
||||
auto remappedIt = remapped.find(oldId);
|
||||
if(remappedIt != remapped.end()) {
|
||||
if(remappedIt->second != oldId) {
|
||||
img.d.imageID = remappedIt->second;
|
||||
objInfo->obj->send_comp_update(*this, true);
|
||||
objInfo->obj->commit_update(*this);
|
||||
changedInstances++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
auto resource = world.netObjMan.get_obj_temporary_ref_from_id<ResourceData>(oldId);
|
||||
if(!resource || !resource->data) {
|
||||
remapped[oldId] = oldId;
|
||||
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)) {
|
||||
remapped[oldId] = oldId;
|
||||
continue;
|
||||
}
|
||||
auto newId = world.rMan.add_resource(newRes).get_net_id();
|
||||
remapped[oldId] = newId;
|
||||
if(newId == oldId)
|
||||
continue;
|
||||
img.d.imageID = newId;
|
||||
objInfo->obj->send_comp_update(*this, true);
|
||||
objInfo->obj->commit_update(*this);
|
||||
changedUnique++;
|
||||
changedInstances++;
|
||||
}
|
||||
|
||||
if(changedUnique > 0)
|
||||
world.rMan.clear_display_cache();
|
||||
Logger::get().log(Logger::LogType::INFO,
|
||||
"[DrawingProgram] Image process: " + std::to_string(changedUnique) + " unique resources, " +
|
||||
std::to_string(changedInstances) + " placed images");
|
||||
}
|
||||
|
||||
DrawingProgram::ImageSizeSummaryLines DrawingProgram::canvas_image_size_summary() {
|
||||
return image_size_summary_for(layerMan.get_flattened_component_list(), canvasImageSizeCache,
|
||||
"No raster images on the canvas.",
|
||||
"On canvas now",
|
||||
"After Compress all");
|
||||
}
|
||||
|
||||
DrawingProgram::ImageSizeSummaryLines DrawingProgram::selected_image_size_summary() {
|
||||
auto selected = selection.get_selection_as_set();
|
||||
std::vector<CanvasComponentContainer::ObjInfo*> objs(selected.begin(), selected.end());
|
||||
return image_size_summary_for(objs, selectedImageSizeCache,
|
||||
"No raster images in the selection.",
|
||||
"Selected now",
|
||||
"After Compress Images");
|
||||
}
|
||||
|
||||
DrawingProgram::ImageSizeSummaryLines DrawingProgram::image_size_summary_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs, ImageSizeEstimateCache& cache, const char* emptyMsg, const char* nowPrefix, const char* afterPrefix) {
|
||||
using namespace ImagePreprocess;
|
||||
CompressFormat fmt = world.main.conf.imageCompressFormat;
|
||||
int quality = world.main.conf.imageWebpQuality;
|
||||
if(fmt == CompressFormat::KEEP)
|
||||
fmt = CompressFormat::WEBP;
|
||||
|
||||
struct UniqueRaster {
|
||||
NetworkingObjects::NetObjID id;
|
||||
std::string_view name;
|
||||
const std::string* data = nullptr;
|
||||
};
|
||||
std::vector<UniqueRaster> unique;
|
||||
unique.reserve(objs.size());
|
||||
std::unordered_set<NetworkingObjects::NetObjID> seen;
|
||||
int placed = 0;
|
||||
uint64_t fingerprint = 0;
|
||||
uint64_t currentBytes = 0;
|
||||
|
||||
for(auto* objInfo : objs) {
|
||||
if(!objInfo || !objInfo->obj)
|
||||
continue;
|
||||
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)
|
||||
if(!resource || !resource->data || resource->data->empty())
|
||||
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))
|
||||
if(!looks_like_raster_image(resource->name, *resource->data))
|
||||
continue;
|
||||
img.d.imageID = world.rMan.add_resource(newRes).get_net_id();
|
||||
objInfo->obj->send_comp_update(*this, true);
|
||||
objInfo->obj->commit_update(*this);
|
||||
placed++;
|
||||
if(!seen.insert(img.d.imageID).second)
|
||||
continue;
|
||||
UniqueRaster u;
|
||||
u.id = img.d.imageID;
|
||||
u.name = resource->name;
|
||||
u.data = resource->data.get();
|
||||
unique.push_back(u);
|
||||
currentBytes += static_cast<uint64_t>(u.data->size());
|
||||
hash_combine(fingerprint, u.id.data[0], u.id.data[1], reinterpret_cast<uintptr_t>(u.data), u.data->size());
|
||||
}
|
||||
world.rMan.clear_display_cache();
|
||||
|
||||
uint8_t fmtByte = static_cast<uint8_t>(fmt);
|
||||
bool keyChanged = fingerprint != cache.fingerprint || fmtByte != cache.format || quality != cache.quality;
|
||||
if(keyChanged) {
|
||||
cache.fingerprint = fingerprint;
|
||||
cache.format = fmtByte;
|
||||
cache.quality = quality;
|
||||
cache.placed = placed;
|
||||
cache.unique = static_cast<int>(unique.size());
|
||||
cache.currentBytes = currentBytes;
|
||||
cache.estimatedBytes = currentBytes;
|
||||
cache.estimateReady = unique.empty();
|
||||
cache.dirtySince = std::chrono::steady_clock::now();
|
||||
}
|
||||
else {
|
||||
cache.placed = placed;
|
||||
cache.unique = static_cast<int>(unique.size());
|
||||
cache.currentBytes = currentBytes;
|
||||
}
|
||||
|
||||
if(!cache.estimateReady && !unique.empty()) {
|
||||
auto elapsed = std::chrono::steady_clock::now() - cache.dirtySince;
|
||||
if(elapsed >= std::chrono::milliseconds(250)) {
|
||||
uint64_t estimated = 0;
|
||||
for(const auto& u : unique) {
|
||||
auto est = estimate_compress_size(u.name, *u.data, fmt, quality);
|
||||
if(est)
|
||||
estimated += est->estimatedBytes;
|
||||
else
|
||||
estimated += static_cast<uint64_t>(u.data->size());
|
||||
}
|
||||
cache.estimatedBytes = estimated;
|
||||
cache.estimateReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImageSizeSummaryLines out;
|
||||
if(cache.unique == 0) {
|
||||
out.nowLine = emptyMsg;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string fmtName = fmt == CompressFormat::PNG ? "PNG" : ("WebP q" + std::to_string(quality));
|
||||
out.nowLine = std::string(nowPrefix) + ": " + std::to_string(cache.unique) + " unique raster" +
|
||||
(cache.unique == 1 ? "" : "s") + " (" + std::to_string(cache.placed) + " placed) — " +
|
||||
format_byte_size(cache.currentBytes);
|
||||
out.afterLine = std::string(afterPrefix) + " (" + fmtName + "): ";
|
||||
if(cache.estimateReady)
|
||||
out.afterLine += format_byte_size(cache.estimatedBytes);
|
||||
else
|
||||
out.afterLine += "estimating…";
|
||||
return out;
|
||||
}
|
||||
|
||||
float DrawingProgram::drag_point_radius() {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
#pragma once
|
||||
#include "../DrawData.hpp"
|
||||
#include <chrono>
|
||||
#include "DrawingProgramCache.hpp"
|
||||
#include "ToolConfiguration.hpp"
|
||||
#include "Tools/GridModifyTool.hpp"
|
||||
|
|
@ -54,8 +55,8 @@ class DrawingProgram {
|
|||
void read_components_client(cereal::PortableBinaryInputArchive& a);
|
||||
void init_server_callbacks();
|
||||
void init_client_callbacks();
|
||||
void add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos);
|
||||
CanvasComponentContainer::ObjInfo* add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos);
|
||||
void add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos, bool applyFreistellen = false);
|
||||
CanvasComponentContainer::ObjInfo* add_file_to_canvas_by_data(const std::string& fileName, std::string_view fileBuffer, Vector2f dropPos, bool applyFreistellen = false);
|
||||
void get_used_resources(std::unordered_set<NetworkingObjects::NetObjID>& resourceSet);
|
||||
|
||||
Vector4f* color_picker_color(Vector4f* oldColor);
|
||||
|
|
@ -82,6 +83,13 @@ class DrawingProgram {
|
|||
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 process_all_canvas_images(bool freistellen, bool compress);
|
||||
struct ImageSizeSummaryLines {
|
||||
std::string nowLine;
|
||||
std::string afterLine;
|
||||
};
|
||||
ImageSizeSummaryLines canvas_image_size_summary();
|
||||
ImageSizeSummaryLines selected_image_size_summary();
|
||||
|
||||
void on_tab_out();
|
||||
void input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile);
|
||||
|
|
@ -110,12 +118,28 @@ class DrawingProgram {
|
|||
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 process_images_on(const std::vector<CanvasComponentContainer::ObjInfo*>& objs, bool freistellen, bool compress);
|
||||
|
||||
struct ImageSizeEstimateCache {
|
||||
uint64_t fingerprint = 0;
|
||||
uint8_t format = 255;
|
||||
int quality = -999;
|
||||
int placed = 0;
|
||||
int unique = 0;
|
||||
uint64_t currentBytes = 0;
|
||||
uint64_t estimatedBytes = 0;
|
||||
bool estimateReady = false;
|
||||
std::chrono::steady_clock::time_point dirtySince{};
|
||||
};
|
||||
ImageSizeSummaryLines image_size_summary_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs, ImageSizeEstimateCache& cache, const char* emptyMsg, const char* nowPrefix, const char* afterPrefix);
|
||||
|
||||
void drag_drop_update();
|
||||
void check_updateable_components();
|
||||
void update_downloading_dropped_files();
|
||||
|
||||
void selection_action_menu(Vector2f popupPos);
|
||||
void image_paste_choice_menu(Vector2f popupPos);
|
||||
void request_clipboard_image_paste(Vector2f screenPos, bool applyFreistellen);
|
||||
void right_click_action_menu(Vector2f popupPos, const std::function<void()>& innerContent);
|
||||
void popup_menu_action_button(const char* id, const char* text, const std::function<void()>& onClick);
|
||||
void rebuild_cache();
|
||||
|
|
@ -127,6 +151,8 @@ class DrawingProgram {
|
|||
bool is_selection_allowing_tool(DrawingProgramToolType typeToCheck);
|
||||
|
||||
DrawingProgramSelection selection;
|
||||
ImageSizeEstimateCache canvasImageSizeCache;
|
||||
ImageSizeEstimateCache selectedImageSizeCache;
|
||||
|
||||
std::unique_ptr<DrawingProgramToolBase> toolToSwitchToAfterUpdate;
|
||||
std::unordered_set<CanvasComponentContainer::ObjInfo*> updateableComponents;
|
||||
|
|
@ -159,6 +185,7 @@ class DrawingProgram {
|
|||
std::vector<DroppedDownloadingFile> droppedDownloadingFiles;
|
||||
|
||||
std::optional<Vector2f> rightClickPopupLocation;
|
||||
bool showingImagePasteChooser = false;
|
||||
|
||||
uint32_t nextID = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
#include "../MainProgram.hpp"
|
||||
#include "Helpers/Parallel.hpp"
|
||||
#include "Layers/DrawingProgramLayerManager.hpp"
|
||||
#include "Layers/SerializedBlendMode.hpp"
|
||||
|
||||
#ifdef USE_SKIA_BACKEND_GRAPHITE
|
||||
#include <include/gpu/graphite/Surface.h>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,12 @@ void DrawingProgramSelection::selection_gui(Toolbar& t) {
|
|||
.onClick = [&] { drawP.process_selected_images(true, true); }
|
||||
});
|
||||
});
|
||||
{
|
||||
auto sizes = drawP.selected_image_size_summary();
|
||||
mutable_text_label_light(gui, "selection image size now", sizes.nowLine);
|
||||
if(!sizes.afterLine.empty())
|
||||
mutable_text_label_light(gui, "selection image size after", sizes.afterLine);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -356,7 +362,8 @@ void DrawingProgramSelection::expand_selection_with_groups() {
|
|||
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)) {
|
||||
if(c->obj->groupId != 0 && groupIds.contains(c->obj->groupId) && !already.contains(c)
|
||||
&& drawP.layerMan.can_current_peer_edit_layer(c->obj->parentLayer)) {
|
||||
selectedSet.push_back(c);
|
||||
already.insert(c);
|
||||
}
|
||||
|
|
@ -381,14 +388,23 @@ void DrawingProgramSelection::ungroup_selection() {
|
|||
}
|
||||
|
||||
void DrawingProgramSelection::add_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) {
|
||||
selectedSet.insert(selectedSet.end(), newSelection.begin(), newSelection.end());
|
||||
for(auto* c : newSelection) {
|
||||
if(c->obj->parentLayer && !drawP.layerMan.can_current_peer_edit_layer(c->obj->parentLayer))
|
||||
continue;
|
||||
selectedSet.emplace_back(c);
|
||||
}
|
||||
expand_selection_with_groups();
|
||||
sort_selection();
|
||||
calculate_aabb();
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::set_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) {
|
||||
selectedSet = newSelection;
|
||||
selectedSet.clear();
|
||||
for(auto* c : newSelection) {
|
||||
if(c->obj->parentLayer && !drawP.layerMan.can_current_peer_edit_layer(c->obj->parentLayer))
|
||||
continue;
|
||||
selectedSet.emplace_back(c);
|
||||
}
|
||||
expand_selection_with_groups();
|
||||
sort_selection();
|
||||
calculate_aabb();
|
||||
|
|
@ -598,11 +614,8 @@ void DrawingProgramSelection::input_key_callback_display_selection(const InputMa
|
|||
}
|
||||
case InputManager::KEY_PASTE_IMAGE: {
|
||||
if(key.down && !key.repeat) {
|
||||
deselect_all();
|
||||
drawP.world.main.input.call_paste(CustomEvents::PasteEvent::DataType::IMAGE, {
|
||||
.pastePosition = drawP.world.main.input.mouse.pos
|
||||
});
|
||||
drawP.world.main.g.gui.set_to_layout();
|
||||
drawP.showingImagePasteChooser = true;
|
||||
drawP.set_right_click_popup_location(drawP.world.main.input.mouse.pos / drawP.world.main.g.final_gui_scale());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -761,6 +774,28 @@ void DrawingProgramSelection::deselect_all() {
|
|||
}
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::remove_uneditable_from_selection() {
|
||||
if(selectedSet.empty())
|
||||
return;
|
||||
bool removed = false;
|
||||
std::erase_if(selectedSet, [&](CanvasComponentContainer::ObjInfo* c) {
|
||||
if(!c || !c->obj || drawP.layerMan.can_current_peer_edit_layer(c->obj->parentLayer))
|
||||
return false;
|
||||
drawP.drawCache.add_component(c);
|
||||
removed = true;
|
||||
return true;
|
||||
});
|
||||
if(removed) {
|
||||
if(selectedSet.empty())
|
||||
reset_all();
|
||||
else {
|
||||
sort_selection();
|
||||
calculate_aabb();
|
||||
}
|
||||
drawP.world.main.g.gui.set_to_layout();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::push_selection_to_front() {
|
||||
check_add_stroke_color_change_undo();
|
||||
|
||||
|
|
@ -874,7 +909,7 @@ void DrawingProgramSelection::paste_image_process_event(const CustomEvents::Past
|
|||
drawP.switch_to_tool(DrawingProgramToolType::EDIT);
|
||||
|
||||
std::vector<CanvasComponentContainer::ObjInfo*> compSetInserted;
|
||||
compSetInserted.emplace_back(drawP.add_file_to_canvas_by_data("Image from clipboard", paste.data, paste.mousePos.value()));
|
||||
compSetInserted.emplace_back(drawP.add_file_to_canvas_by_data("Image from clipboard", paste.data, paste.mousePos.value(), paste.applyFreistellen));
|
||||
if(compSetInserted.back() != nullptr) {
|
||||
drawP.drawCache.erase_component(compSetInserted.back());
|
||||
set_to_selection(compSetInserted);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class DrawingProgramSelection {
|
|||
void update();
|
||||
void sort_selection();
|
||||
void deselect_all();
|
||||
void remove_uneditable_from_selection();
|
||||
void paste_clipboard(Vector2f pasteScreenPos);
|
||||
void paste_image_process_event(const CustomEvents::PasteEvent& paste);
|
||||
std::unordered_set<CanvasComponentContainer::ObjInfo*> get_selection_as_set();
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ void DrawingProgramLayer::set_component_list_callbacks(DrawingProgramLayerListIt
|
|||
layerMan.drawP.drawCache.add_component(&(*c));
|
||||
if(c->obj->get_comp().get_type() == CanvasComponentType::IMAGE)
|
||||
layerMan.drawP.updateableComponents.emplace(&(*c));
|
||||
|
||||
// Live fog / lock enforcement on host
|
||||
if(layerMan.addToCacheOnComponentInsert && layerMan.drawP.world.netServer) {
|
||||
if(layerMan.drawP.world.currentNetUpdateClient && layerListItem.is_player_edit_blocked()) {
|
||||
layerMan.defer_reject_component(&layerListItem, c->obj.get_net_id());
|
||||
return;
|
||||
}
|
||||
layerMan.mark_fog_client_projection_dirty();
|
||||
}
|
||||
};
|
||||
eraseCallback = [&](const CanvasComponentContainer::ObjInfoIterator& c) {
|
||||
layerMan.drawP.selection.erase_component(&(*c));
|
||||
|
|
@ -48,6 +57,8 @@ void DrawingProgramLayer::set_component_list_callbacks(DrawingProgramLayerListIt
|
|||
return downloadingFile.comp == &(*c);
|
||||
});
|
||||
layerMan.drawP.updateableComponents.erase(&(*c));
|
||||
if(layerMan.addToCacheOnComponentInsert && layerMan.drawP.world.netServer)
|
||||
layerMan.mark_fog_client_projection_dirty();
|
||||
};
|
||||
components->set_insert_callback(insertCallback);
|
||||
components->set_erase_callback(eraseCallback);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <cereal/types/string.hpp>
|
||||
#include "DrawingProgramLayer.hpp"
|
||||
#include "../../World.hpp"
|
||||
#include "../../MainProgram.hpp"
|
||||
#include "SerializedBlendMode.hpp"
|
||||
|
||||
using namespace NetworkingObjects;
|
||||
|
|
@ -78,6 +79,8 @@ DrawingProgramLayerListItem::DrawingProgramLayerListItem(World& w, const Drawing
|
|||
displayData = w.netObjMan.make_obj<DisplayData>();
|
||||
displayData->alpha = initData.metaInfo.alpha;
|
||||
displayData->blendMode = initData.metaInfo.blendMode;
|
||||
displayData->playerLocked = initData.metaInfo.playerLocked;
|
||||
displayData->fogLayer = initData.metaInfo.fogLayer;
|
||||
}
|
||||
|
||||
DrawingProgramLayerListItemUndoData DrawingProgramLayerListItem::get_undo_data(WorldUndoManager& u) const {
|
||||
|
|
@ -85,6 +88,8 @@ DrawingProgramLayerListItemUndoData DrawingProgramLayerListItem::get_undo_data(W
|
|||
toRet.metaInfo.name = nameData->name;
|
||||
toRet.metaInfo.blendMode = displayData->blendMode;
|
||||
toRet.metaInfo.alpha = displayData->alpha;
|
||||
toRet.metaInfo.playerLocked = displayData->playerLocked;
|
||||
toRet.metaInfo.fogLayer = displayData->fogLayer;
|
||||
if(folderData) {
|
||||
toRet.containerUndoID = u.get_undoid_from_netid(folderData->folderList.get_net_id());
|
||||
toRet.folderData = std::vector<DrawingProgramLayerListItemUndoData>();
|
||||
|
|
@ -178,11 +183,24 @@ void DrawingProgramLayerListItem::load_file(cereal::PortableBinaryInputArchive&
|
|||
a(*nameData);
|
||||
|
||||
displayData = layerMan.drawP.world.netObjMan.make_obj<DisplayData>();
|
||||
a(*displayData);
|
||||
if(version >= VersionNumber(0, 8, 0)) {
|
||||
a(*displayData);
|
||||
// Normalize cereal bools (non-0/1 bytes must not stick as "true" forever).
|
||||
displayData->visible = displayData->visible ? true : false;
|
||||
displayData->playerLocked = displayData->playerLocked ? true : false;
|
||||
displayData->fogLayer = displayData->fogLayer ? true : false;
|
||||
}
|
||||
else {
|
||||
a(displayData->alpha, displayData->visible, displayData->blendMode);
|
||||
displayData->visible = displayData->visible ? true : false;
|
||||
displayData->playerLocked = false;
|
||||
displayData->fogLayer = false;
|
||||
}
|
||||
|
||||
bool isFolder;
|
||||
a(isFolder);
|
||||
if(isFolder) {
|
||||
displayData->fogLayer = false;
|
||||
folderData = std::make_unique<DrawingProgramLayerFolder>();
|
||||
folderData->load_file(a, version, layerMan);
|
||||
}
|
||||
|
|
@ -275,20 +293,84 @@ SerializedBlendMode DrawingProgramLayerListItem::get_blend_mode() const {
|
|||
return displayData->blendMode;
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::set_player_locked(DrawingProgramLayerManager& layerMan, bool locked) const {
|
||||
if(!displayData)
|
||||
return;
|
||||
bool newLocked = locked || displayData->fogLayer;
|
||||
if(displayData->playerLocked != newLocked) {
|
||||
displayData->playerLocked = newLocked;
|
||||
layerMan.drawP.drawCache.clear_own_cached_surfaces();
|
||||
layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all<DisplayData>(displayData, false);
|
||||
layerMan.refresh_fog_client_projection();
|
||||
}
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerListItem::get_player_locked() const {
|
||||
return displayData && displayData->playerLocked;
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::set_fog_layer(DrawingProgramLayerManager& layerMan, bool fog) const {
|
||||
if(!displayData || displayData->fogLayer == fog || is_folder())
|
||||
return;
|
||||
displayData->fogLayer = fog;
|
||||
if(fog)
|
||||
displayData->playerLocked = true;
|
||||
layerMan.drawP.drawCache.clear_own_cached_surfaces();
|
||||
layerMan.drawP.world.delayedUpdateObjectManager.send_update_to_all<DisplayData>(displayData, false);
|
||||
layerMan.refresh_fog_client_projection();
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerListItem::is_fog_layer() const {
|
||||
return displayData && displayData->fogLayer;
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerListItem::is_player_edit_blocked() const {
|
||||
return is_fog_layer() || get_player_locked();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::set_metainfo(DrawingProgramLayerManager& layerMan, const DrawingProgramLayerListItemMetaInfo& metaInfo) {
|
||||
set_blend_mode(layerMan, metaInfo.blendMode);
|
||||
set_alpha(layerMan, metaInfo.alpha);
|
||||
set_name(layerMan.drawP.world.delayedUpdateObjectManager, metaInfo.name);
|
||||
set_fog_layer(layerMan, metaInfo.fogLayer);
|
||||
set_player_locked(layerMan, metaInfo.playerLocked || metaInfo.fogLayer);
|
||||
}
|
||||
|
||||
DrawingProgramLayerListItemMetaInfo DrawingProgramLayerListItem::get_metainfo() const {
|
||||
return DrawingProgramLayerListItemMetaInfo{
|
||||
.name = get_name(),
|
||||
.alpha = get_alpha(),
|
||||
.blendMode = get_blend_mode()
|
||||
.blendMode = get_blend_mode(),
|
||||
.playerLocked = get_player_locked(),
|
||||
.fogLayer = is_fog_layer()
|
||||
};
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::write_stripped_constructor(cereal::PortableBinaryOutputArchive& a, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds) const {
|
||||
a(static_cast<bool>(folderData));
|
||||
if(folderData) {
|
||||
a(folderData->folderList.get_net_id());
|
||||
a(static_cast<uint32_t>(folderData->folderList->size()));
|
||||
for(auto& child : *folderData->folderList) {
|
||||
a(child.obj.get_net_id());
|
||||
child.obj->write_stripped_constructor(a, hiddenComponentIds);
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::vector<NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer>> keep;
|
||||
for(auto& c : *layerData->components) {
|
||||
if(!hiddenComponentIds.contains(c.obj.get_net_id()))
|
||||
keep.emplace_back(c.obj);
|
||||
}
|
||||
a(layerData->components.get_net_id());
|
||||
a(static_cast<uint32_t>(keep.size()));
|
||||
for(auto& c : keep)
|
||||
c.write_create_message(a);
|
||||
}
|
||||
nameData.write_create_message(a);
|
||||
displayData.write_create_message(a);
|
||||
}
|
||||
|
||||
void DrawingProgramLayerListItem::erase_invalid_components() {
|
||||
if(folderData)
|
||||
folderData->erase_invalid_components();
|
||||
|
|
@ -312,7 +394,9 @@ void DrawingProgramLayerListItem::register_class(World& w) {
|
|||
});
|
||||
w.delayedUpdateObjectManager.register_class<DisplayData>(w.netObjMan, NetworkingObjects::DelayUpdateSerializedClassManager::CustomConstructors<DisplayData>{
|
||||
.postUpdateFunc = [&](DisplayData& o) {
|
||||
(void)o;
|
||||
w.drawProg.drawCache.clear_own_cached_surfaces();
|
||||
w.drawProg.layerMan.ensure_editing_layer_is_editable();
|
||||
w.set_to_layout_gui_if_focus();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ struct DrawingProgramLayerListItemMetaInfo {
|
|||
std::string name;
|
||||
float alpha = 1.0f;
|
||||
SerializedBlendMode blendMode = SerializedBlendMode::BLEND_SRC_OVER;
|
||||
bool playerLocked = false;
|
||||
bool fogLayer = false;
|
||||
bool operator==(const DrawingProgramLayerListItemMetaInfo&) const = default;
|
||||
};
|
||||
|
||||
|
|
@ -80,12 +82,22 @@ class DrawingProgramLayerListItem {
|
|||
void set_blend_mode(DrawingProgramLayerManager& layerMan, SerializedBlendMode newBlendMode) const;
|
||||
SerializedBlendMode get_blend_mode() const;
|
||||
|
||||
void set_player_locked(DrawingProgramLayerManager& layerMan, bool locked) const;
|
||||
bool get_player_locked() const;
|
||||
void set_fog_layer(DrawingProgramLayerManager& layerMan, bool fog) const;
|
||||
bool is_fog_layer() const;
|
||||
// Fog layers are always treated as player-locked for edit checks.
|
||||
bool is_player_edit_blocked() const;
|
||||
|
||||
void set_metainfo(DrawingProgramLayerManager& layerMan, const DrawingProgramLayerListItemMetaInfo& metaInfo);
|
||||
DrawingProgramLayerListItemMetaInfo get_metainfo() const;
|
||||
|
||||
uint32_t get_component_count() const;
|
||||
|
||||
void erase_invalid_components();
|
||||
|
||||
// Host→client join projection: same shape as write_constructor_func, omits hidden comps.
|
||||
void write_stripped_constructor(cereal::PortableBinaryOutputArchive& a, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds) const;
|
||||
private:
|
||||
static void write_constructor_func(const NetworkingObjects::NetObjTemporaryPtr<DrawingProgramLayerListItem>& o, cereal::PortableBinaryOutputArchive& a);
|
||||
static void read_constructor_func(const NetworkingObjects::NetObjTemporaryPtr<DrawingProgramLayerListItem>& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr<NetServer::ClientData>& c);
|
||||
|
|
@ -101,8 +113,10 @@ class DrawingProgramLayerListItem {
|
|||
float alpha = 1.0f;
|
||||
bool visible = true;
|
||||
SerializedBlendMode blendMode = SerializedBlendMode::BLEND_SRC_OVER;
|
||||
bool playerLocked = false;
|
||||
bool fogLayer = false;
|
||||
template <typename Archive> void serialize(Archive& a) {
|
||||
a(alpha, visible, blendMode);
|
||||
a(alpha, visible, blendMode, playerLocked, fogLayer);
|
||||
}
|
||||
};
|
||||
NetworkingObjects::NetObjOwnerPtr<NameData> nameData;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
#include "DrawingProgramLayerManager.hpp"
|
||||
#include "FogStripper.hpp"
|
||||
#include "../../World.hpp"
|
||||
#include "Helpers/NetworkingObjects/NetObjID.hpp"
|
||||
#include "Helpers/NetworkingObjects/NetObjOrderedList.hpp"
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
#include "../../CanvasComponents/CanvasComponentAllocator.hpp"
|
||||
#include "../../CanvasComponents/CanvasComponent.hpp"
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
|
||||
DrawingProgramLayerManager::DrawingProgramLayerManager(DrawingProgram& drawProg):
|
||||
drawP(drawProg), // initialize drawP first for next objects to be initialized properly
|
||||
|
|
@ -44,7 +46,10 @@ void DrawingProgramLayerManager::scale_up(const WorldScalar& scaleUpAmount) {
|
|||
}
|
||||
|
||||
void DrawingProgramLayerManager::write_components_server(cereal::PortableBinaryOutputArchive& a) {
|
||||
layerTreeRoot.write_create_message(a);
|
||||
auto hidden = FogStripper::compute_hidden_component_ids(*this);
|
||||
clientHiddenComponentIds = hidden;
|
||||
a(layerTreeRoot.get_net_id());
|
||||
layerTreeRoot->write_stripped_constructor(a, hidden);
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::read_components_client(cereal::PortableBinaryInputArchive& a) {
|
||||
|
|
@ -58,6 +63,7 @@ void DrawingProgramLayerManager::read_components_client(cereal::PortableBinaryIn
|
|||
});
|
||||
drawP.rebuild_cache();
|
||||
editingLayer = layerTreeRoot->get_folder().get_initial_editing_layer();
|
||||
ensure_editing_layer_is_editable();
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerManager::is_a_layer_being_edited() {
|
||||
|
|
@ -104,6 +110,10 @@ const DrawingProgramLayerListItem& DrawingProgramLayerManager::get_layer_root()
|
|||
}
|
||||
|
||||
bool DrawingProgramLayerManager::component_passes_layer_selector(CanvasComponentContainer::ObjInfo* c, LayerSelector layerSelector) {
|
||||
if(!c || !c->obj || !c->obj->parentLayer)
|
||||
return false;
|
||||
if(!can_current_peer_edit_layer(c->obj->parentLayer))
|
||||
return false;
|
||||
switch(layerSelector) {
|
||||
case LayerSelector::ALL_VISIBLE_LAYERS:
|
||||
return c->obj->parentLayer->get_visible();
|
||||
|
|
@ -243,6 +253,11 @@ std::vector<CanvasComponentContainer::ObjInfoIterator> DrawingProgramLayerManage
|
|||
auto editLayerPtr = editingLayer.lock();
|
||||
if(!editLayerPtr)
|
||||
throw std::runtime_error("[DrawingProgramLayerManager::add_many_components_to_layer_being_edited] No layer selected!");
|
||||
if(!can_current_peer_edit_layer(editLayerPtr.get())) {
|
||||
for(auto& [it, obj] : newObjs)
|
||||
delete obj;
|
||||
return {};
|
||||
}
|
||||
auto toRet = editLayerPtr->get_layer().components->insert_sorted_list_and_send_create(editLayerPtr->get_layer().components, newObjs);
|
||||
add_undo_place_components(editingLayer.lock().get(), toRet);
|
||||
return toRet;
|
||||
|
|
@ -276,10 +291,90 @@ void DrawingProgramLayerManager::disable_add_to_cache_and_commit_update_block(co
|
|||
addToCacheOnComponentInsert = true;
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerManager::can_current_peer_edit_layer(const DrawingProgramLayerListItem* layer) const {
|
||||
if(!layer)
|
||||
return false;
|
||||
if(FogStripper::world_is_host(drawP.world))
|
||||
return true;
|
||||
return !layer->is_player_edit_blocked();
|
||||
}
|
||||
|
||||
bool DrawingProgramLayerManager::client_may_modify_component(const CanvasComponentContainer* comp) const {
|
||||
// Host-side authority check for CLIENT messages — must NOT use can_current_peer_edit_layer
|
||||
// (that always returns true on the host).
|
||||
if(!comp || !comp->parentLayer)
|
||||
return false;
|
||||
return !comp->parentLayer->is_player_edit_blocked();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::ensure_editing_layer_is_editable() {
|
||||
auto cur = editingLayer.lock();
|
||||
if(cur && can_current_peer_edit_layer(cur.get()))
|
||||
return;
|
||||
if(!layerTreeRoot)
|
||||
return;
|
||||
std::function<NetworkingObjects::NetObjWeakPtr<DrawingProgramLayerListItem>(const DrawingProgramLayerFolder&)> findEditable;
|
||||
findEditable = [&](const DrawingProgramLayerFolder& folder) -> NetworkingObjects::NetObjWeakPtr<DrawingProgramLayerListItem> {
|
||||
for(auto& c : *folder.folderList) {
|
||||
if(!c.obj->is_folder() && can_current_peer_edit_layer(c.obj.get()))
|
||||
return c.obj;
|
||||
}
|
||||
for(auto& c : *folder.folderList) {
|
||||
if(c.obj->is_folder()) {
|
||||
auto found = findEditable(c.obj->get_folder());
|
||||
if(!found.expired())
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
editingLayer = findEditable(layerTreeRoot->get_folder());
|
||||
drawP.selection.remove_uneditable_from_selection();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::refresh_fog_client_projection() {
|
||||
fogClientProjectionDirty = false;
|
||||
FogStripper::sync_hidden_set_to_clients(drawP.world, *this);
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::mark_fog_client_projection_dirty() {
|
||||
if(drawP.world.netServer)
|
||||
fogClientProjectionDirty = true;
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::flush_fog_client_projection_if_dirty() {
|
||||
if(!fogClientProjectionDirty)
|
||||
return;
|
||||
refresh_fog_client_projection();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::defer_reject_component(DrawingProgramLayerListItem* layer, NetworkingObjects::NetObjID compId) {
|
||||
deferredRejectInserts.emplace_back(layer, compId);
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::flush_deferred_rejects() {
|
||||
if(deferredRejectInserts.empty())
|
||||
return;
|
||||
auto pending = std::move(deferredRejectInserts);
|
||||
deferredRejectInserts.clear();
|
||||
for(auto& [layer, compId] : pending) {
|
||||
if(!layer || layer->is_folder())
|
||||
continue;
|
||||
auto& comps = layer->get_layer().components;
|
||||
if(!comps->contains(compId))
|
||||
continue;
|
||||
comps->erase(comps, comps->get(compId));
|
||||
}
|
||||
}
|
||||
|
||||
CanvasComponentContainer::ObjInfo* DrawingProgramLayerManager::add_component_to_layer_being_edited(CanvasComponentContainer* newObj) {
|
||||
auto editLayerPtr = editingLayer.lock();
|
||||
if(!editLayerPtr)
|
||||
throw std::runtime_error("[DrawingProgramLayerManager::add_component_to_layer_being_edited] No layer selected!");
|
||||
if(!can_current_peer_edit_layer(editLayerPtr.get())) {
|
||||
delete newObj;
|
||||
return nullptr;
|
||||
}
|
||||
return &(*editLayerPtr->get_layer().components->push_back_and_send_create(editLayerPtr->get_layer().components, newObj));
|
||||
}
|
||||
|
||||
|
|
@ -311,6 +406,7 @@ void DrawingProgramLayerManager::load_file(cereal::PortableBinaryInputArchive& a
|
|||
layerTreeRoot->erase_invalid_components();
|
||||
drawP.rebuild_cache();
|
||||
editingLayer = layerTreeRoot->get_folder().get_initial_editing_layer();
|
||||
ensure_editing_layer_is_editable();
|
||||
}
|
||||
|
||||
void DrawingProgramLayerManager::save_file(cereal::PortableBinaryOutputArchive& a) const {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include "DrawingProgramLayerListItem.hpp"
|
||||
#include "DrawingProgramLayerManagerGUI.hpp"
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
class DrawingProgramLayerManager {
|
||||
private:
|
||||
|
|
@ -85,5 +86,25 @@ class DrawingProgramLayerManager {
|
|||
std::vector<CanvasComponentContainer::ObjInfo*> get_flattened_component_list() const;
|
||||
std::vector<DrawingProgramLayerListItem*> get_flattened_layer_list();
|
||||
void add_undo_place_component(CanvasComponentContainer::ObjInfo* objInfo);
|
||||
|
||||
bool can_current_peer_edit_layer(const DrawingProgramLayerListItem* layer) const;
|
||||
bool client_may_modify_component(const CanvasComponentContainer* comp) const;
|
||||
void ensure_editing_layer_is_editable();
|
||||
/** Host: recompute fog coverage and erase/reveal (+ resources) on clients. */
|
||||
void refresh_fog_client_projection();
|
||||
/** Coalesce frequent mesh edits; flushed once per frame in DrawingProgram::update. */
|
||||
void mark_fog_client_projection_dirty();
|
||||
void flush_fog_client_projection_if_dirty();
|
||||
|
||||
// NetObjIDs omitted from the last client projection (host only).
|
||||
std::unordered_set<NetworkingObjects::NetObjID> clientHiddenComponentIds;
|
||||
|
||||
// Client inserts into locked/fog layers — erase after the NetObj update finishes.
|
||||
void defer_reject_component(DrawingProgramLayerListItem* layer, NetworkingObjects::NetObjID compId);
|
||||
void flush_deferred_rejects();
|
||||
|
||||
DrawingProgramLayerManagerGUI listGUI;
|
||||
private:
|
||||
std::vector<std::pair<DrawingProgramLayerListItem*, NetworkingObjects::NetObjID>> deferredRejectInserts;
|
||||
bool fogClientProjectionDirty = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include "DrawingProgramLayerManagerGUI.hpp"
|
||||
#include "DrawingProgramLayerListItem.hpp"
|
||||
#include "DrawingProgramLayerManager.hpp"
|
||||
#include "FogStripper.hpp"
|
||||
#include "../DrawingProgram.hpp"
|
||||
#include "../../World.hpp"
|
||||
#include "../../MainProgram.hpp"
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
#include "../../GUIStuff/ElementHelpers/LayoutHelpers.hpp"
|
||||
#include "../../GUIStuff/ElementHelpers/TextBoxHelpers.hpp"
|
||||
#include "../../GUIStuff/ElementHelpers/NumberSliderHelpers.hpp"
|
||||
#include "../../GUIStuff/ElementHelpers/CheckBoxHelpers.hpp"
|
||||
#include "../../GUIStuff/Elements/SVGIcon.hpp"
|
||||
#include "../../GUIStuff/Elements/DropDown.hpp"
|
||||
|
||||
|
|
@ -122,15 +124,21 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() {
|
|||
.childAlignment = {.x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}
|
||||
},
|
||||
}) {
|
||||
ellipse_wide_paragraph_label(gui, "layer name", layer->get_name());
|
||||
std::string label = layer->get_name();
|
||||
if(layer->is_fog_layer())
|
||||
label += " [fog]";
|
||||
else if(layer->is_player_edit_blocked())
|
||||
label += " [lock]";
|
||||
ellipse_wide_paragraph_label(gui, "layer name", label);
|
||||
}
|
||||
if(!layer->is_folder()) {
|
||||
if(!layer->is_folder() && layerMan.can_current_peer_edit_layer(layer.get())) {
|
||||
gui.set_z_index_keep_clipping_region(gui.get_z_index() + 1, [&] {
|
||||
svg_icon_button(gui, "edit button", "data/icons/pencil.svg", {
|
||||
.drawType = SelectableButton::DrawType::TRANSPARENT_ALL,
|
||||
.size = TreeListing::ENTRY_HEIGHT,
|
||||
.onClick = [&, layer] {
|
||||
layerMan.editingLayer = layer;
|
||||
if(layerMan.can_current_peer_edit_layer(layer.get()))
|
||||
layerMan.editingLayer = layer;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -155,8 +163,11 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() {
|
|||
});
|
||||
},
|
||||
.onDoubleClick = [&](const TreeListingObjIndexList& objIndex) {
|
||||
layerMan.editingLayer = get_layer_from_obj_index(objIndex);
|
||||
layerMan.drawP.world.main.g.gui.set_to_layout();
|
||||
auto layer = get_layer_from_obj_index(objIndex);
|
||||
if(layerMan.can_current_peer_edit_layer(layer.get())) {
|
||||
layerMan.editingLayer = layer;
|
||||
layerMan.drawP.world.main.g.gui.set_to_layout();
|
||||
}
|
||||
},
|
||||
.onSelectChange = [&] {
|
||||
editing_layer_check();
|
||||
|
|
@ -441,6 +452,34 @@ void DrawingProgramLayerManagerGUI::setup_list_gui() {
|
|||
}
|
||||
});
|
||||
});
|
||||
if(FogStripper::world_is_host(world) && !editingLayerLock->is_folder()) {
|
||||
// Must not use stack bool* with checkbox_boolean_field — CheckBox
|
||||
// calls isTicked/onClick after layout returns (UAF → random fog flags).
|
||||
checkbox_field(gui, "fog layer toggle", "Fog of War layer",
|
||||
[&] {
|
||||
auto layer = editingLayer.lock();
|
||||
return layer && layer->is_fog_layer();
|
||||
},
|
||||
[&] {
|
||||
auto layer = editingLayer.lock();
|
||||
if(layer)
|
||||
layer->set_fog_layer(layerMan, !layer->is_fog_layer());
|
||||
});
|
||||
if(!editingLayerLock->is_fog_layer()) {
|
||||
checkbox_field(gui, "player lock toggle", "Lock for players",
|
||||
[&] {
|
||||
auto layer = editingLayer.lock();
|
||||
return layer && layer->get_player_locked();
|
||||
},
|
||||
[&] {
|
||||
auto layer = editingLayer.lock();
|
||||
if(layer)
|
||||
layer->set_player_locked(layerMan, !layer->get_player_locked());
|
||||
});
|
||||
}
|
||||
else
|
||||
text_label(gui, "Fog layers are always locked for players.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
258
src/DrawingProgram/Layers/FogStripper.cpp
Normal file
258
src/DrawingProgram/Layers/FogStripper.cpp
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* InfiniPaint
|
||||
* Copyright (C) 2025-2026 Yousef Khadadeh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FogStripper.hpp"
|
||||
#include "DrawingProgramLayerManager.hpp"
|
||||
#include "DrawingProgramLayerListItem.hpp"
|
||||
#include "../../World.hpp"
|
||||
#include "../../CommandList.hpp"
|
||||
#include "../../CanvasComponents/MeshCanvasComponent.hpp"
|
||||
#include "../../CanvasComponents/RectangleCanvasComponent.hpp"
|
||||
#include "../../CanvasComponents/EllipseCanvasComponent.hpp"
|
||||
#include <Helpers/NetworkingObjects/NetObjOrderedList.hpp>
|
||||
#include <Helpers/SCollision.hpp>
|
||||
#include <include/core/SkPath.h>
|
||||
#include <include/core/SkPathBuilder.h>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace FogStripper {
|
||||
namespace {
|
||||
|
||||
struct FogCoverPiece {
|
||||
CanvasComponentContainer* fogComp = nullptr;
|
||||
float effectiveAlpha = 1.0f;
|
||||
};
|
||||
|
||||
float component_paint_alpha(const CanvasComponentContainer& c) {
|
||||
switch(c.get_comp().get_type()) {
|
||||
case CanvasComponentType::MESH:
|
||||
return static_cast<const MeshCanvasComponent&>(c.get_comp()).d.color.w();
|
||||
case CanvasComponentType::RECTANGLE: {
|
||||
auto& r = static_cast<const RectangleCanvasComponent&>(c.get_comp());
|
||||
return std::max(r.d.fillColor.w(), r.d.strokeColor.w());
|
||||
}
|
||||
case CanvasComponentType::ELLIPSE: {
|
||||
auto& e = static_cast<const EllipseCanvasComponent&>(c.get_comp());
|
||||
return std::max(e.d.fillColor.w(), e.d.strokeColor.w());
|
||||
}
|
||||
default:
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
bool point_inside_fog(CanvasComponentContainer& fogComp, const WorldVec& worldPoint) {
|
||||
// collides_with_point expects the probe in fogComp.coords space.
|
||||
Vector2f local = fogComp.coords.to_space(worldPoint);
|
||||
return fogComp.collides_with_point(fogComp.coords, local);
|
||||
}
|
||||
|
||||
bool victim_fully_covered_by_fog(CanvasComponentContainer& victim, const FogCoverPiece& fogPiece) {
|
||||
if(!victim.get_world_bounds().has_value() || !fogPiece.fogComp->get_world_bounds().has_value())
|
||||
return false;
|
||||
if(fogPiece.effectiveAlpha < OPAQUE_ALPHA_THRESHOLD)
|
||||
return false;
|
||||
|
||||
const auto& victimAABB = victim.get_world_bounds().value();
|
||||
const auto& fogAABB = fogPiece.fogComp->get_world_bounds().value();
|
||||
// Must be completely inside the fog piece, not merely overlapping.
|
||||
if(!fogAABB.fully_contains_aabb(victimAABB))
|
||||
return false;
|
||||
|
||||
// Confirm shape coverage (brush blobs are not the AABB): corners + center inside fog paint.
|
||||
const WorldVec corners[5] = {
|
||||
victimAABB.min,
|
||||
victimAABB.max,
|
||||
victimAABB.top_right(),
|
||||
victimAABB.bottom_left(),
|
||||
victimAABB.center()
|
||||
};
|
||||
for(const auto& corner : corners) {
|
||||
if(!point_inside_fog(*fogPiece.fogComp, corner))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool world_is_host(const World& world) {
|
||||
return world.netServer != nullptr || world.netClient == nullptr;
|
||||
}
|
||||
|
||||
std::unordered_set<NetworkingObjects::NetObjID> compute_hidden_component_ids(DrawingProgramLayerManager& layerMan) {
|
||||
std::unordered_set<NetworkingObjects::NetObjID> hidden;
|
||||
if(!layerMan.layer_tree_root_exists())
|
||||
return hidden;
|
||||
|
||||
std::vector<DrawingProgramLayerListItem*> layers = layerMan.get_flattened_layer_list();
|
||||
// Flattened forward order: index 0 is top of stack (drawn last). Fog at i covers layers j > i.
|
||||
std::vector<FogCoverPiece> activeFog;
|
||||
|
||||
for(size_t i = 0; i < layers.size(); ++i) {
|
||||
DrawingProgramLayerListItem* layer = layers[i];
|
||||
if(layer->is_fog_layer() && layer->get_visible()) {
|
||||
float layerAlpha = layer->get_alpha();
|
||||
for(auto& c : *layer->get_layer().components) {
|
||||
FogCoverPiece piece;
|
||||
piece.fogComp = c.obj.get();
|
||||
piece.effectiveAlpha = layerAlpha * component_paint_alpha(*c.obj);
|
||||
if(piece.effectiveAlpha >= OPAQUE_ALPHA_THRESHOLD)
|
||||
activeFog.emplace_back(piece);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(activeFog.empty())
|
||||
continue;
|
||||
|
||||
for(auto& c : *layer->get_layer().components) {
|
||||
for(const auto& fogPiece : activeFog) {
|
||||
if(victim_fully_covered_by_fog(*c.obj, fogPiece)) {
|
||||
hidden.insert(c.obj.get_net_id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hidden;
|
||||
}
|
||||
|
||||
std::unordered_set<NetworkingObjects::NetObjID> compute_visible_resource_ids(DrawingProgramLayerManager& layerMan, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds) {
|
||||
std::unordered_set<NetworkingObjects::NetObjID> resources;
|
||||
auto comps = layerMan.get_flattened_component_list();
|
||||
for(auto* info : comps) {
|
||||
if(hiddenComponentIds.contains(info->obj.get_net_id()))
|
||||
continue;
|
||||
info->obj->get_comp().get_used_resources(resources);
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
void write_stripped_layer_tree(const DrawingProgramLayerListItem& root, cereal::PortableBinaryOutputArchive& a, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds) {
|
||||
root.write_stripped_constructor(a, hiddenComponentIds);
|
||||
}
|
||||
|
||||
void sync_hidden_set_to_clients(World& world, DrawingProgramLayerManager& layerMan) {
|
||||
if(!world.netServer)
|
||||
return;
|
||||
|
||||
auto newHidden = compute_hidden_component_ids(layerMan);
|
||||
auto& prevHidden = layerMan.clientHiddenComponentIds;
|
||||
|
||||
std::vector<NetworkingObjects::NetObjID> newlyHidden;
|
||||
std::vector<NetworkingObjects::NetObjID> newlyRevealed;
|
||||
for(const auto& id : newHidden) {
|
||||
if(!prevHidden.contains(id))
|
||||
newlyHidden.push_back(id);
|
||||
}
|
||||
for(const auto& id : prevHidden) {
|
||||
if(!newHidden.contains(id))
|
||||
newlyRevealed.push_back(id);
|
||||
}
|
||||
|
||||
if(!newlyHidden.empty()) {
|
||||
std::unordered_map<NetworkingObjects::NetObjID, std::vector<NetworkingObjects::NetObjID>> eraseByList;
|
||||
auto comps = layerMan.get_flattened_component_list();
|
||||
for(auto* info : comps) {
|
||||
NetworkingObjects::NetObjID compId = info->obj.get_net_id();
|
||||
if(std::find(newlyHidden.begin(), newlyHidden.end(), compId) == newlyHidden.end())
|
||||
continue;
|
||||
auto* parent = info->obj->parentLayer;
|
||||
if(!parent || parent->is_folder())
|
||||
continue;
|
||||
eraseByList[parent->get_layer().components.get_net_id()].push_back(compId);
|
||||
}
|
||||
for(auto& [listId, ids] : eraseByList) {
|
||||
auto listPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer::NetList>(listId);
|
||||
if(!listPtr)
|
||||
continue;
|
||||
listPtr.send_server_update_to_all_clients(RELIABLE_COMMAND_CHANNEL, [&ids](const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer::NetList>&, cereal::PortableBinaryOutputArchive& a) {
|
||||
a(NetworkingObjects::ObjPtrOrderedListCommand_StoC::ERASE_ID_LIST, ids);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(!newlyRevealed.empty()) {
|
||||
struct RevealItem {
|
||||
NetworkingObjects::NetObjID listId;
|
||||
NetworkingObjects::NetObjID afterId;
|
||||
NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer> obj;
|
||||
};
|
||||
std::vector<RevealItem> reveals;
|
||||
auto comps = layerMan.get_flattened_component_list();
|
||||
for(auto* info : comps) {
|
||||
NetworkingObjects::NetObjID compId = info->obj.get_net_id();
|
||||
if(std::find(newlyRevealed.begin(), newlyRevealed.end(), compId) == newlyRevealed.end())
|
||||
continue;
|
||||
auto* parent = info->obj->parentLayer;
|
||||
if(!parent || parent->is_folder())
|
||||
continue;
|
||||
RevealItem r;
|
||||
r.listId = parent->get_layer().components.get_net_id();
|
||||
r.obj = info->obj;
|
||||
r.afterId = NetworkingObjects::NetObjID{0, 0};
|
||||
if(info->pos > 0) {
|
||||
auto prevIt = parent->get_layer().components->at(info->pos - 1);
|
||||
r.afterId = prevIt->obj.get_net_id();
|
||||
while(prevHidden.contains(r.afterId) || newHidden.contains(r.afterId)) {
|
||||
if(prevIt->pos == 0) {
|
||||
r.afterId = NetworkingObjects::NetObjID{0, 0};
|
||||
break;
|
||||
}
|
||||
prevIt = parent->get_layer().components->at(prevIt->pos - 1);
|
||||
r.afterId = prevIt->obj.get_net_id();
|
||||
}
|
||||
}
|
||||
reveals.push_back(r);
|
||||
}
|
||||
|
||||
std::unordered_map<NetworkingObjects::NetObjID, std::vector<RevealItem>> byList;
|
||||
for(auto& r : reveals)
|
||||
byList[r.listId].push_back(r);
|
||||
|
||||
for(auto& [listId, items] : byList) {
|
||||
auto listPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer::NetList>(listId);
|
||||
if(!listPtr)
|
||||
continue;
|
||||
listPtr.send_server_update_to_all_clients(RELIABLE_COMMAND_CHANNEL, [&items](const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer::NetList>&, cereal::PortableBinaryOutputArchive& a) {
|
||||
a(NetworkingObjects::ObjPtrOrderedListCommand_StoC::INSERT_LIST_CONSTRUCT, static_cast<uint32_t>(items.size()));
|
||||
for(auto& item : items) {
|
||||
a(item.afterId);
|
||||
item.obj.write_create_message(a);
|
||||
}
|
||||
});
|
||||
std::unordered_set<NetworkingObjects::NetObjID> needed;
|
||||
for(auto& item : items)
|
||||
item.obj->get_comp().get_used_resources(needed);
|
||||
for(const auto& resId : needed) {
|
||||
for(auto& r : world.rMan.resource_list()) {
|
||||
if(r.get_net_id() == resId) {
|
||||
world.netServer->send_items_to_all_clients(RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE_ID, resId);
|
||||
world.netServer->send_items_to_all_clients(RESOURCE_COMMAND_CHANNEL, CLIENT_NEW_RESOURCE_DATA, *r);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevHidden = std::move(newHidden);
|
||||
}
|
||||
|
||||
} // namespace FogStripper
|
||||
45
src/DrawingProgram/Layers/FogStripper.hpp
Normal file
45
src/DrawingProgram/Layers/FogStripper.hpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* InfiniPaint
|
||||
* Copyright (C) 2025-2026 Yousef Khadadeh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Helpers/NetworkingObjects/NetObjID.hpp>
|
||||
#include <cereal/archives/portable_binary.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
class DrawingProgramLayerManager;
|
||||
class DrawingProgramLayerListItem;
|
||||
class World;
|
||||
|
||||
// Host-side projection only. Never mutates the host world graph.
|
||||
namespace FogStripper {
|
||||
constexpr float OPAQUE_ALPHA_THRESHOLD = 0.95f;
|
||||
|
||||
bool world_is_host(const World& world);
|
||||
|
||||
// Component NetObjIDs that must be omitted from player join/live views.
|
||||
std::unordered_set<NetworkingObjects::NetObjID> compute_hidden_component_ids(DrawingProgramLayerManager& layerMan);
|
||||
|
||||
// Resources still referenced by the player-visible projection (not hidden, plus fog paint).
|
||||
std::unordered_set<NetworkingObjects::NetObjID> compute_visible_resource_ids(DrawingProgramLayerManager& layerMan, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds);
|
||||
|
||||
// Write layerTreeRoot create message with hidden components omitted from leaf lists.
|
||||
void write_stripped_layer_tree(const DrawingProgramLayerListItem& root, cereal::PortableBinaryOutputArchive& a, const std::unordered_set<NetworkingObjects::NetObjID>& hiddenComponentIds);
|
||||
|
||||
// After fog geometry/flags change: erase newly hidden IDs from clients; insert newly revealed ones.
|
||||
void sync_hidden_set_to_clients(World& world, DrawingProgramLayerManager& layerMan);
|
||||
}
|
||||
|
|
@ -68,6 +68,8 @@ void BrushTool::input_mouse_button_on_canvas_callback(const InputManager::MouseB
|
|||
BrushComponentCode::mouse_button(drawP, genData, newMeshContainer->coords, button, relativeWidthResult.first.value());
|
||||
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newMeshContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
commit_data(false);
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited)
|
||||
|
|
|
|||
|
|
@ -267,7 +267,10 @@ void EditTool::edit_start(CanvasComponentContainer::ObjInfo* comp, bool initUndo
|
|||
}
|
||||
|
||||
bool EditTool::is_editable(CanvasComponentContainer::ObjInfo* comp) {
|
||||
return true; // Brush strokes used to not be editable, but now they are so this always returns true
|
||||
if(!comp || !comp->obj)
|
||||
return false;
|
||||
// Host may edit fog; players may not (fog / player-locked layers).
|
||||
return drawP.layerMan.can_current_peer_edit_layer(comp->obj->parentLayer);
|
||||
}
|
||||
|
||||
void EditTool::tool_update() {
|
||||
|
|
|
|||
|
|
@ -88,12 +88,39 @@ void ImageEditTool::edit_gui(Toolbar& t) {
|
|||
}, resourceData->name, true);
|
||||
}
|
||||
});
|
||||
text_button(gui, "file freistellen", "Freistellen", {
|
||||
.wide = true,
|
||||
.onClick = [&] {
|
||||
drawP.process_images_on({comp}, true, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
text_label_centered(gui, "Loading resource...");
|
||||
});
|
||||
}
|
||||
|
||||
void ImageEditTool::right_click_popup_gui(Toolbar& t, Vector2f popupPos) {
|
||||
using namespace GUIStuff;
|
||||
using namespace ElementHelpers;
|
||||
|
||||
auto& gui = drawP.world.main.g.gui;
|
||||
drawP.right_click_action_menu(popupPos, [&] {
|
||||
text_label_light(gui, "Image");
|
||||
drawP.popup_menu_action_button("Freistellen", "Freistellen", [&] {
|
||||
drawP.process_images_on({comp}, true, true);
|
||||
});
|
||||
drawP.popup_menu_action_button("Paste Image", "Paste Image", [&, popupPos] {
|
||||
drawP.selection.deselect_all();
|
||||
drawP.request_clipboard_image_paste(popupPos * drawP.world.main.g.final_gui_scale(), false);
|
||||
});
|
||||
drawP.popup_menu_action_button("Paste Image Freistellen", "Paste Image + Freistellen", [&, popupPos] {
|
||||
drawP.selection.deselect_all();
|
||||
drawP.request_clipboard_image_paste(popupPos * drawP.world.main.g.final_gui_scale(), true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ImageEditTool::gui_phone_toolbox(PhoneDrawingProgramScreen& t) {
|
||||
using namespace GUIStuff;
|
||||
using namespace ElementHelpers;
|
||||
|
|
|
|||
|
|
@ -30,4 +30,5 @@ class ImageEditTool : public DrawingProgramEditToolBase {
|
|||
virtual void edit_update() override;
|
||||
virtual void edit_gui(Toolbar& t) override;
|
||||
virtual void gui_phone_toolbox(PhoneDrawingProgramScreen& t) override;
|
||||
virtual void right_click_popup_gui(Toolbar& t, Vector2f popupPos) override;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ void EllipseDrawTool::input_mouse_button_on_canvas_callback(const InputManager::
|
|||
newContainer->coords = drawP.world.drawData.cam.c;
|
||||
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited)
|
||||
commit();
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ void LineDrawTool::input_mouse_button_on_canvas_callback(const InputManager::Mou
|
|||
p.pos = ensure_points_have_distance(p.pos, p.pos, 1.0f);
|
||||
brushPoints.emplace_back(p);
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newMeshContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
commit_data(false);
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited)
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ void RectDrawTool::input_mouse_button_on_canvas_callback(const InputManager::Mou
|
|||
newRectangle.d.fillStrokeMode = static_cast<uint8_t>(fillStrokeMode);
|
||||
newContainer->coords = drawP.world.drawData.cam.c;
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited)
|
||||
commit();
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ void TextBoxTool::input_mouse_button_on_canvas_callback(const InputManager::Mous
|
|||
newContainer->coords = drawP.world.drawData.cam.c;
|
||||
newContainer->coords.scale_about_double(drawP.world.drawData.cam.c.from_space(startAt), 1.0 / drawP.world.main.get_scale_and_density_factor_gui());
|
||||
objInfoBeingEdited = drawP.layerMan.add_component_to_layer_being_edited(newContainer);
|
||||
if(!objInfoBeingEdited)
|
||||
return;
|
||||
}
|
||||
else if(!button.down && objInfoBeingEdited) {
|
||||
make_sure_textbox_is_big();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue