/* * 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 . */ #include "DrawingProgramSelection.hpp" #include "DrawingProgram.hpp" #include "../World.hpp" #include "../MainProgram.hpp" #include "../InputManager.hpp" #include #include #ifdef USE_SKIA_BACKEND_GRAPHITE #include #elif USE_SKIA_BACKEND_GANESH #include #include #endif #include #include #define ROTATION_POINT_RADIUS_MULTIPLIER 0.7f #define ROTATION_POINTS_DISTANCE 20.0f #ifdef __EMSCRIPTEN__ #include #endif #include "../GUIStuff/ElementHelpers/TextLabelHelpers.hpp" #include "../GUIStuff/ElementHelpers/RadioButtonHelpers.hpp" #include "../GUIStuff/ElementHelpers/LayoutHelpers.hpp" #include "../GUIStuff/ElementHelpers/ButtonHelpers.hpp" DrawingProgramSelection::DrawingProgramSelection(DrawingProgram& initDrawP): drawP(initDrawP) {} void DrawingProgramSelection::selection_gui(Toolbar& t) { using namespace GUIStuff; using namespace ElementHelpers; auto& gui = drawP.world.main.g.gui; gui.new_id("general selection gui", [&] { text_label(gui, "Select from:"); radio_button_selector(gui, "layer selector", &drawP.controls.layerSelector, { {"Layer being edited", DrawingProgramLayerManager::LayerSelector::LAYER_BEING_EDITED}, {"All visible layers", DrawingProgramLayerManager::LayerSelector::ALL_VISIBLE_LAYERS} }); if(is_something_selected()) { left_to_right_line_layout(gui, [&]() { t.color_button_right("Stroke Color Button", &strokeColorChangeData.newColor, { .onSelectorButtonClick = [&] { if(strokeColorChangeData.newColor.w() == 0.0f) strokeColorChangeData.newColor.w() = 1.0f; update_selection_stroke_color(); }, .onChange = [&] { update_selection_stroke_color(); } }); text_label(gui, "Stroke Color"); }); } }); } void DrawingProgramSelection::phone_selection_gui(PhoneDrawingProgramScreen& t) { using namespace GUIStuff; using namespace ElementHelpers; auto& gui = drawP.world.main.g.gui; gui.new_id("general selection gui", [&] { text_label(gui, "Select from:"); radio_button_selector(gui, "layer selector", &drawP.controls.layerSelector, { {"Layer being edited", DrawingProgramLayerManager::LayerSelector::LAYER_BEING_EDITED}, {"All visible layers", DrawingProgramLayerManager::LayerSelector::ALL_VISIBLE_LAYERS} }); }); } Vector4f* DrawingProgramSelection::color_picker_color(Vector4f* oldColor) { if(is_something_selected()) { if(oldColor == &strokeColorChangeData.newColor) return oldColor; } return nullptr; } void DrawingProgramSelection::phone_selection_bottom_toolbar(PhoneDrawingProgramScreen& t) { using namespace GUIStuff; using namespace ElementHelpers; auto& gui = drawP.world.main.g.gui; CLAY_AUTO_ID({ .layout = { .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) }, .childGap = gui.io.theme->childGap1, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER}, .layoutDirection = CLAY_LEFT_TO_RIGHT } }) { gui.element("selection bottom toolbar", [&](LayoutElement*, const Clay_ElementId& lId) { CLAY(lId, { .layout = { .sizing = {.width = CLAY_SIZING_FIT(0), .height = CLAY_SIZING_FIT(0)} }, .backgroundColor = convert_vec4(gui.io.theme->backColor1), .cornerRadius = CLAY_CORNER_RADIUS(gui.io.theme->windowCorners1) }) { gui.clipping_element("selection tools scroll", ScrollArea::Options{ .scrollHorizontal = true, .clipHorizontal = true, .scrollbarX = ScrollArea::ScrollbarType::NONE, .layoutDirection = CLAY_LEFT_TO_RIGHT, .xAlign = CLAY_ALIGN_X_LEFT, .yAlign = CLAY_ALIGN_Y_CENTER, .innerContent = [&](auto&) { phone_bottom_toolbar_gui(t); } }); } }); } } void DrawingProgramSelection::phone_bottom_toolbar_gui(PhoneDrawingProgramScreen& t) { using namespace GUIStuff; using namespace ElementHelpers; auto& gui = drawP.world.main.g.gui; gui.new_id("phone selection bottom toolbar", [&] { svg_icon_button(gui, "Copy button", "data/icons/RemixIcon/file-copy-line.svg", { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .onClick = [&] { selection_to_clipboard(); } }); svg_icon_button(gui, "Cut button", "data/icons/RemixIcon/scissors-line.svg", { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .onClick = [&] { selection_to_clipboard(); delete_all(); } }); svg_icon_button(gui, "Delete button", "data/icons/trash.svg", { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .onClick = [&] { delete_all(); } }); svg_icon_button(gui, "Bring to front of layer", "data/icons/RemixIcon/bring-to-front.svg", { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .onClick = [&] { push_selection_to_front(); } }); svg_icon_button(gui, "Send to back of layer", "data/icons/RemixIcon/send-to-back.svg", { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .onClick = [&] { push_selection_to_back(); } }); }); } void DrawingProgramSelection::update_selection_stroke_color() { if(strokeColorChangeData.oldColor != strokeColorChangeData.newColor) { for(auto& c : selectedSet) { if(c->obj->get_comp().get_stroke_color() != std::nullopt) { strokeColorChangeData.oldColorData.emplace(c->obj.get_net_id(), c->obj->get_comp().get_stroke_color().value()); c->obj->get_comp().change_stroke_color(strokeColorChangeData.newColor); c->obj->commit_update_dont_invalidate_cache(drawP); // Object isn't in the cache since it's selected c->obj->send_comp_update(drawP, false); } } strokeColorChangeData.oldColor = strokeColorChangeData.newColor; } } void DrawingProgramSelection::check_add_stroke_color_change_undo() { if(!strokeColorChangeData.oldColorData.empty()) { class EditCanvasComponentsStrokeColorWorldUndoAction : public WorldUndoAction { public: EditCanvasComponentsStrokeColorWorldUndoAction(std::vector initUndoIDs, std::vector initOldColors): undoIDs(std::move(initUndoIDs)), colors(std::move(initOldColors)) {} std::string get_name() const override { return "Edit Stroke Colors"; } bool undo(WorldUndoManager& undoMan) override { return undo_redo(undoMan); } bool redo(WorldUndoManager& undoMan) override { return undo_redo(undoMan); } bool undo_redo(WorldUndoManager& undoMan) { std::vector netIDs; if(!undoMan.fill_netid_list_from_undoid_list(netIDs, undoIDs)) return false; undoMan.world.netObjMan.send_multi_update_messsage([&]() { for(size_t i = 0; i < netIDs.size(); i++) { auto objPtr = undoMan.world.netObjMan.get_obj_temporary_ref_from_id(netIDs[i]); if(objPtr) { Vector4f oldColor = objPtr->get_comp().get_stroke_color().value(); objPtr->get_comp().change_stroke_color(colors[i]); colors[i] = oldColor; objPtr->commit_update(undoMan.world.drawProg); objPtr->send_comp_update(undoMan.world.drawProg, true); } } }, NetworkingObjects::NetObjManager::SendUpdateType::SEND_TO_ALL, nullptr); return true; } ~EditCanvasComponentsStrokeColorWorldUndoAction() {} std::vector undoIDs; std::vector colors; }; std::vector undoIDs; std::vector oldColors; for(auto& [netID, strokeColor] : strokeColorChangeData.oldColorData) { undoIDs.emplace_back(drawP.world.undo.get_undoid_from_netid(netID)); oldColors.emplace_back(strokeColor); } drawP.world.undo.push(std::make_unique(std::move(undoIDs), std::move(oldColors))); strokeColorChangeData.oldColorData.clear(); } } void DrawingProgramSelection::add_from_cam_coord_collider_to_selection(const SCollision::ColliderCollection& cC, DrawingProgramLayerManager::LayerSelector layerSelector, bool frontObjectOnly) { check_add_stroke_color_change_undo(); std::vector selectedComponents; if(frontObjectOnly) { CanvasComponentContainer::ObjInfo* a = drawP.drawCache.get_front_object_colliding_with_in_editing_layer(cC); if(a) { selectedComponents.emplace_back(a); drawP.drawCache.erase_component(a); } } else { auto cCWorld = drawP.world.drawData.cam.c.collider_to_world, SCollision::ColliderCollection>(cC); drawP.drawCache.traverse_bvh_run_function(cCWorld.bounds, erase_select_objects_in_bvh_func(selectedComponents, cC, cCWorld, layerSelector)); } add_to_selection(selectedComponents); } void DrawingProgramSelection::remove_from_cam_coord_collider_to_selection(const SCollision::ColliderCollection& cC, DrawingProgramLayerManager::LayerSelector layerSelector, bool frontObjectOnly) { check_add_stroke_color_change_undo(); if(frontObjectOnly) { CanvasComponentContainer::ObjInfo* p = get_front_object_colliding_with_in_editing_layer(cC); if(p) { drawP.drawCache.add_component(p); std::erase(selectedSet, p); } } else { auto cCWorld = drawP.world.drawData.cam.c.collider_to_world, SCollision::ColliderCollection>(cC); std::erase_if(selectedSet, [&](auto& c) { if(drawP.layerMan.component_passes_layer_selector(c, layerSelector) && c->obj->collides_with(drawP.world.drawData.cam.c, cCWorld, cC)) { drawP.drawCache.add_component(c); return true; } return false; }); } calculate_aabb(); } std::function&)> DrawingProgramSelection::erase_select_objects_in_bvh_func(std::vector& selectedComponents, const SCollision::ColliderCollection& cC, const SCollision::ColliderCollection& cCWorld, DrawingProgramLayerManager::LayerSelector layerSelector) { auto toRet = [&](const auto& bvhNode) { if(bvhNode && (bvhNode->coords.inverseScale << 9) < drawP.world.drawData.cam.c.inverseScale && SCollision::collide(cC, drawP.world.drawData.cam.c.to_space(bvhNode->bounds.min)) && SCollision::collide(cC, drawP.world.drawData.cam.c.to_space(bvhNode->bounds.max)) && SCollision::collide(cC, drawP.world.drawData.cam.c.to_space(bvhNode->bounds.top_right())) && SCollision::collide(cC, drawP.world.drawData.cam.c.to_space(bvhNode->bounds.bottom_left()))) { drawP.drawCache.invalidate_cache_at_aabb(bvhNode->bounds); drawP.drawCache.traverse_bvh_run_function_starting_at_node_no_collision_check(bvhNode, [&](const auto& bvhNodeChild) { drawP.drawCache.node_loop_erase_if_components(bvhNodeChild, [&](auto c) { if(drawP.layerMan.component_passes_layer_selector(c, drawP.controls.layerSelector)) { selectedComponents.emplace_back(c); return true; } return false; }); return true; }); return false; } drawP.drawCache.node_loop_erase_if_components(bvhNode, [&](auto c) { if(drawP.layerMan.component_passes_layer_selector(c, drawP.controls.layerSelector) && c->obj->collides_with(drawP.world.drawData.cam.c, cCWorld, cC)) { selectedComponents.emplace_back(c); drawP.drawCache.invalidate_cache_at_optional_aabb(c->obj->get_world_bounds()); return true; } return false; }); return true; }; return toRet; } void DrawingProgramSelection::add_to_selection(const std::vector& newSelection) { selectedSet.insert(selectedSet.end(), newSelection.begin(), newSelection.end()); sort_selection(); calculate_aabb(); } void DrawingProgramSelection::set_to_selection(const std::vector& newSelection) { selectedSet = newSelection; sort_selection(); calculate_aabb(); } void DrawingProgramSelection::sort_selection() { std::vector flattenedLayerList = drawP.layerMan.get_flattened_layer_list(); std::unordered_map flattenedLayerListOrder; for(size_t i = 0; i < flattenedLayerList.size(); i++) flattenedLayerListOrder[flattenedLayerList[i]] = flattenedLayerList.size() - 1 - i; std::sort(selectedSet.begin(), selectedSet.end(), [&](auto& a, auto& b) { return (flattenedLayerListOrder[a->obj->parentLayer] < flattenedLayerListOrder[b->obj->parentLayer]) || (a->obj->parentLayer == b->obj->parentLayer && a->pos < b->pos); }); } void DrawingProgramSelection::calculate_aabb() { if(is_something_selected()) { initialSelectionAABB = (*selectedSet.begin())->obj->get_world_bounds().value(); for(auto& c : selectedSet) initialSelectionAABB.include_aabb_in_bounds(c->obj->get_world_bounds().value()); rotateData.centerPos = initialSelectionAABB.center(); } } bool DrawingProgramSelection::is_something_selected() { return !selectedSet.empty(); } bool DrawingProgramSelection::is_selected(CanvasComponentContainer::ObjInfo* objToCheck) { return std::find(selectedSet.begin(), selectedSet.end(), objToCheck) != selectedSet.end(); } void DrawingProgramSelection::reset_all() { check_add_stroke_color_change_undo(); selectedSet.clear(); reset_transform_data(); strokeColorChangeData = StrokeColorChangeData(); } void DrawingProgramSelection::reset_transform_data() { check_add_stroke_color_change_undo(); selectionTransformCoords = CoordSpaceHelperTransform(); transformOpHappening = TransformOperation::NONE; translateData = TranslationData(); scaleData = ScaleData(); rotateData = RotationData(); } bool DrawingProgramSelection::mouse_collided_with_selection_aabb() { return SCollision::collide(camSpaceSelection, drawP.world.main.input.mouse.pos); } bool DrawingProgramSelection::mouse_collided_with_scale_point() { return SCollision::collide(SCollision::Circle(scaleData.handlePoint, drawP.drag_point_radius()), drawP.world.main.input.mouse.pos); } bool DrawingProgramSelection::mouse_collided_with_rotate_center_handle_point() { return SCollision::collide(SCollision::Circle(rotateData.centerHandlePoint, drawP.drag_point_radius()), drawP.world.main.input.mouse.pos); } bool DrawingProgramSelection::mouse_collided_with_rotate_handle_point() { return SCollision::collide(SCollision::Circle(rotateData.handlePoint, drawP.drag_point_radius() * ROTATION_POINT_RADIUS_MULTIPLIER), drawP.world.main.input.mouse.pos); } void DrawingProgramSelection::commit_transform_selection() { check_add_stroke_color_change_undo(); if(!is_empty_transform()) { std::vector undoIDList; std::vector> transformDataList; for(auto& comp : selectedSet) { undoIDList.emplace_back(drawP.world.undo.get_undoid_from_netid(comp->obj.get_net_id())); auto& transformData = transformDataList.emplace_back(); transformData.first = comp->obj->coords; comp->obj->coords = selectionTransformCoords.other_coord_space_from_this_space(comp->obj->coords); transformData.second = comp->obj->coords; comp->obj->commit_transform_dont_invalidate_cache(); } class TransformCanvasComponentsWorldUndoAction : public WorldUndoAction { public: TransformCanvasComponentsWorldUndoAction(std::vector initUndoIDs, std::vector> initTransformData): undoIDs(std::move(initUndoIDs)), transformData(std::move(initTransformData)) {} std::string get_name() const override { return "Transform Canvas Components"; } bool undo(WorldUndoManager& undoMan) override { std::vector toEditIDs; if(!undoMan.fill_netid_list_from_undoid_list(toEditIDs, undoIDs)) return false; std::vector transformSet; for(size_t i = 0; i < toEditIDs.size(); i++) { auto objPtr = undoMan.world.netObjMan.get_obj_temporary_ref_from_id(toEditIDs[i]); objPtr->coords = transformData[i].first; objPtr->commit_transform(undoMan.world.drawProg); transformSet.emplace_back(&(*objPtr->objInfo)); } undoMan.world.drawProg.send_transforms_for(transformSet); return true; } bool redo(WorldUndoManager& undoMan) override { std::vector toEditIDs; if(!undoMan.fill_netid_list_from_undoid_list(toEditIDs, undoIDs)) return false; std::vector transformSet; for(size_t i = 0; i < toEditIDs.size(); i++) { auto objPtr = undoMan.world.netObjMan.get_obj_temporary_ref_from_id(toEditIDs[i]); objPtr->coords = transformData[i].second; objPtr->commit_transform(undoMan.world.drawProg); transformSet.emplace_back(&(*objPtr->objInfo)); } undoMan.world.drawProg.send_transforms_for(transformSet); return true; } void scale_up(const WorldScalar& scaleUpAmount) override { for(auto& [oldTransform, newTransform] : transformData) { oldTransform.scale_about(WorldVec{0, 0}, scaleUpAmount, true); newTransform.scale_about(WorldVec{0, 0}, scaleUpAmount, true); } } ~TransformCanvasComponentsWorldUndoAction() {} std::vector undoIDs; std::vector> transformData; }; drawP.world.undo.push(std::make_unique(std::move(undoIDList), std::move(transformDataList))); } drawP.send_transforms_for(selectedSet); reset_transform_data(); calculate_aabb(); } void DrawingProgramSelection::update() { } void DrawingProgramSelection::input_key_callback_modify_selection(const InputManager::KeyCallbackArgs& key) { switch(key.key) { case InputManager::KEY_GENERIC_UP: { translate_key(InputManager::KEY_GENERIC_UP, key.down); break; } case InputManager::KEY_GENERIC_DOWN: { translate_key(InputManager::KEY_GENERIC_DOWN, key.down); break; } case InputManager::KEY_GENERIC_LEFT: { translate_key(InputManager::KEY_GENERIC_LEFT, key.down); break; } case InputManager::KEY_GENERIC_RIGHT: { translate_key(InputManager::KEY_GENERIC_RIGHT, key.down); break; } } } void DrawingProgramSelection::input_key_callback_display_selection(const InputManager::KeyCallbackArgs& key) { switch(key.key) { case InputManager::KEY_DRAW_DELETE: { if(key.down && !key.repeat) { delete_all(); drawP.world.main.g.gui.set_to_layout(); } break; } case InputManager::KEY_COPY: { if(key.down && !key.repeat) { selection_to_clipboard(); drawP.world.main.g.gui.set_to_layout(); } break; } case InputManager::KEY_CUT: { if(key.down && !key.repeat) { selection_to_clipboard(); delete_all(); drawP.world.main.g.gui.set_to_layout(); } break; } case InputManager::KEY_PASTE: { if(key.down && !key.repeat) { deselect_all(); paste_clipboard(drawP.world.main.input.mouse.pos); drawP.world.main.g.gui.set_to_layout(); } break; } 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(); } break; } case InputManager::KEY_DESELECT_AND_EDIT_TOOL: { if(key.down && !key.repeat) { if(is_something_selected()) deselect_all(); else drawP.switch_to_tool(DrawingProgramToolType::EDIT, true); drawP.world.main.g.gui.set_to_layout(); } break; } } } void DrawingProgramSelection::input_mouse_button_on_canvas_callback_modify_selection(const InputManager::MouseButtonCallbackArgs& button) { if(button.button == InputManager::MouseButton::LEFT && is_something_selected()) { switch(transformOpHappening) { case TransformOperation::NONE: { if(button.down && !drawP.world.main.input.key(InputManager::KEY_GENERIC_LSHIFT).held && !drawP.world.main.input.key(InputManager::KEY_GENERIC_LALT).held && !drawP.world.main.g.gui.cursor_obstructed()) { if(mouse_collided_with_scale_point()) { scaleData.currentPos = scaleData.startPos = selectionTransformCoords.from_space_world(initialSelectionAABB.max); scaleData.centerPos = selectionTransformCoords.from_space_world(initialSelectionAABB.center()); transformOpHappening = TransformOperation::SCALE; check_add_stroke_color_change_undo(); } else if(mouse_collided_with_rotate_center_handle_point()) { transformOpHappening = TransformOperation::ROTATE_RELOCATE_CENTER; check_add_stroke_color_change_undo(); } else if(mouse_collided_with_rotate_handle_point()) { rotateData.rotationAngle = 0.0; transformOpHappening = TransformOperation::ROTATE; check_add_stroke_color_change_undo(); } else if(mouse_collided_with_selection_aabb()) { translateData.startPos = drawP.world.drawData.cam.c.from_space(button.pos); transformOpHappening = TransformOperation::TRANSLATE; translateData.translateWithKeys = false; check_add_stroke_color_change_undo(); } } break; } case TransformOperation::TRANSLATE: case TransformOperation::ROTATE: case TransformOperation::SCALE: { if(!button.down) commit_transform_selection(); break; } case TransformOperation::ROTATE_RELOCATE_CENTER: { if(!button.down) transformOpHappening = TransformOperation::NONE; break; } } } } void DrawingProgramSelection::input_mouse_motion_callback_modify_selection(const InputManager::MouseMotionCallbackArgs& motion) { if(drawP.world.main.input.mouse.leftDown && is_something_selected()) { rebuild_cam_space(); switch(transformOpHappening) { case TransformOperation::NONE: break; case TransformOperation::TRANSLATE: { if(!translateData.translateWithKeys) selectionTransformCoords = CoordSpaceHelperTransform(drawP.world.drawData.cam.c.from_space(motion.pos) - translateData.startPos); break; } case TransformOperation::SCALE: { WorldVec centerToScaleStart = scaleData.startPos - scaleData.centerPos; Vector2f centerToScaleStartInCamSpace = drawP.world.drawData.cam.c.normalized_dir_to_space(centerToScaleStart.normalized()); Vector2f scaleCenterPointInCamSpace = drawP.world.drawData.cam.c.to_space(scaleData.centerPos); scaleData.currentPos = drawP.world.drawData.cam.c.from_space(project_point_on_line(motion.pos, scaleCenterPointInCamSpace, (scaleCenterPointInCamSpace + centerToScaleStartInCamSpace).eval())); WorldVec centerToScaleCurrent = scaleData.currentPos - scaleData.centerPos; WorldScalar centerToScaleStartNorm = centerToScaleStart.norm(); WorldScalar centerToScaleCurrentNorm = centerToScaleCurrent.norm(); bool isAnyNumberZero = centerToScaleCurrentNorm == WorldScalar(0) || centerToScaleStartNorm == WorldScalar(0); if(!isAnyNumberZero) { WorldMultiplier scaleAmount = WorldMultiplier(centerToScaleStartNorm) / WorldMultiplier(centerToScaleCurrentNorm); selectionTransformCoords = CoordSpaceHelperTransform(scaleData.centerPos, scaleAmount); } break; } case TransformOperation::ROTATE_RELOCATE_CENTER: { rotateData.centerPos = drawP.world.drawData.cam.c.from_space(motion.pos); break; } case TransformOperation::ROTATE: { Vector2f rotationPointDiff = drawP.world.main.input.mouse.pos - rotateData.centerHandlePoint; rotateData.rotationAngle = std::atan2(rotationPointDiff.y(), rotationPointDiff.x()); selectionTransformCoords = CoordSpaceHelperTransform(rotateData.centerPos, rotateData.rotationAngle); break; } } } } void DrawingProgramSelection::translate_key(unsigned keyPressed, bool pressed) { if(is_something_selected()) { if(pressed) { constexpr float KEY_TRANSLATE_MAGNITUDE = 5.0f; Vector2f moveVec; switch(keyPressed) { case InputManager::KEY_GENERIC_UP: moveVec = {0.0f, -1.0f}; break; case InputManager::KEY_GENERIC_DOWN: moveVec = {0.0f, 1.0f}; break; case InputManager::KEY_GENERIC_RIGHT: moveVec = {1.0f, 0.0f}; break; case InputManager::KEY_GENERIC_LEFT: moveVec = {-1.0f, 0.0f}; break; } moveVec *= KEY_TRANSLATE_MAGNITUDE; if(transformOpHappening == TransformOperation::NONE) { translateData.startPos = drawP.world.drawData.cam.c.dir_from_space(moveVec); translateData.translateWithKeys = true; transformOpHappening = TransformOperation::TRANSLATE; check_add_stroke_color_change_undo(); } else if(transformOpHappening == TransformOperation::TRANSLATE && translateData.translateWithKeys) { moveVec *= KEY_TRANSLATE_MAGNITUDE; translateData.startPos += drawP.world.drawData.cam.c.dir_from_space(moveVec); } selectionTransformCoords = CoordSpaceHelperTransform(translateData.startPos); } else if(transformOpHappening == TransformOperation::TRANSLATE && translateData.translateWithKeys) { bool anyKeyHeld = drawP.world.main.input.key(InputManager::KEY_GENERIC_LEFT).held || drawP.world.main.input.key(InputManager::KEY_GENERIC_RIGHT).held || drawP.world.main.input.key(InputManager::KEY_GENERIC_DOWN).held || drawP.world.main.input.key(InputManager::KEY_GENERIC_UP).held; if(!anyKeyHeld) { commit_transform_selection(); return; } } } } void DrawingProgramSelection::deselect_all() { check_add_stroke_color_change_undo(); if(is_something_selected()) { commit_transform_selection(); for(auto& obj : selectedSet) drawP.drawCache.add_component(obj); reset_all(); } } void DrawingProgramSelection::push_selection_to_front() { check_add_stroke_color_change_undo(); auto selectedVec = selectedSet; deselect_all(); drawP.layerMan.push_components_to(selectedVec, false); } void DrawingProgramSelection::push_selection_to_back() { check_add_stroke_color_change_undo(); auto selectedVec = selectedSet; deselect_all(); drawP.layerMan.push_components_to(selectedVec, true); } void DrawingProgramSelection::delete_all() { check_add_stroke_color_change_undo(); if(is_something_selected()) { auto selectedSetTemp = selectedSet; reset_all(); // Clear set before erasing, since calling erase_component_set will run a check to see if each object is selected and erase them one by one, which is slower drawP.layerMan.erase_component_container(selectedSetTemp); } } void DrawingProgramSelection::erase_component(CanvasComponentContainer::ObjInfo* objToCheck) { check_add_stroke_color_change_undo(); std::erase(selectedSet, objToCheck); if(selectedSet.empty()) reset_all(); } CanvasComponentContainer::ObjInfo* DrawingProgramSelection::get_front_object_colliding_with_in_editing_layer(const SCollision::ColliderCollection& cC) { auto cCWorld = drawP.world.drawData.cam.c.collider_to_world, SCollision::ColliderCollection>(cC); CanvasComponentContainer::ObjInfo* p = nullptr; for(auto& c : selectedSet) { if(drawP.layerMan.component_passes_layer_selector(c, DrawingProgramLayerManager::LayerSelector::LAYER_BEING_EDITED) && (!p || c->pos >= p->pos) && c->obj->collides_with(drawP.world.drawData.cam.c, cCWorld, cC)) p = c; } return p; } void DrawingProgramSelection::selection_to_clipboard() { check_add_stroke_color_change_undo(); if(is_something_selected()) { auto& clipboard = drawP.world.main.clipboard; std::unordered_set resourceSet; for(auto& c : selectedSet) c->obj->get_comp().get_used_resources(resourceSet); clipboard.components.clear(); for(auto& c : selectedSet) clipboard.components.emplace_back(c->obj->get_data_copy()); clipboard.pos = initialSelectionAABB.center(); clipboard.inverseScale = drawP.world.drawData.cam.c.inverseScale; clipboard.resources = drawP.world.rMan.copy_resource_set_to_map(resourceSet); } } void DrawingProgramSelection::paste_clipboard(Vector2f pasteScreenPos) { check_add_stroke_color_change_undo(); if(drawP.layerMan.is_a_layer_being_edited()) { auto& clipboard = drawP.world.main.clipboard; if(!drawP.is_selection_allowing_tool(drawP.drawTool->get_type())) drawP.switch_to_tool(DrawingProgramToolType::EDIT); std::vector> placedComponents; WorldVec mousePos = drawP.world.drawData.cam.c.from_space(pasteScreenPos); WorldVec moveVec = drawP.world.main.clipboard.pos - mousePos; WorldMultiplier scaleMultiplier = WorldMultiplier(drawP.world.main.clipboard.inverseScale) / WorldMultiplier(drawP.world.drawData.cam.c.inverseScale); WorldScalar scaleLimit(0.001); for(auto& c : drawP.world.main.clipboard.components) { if((c->coords.inverseScale / scaleMultiplier) < scaleLimit) { Logger::get().log(Logger::LogType::WORLDFATAL, "Some pasted objects will be too small! Scale up the canvas first (zoom in alot)"); return; } } std::unordered_map resourceRemapIDs; for(auto& r : clipboard.resources) resourceRemapIDs[r.first] = drawP.world.rMan.add_resource(r.second).get_net_id(); for(auto& c : clipboard.components) { CanvasComponentContainer* newComponentContainer = new CanvasComponentContainer(drawP.world.netObjMan, *c); newComponentContainer->get_comp().remap_resource_ids(resourceRemapIDs); newComponentContainer->coords.translate(-moveVec); newComponentContainer->coords.scale_about(mousePos, scaleMultiplier); placedComponents.emplace_back(drawP.layerMan.get_edited_layer_end_iterator(), newComponentContainer); } std::vector newlyInsertedObjectIts; drawP.layerMan.disable_add_to_cache_and_commit_update_block([&]() { newlyInsertedObjectIts = drawP.layerMan.add_many_components_to_layer_being_edited(placedComponents); }); std::vector compSetInserted; for(auto& it : newlyInsertedObjectIts) compSetInserted.emplace_back(&(*it)); parallel_loop_container(compSetInserted, [&drawP = drawP](CanvasComponentContainer::ObjInfo* comp) { comp->obj->commit_update_dont_invalidate_cache(drawP); }); set_to_selection(compSetInserted); } } void DrawingProgramSelection::paste_image_process_event(const CustomEvents::PasteEvent& paste) { check_add_stroke_color_change_undo(); if(drawP.layerMan.is_a_layer_being_edited()) { if(!drawP.is_selection_allowing_tool(drawP.drawTool->get_type())) drawP.switch_to_tool(DrawingProgramToolType::EDIT); std::vector compSetInserted; compSetInserted.emplace_back(drawP.add_file_to_canvas_by_data("Image from clipboard", paste.data, paste.mousePos.value())); if(compSetInserted.back() != nullptr) { drawP.drawCache.erase_component(compSetInserted.back()); set_to_selection(compSetInserted); } } } bool DrawingProgramSelection::is_being_transformed() { return transformOpHappening != TransformOperation::NONE; } bool DrawingProgramSelection::is_empty_transform() { return transformOpHappening == TransformOperation::NONE || selectionTransformCoords.is_identity(); } std::unordered_set DrawingProgramSelection::get_selection_as_set() { return std::unordered_set(selectedSet.begin(), selectedSet.end()); } void DrawingProgramSelection::rebuild_cam_space() { selectionRectPoints[0] = selectionTransformCoords.from_space_world(initialSelectionAABB.min); selectionRectPoints[1] = selectionTransformCoords.from_space_world(initialSelectionAABB.bottom_left()); selectionRectPoints[2] = selectionTransformCoords.from_space_world(initialSelectionAABB.max); selectionRectPoints[3] = selectionTransformCoords.from_space_world(initialSelectionAABB.top_right()); scaleData.handlePoint = drawP.world.drawData.cam.c.to_space(selectionRectPoints[2]); rotateData.centerHandlePoint = drawP.world.drawData.cam.c.to_space(rotateData.centerPos); rotateData.handlePoint = get_rotation_point_pos_from_angle(rotateData.rotationAngle); SCollision::ColliderCollection collideRectWorld; collideRectWorld.triangle.emplace_back(selectionRectPoints[0], selectionRectPoints[1], selectionRectPoints[2]); collideRectWorld.triangle.emplace_back(selectionRectPoints[0], selectionRectPoints[2], selectionRectPoints[3]); collideRectWorld.recalculate_bounds(); camSpaceSelection = drawP.world.drawData.cam.c.world_collider_to_coords>(collideRectWorld); } Vector2f DrawingProgramSelection::get_rotation_point_pos_from_angle(double angle) { return Vector2f{rotateData.centerHandlePoint + ROTATION_POINTS_DISTANCE * drawP.world.main.g.final_gui_scale() * Vector2f{std::cos(angle), std::sin(angle)}}; } void DrawingProgramSelection::draw_components(SkCanvas* canvas, const DrawData& drawData) { if(is_something_selected()) { rebuild_cam_space(); DrawData selectionDrawData = drawData; selectionDrawData.cam.c = selectionTransformCoords.other_coord_space_to_this_space(selectionDrawData.cam.c); selectionDrawData.cam.set_viewing_area(drawP.world.main.window.size.cast()); selectionDrawData.refresh_draw_optimizing_values(); for(auto& c : selectedSet) c->obj->draw(canvas, selectionDrawData); } } void DrawingProgramSelection::draw_gui(SkCanvas* canvas, const DrawData& drawData) { if(is_something_selected() && !camSpaceSelection.triangle.empty()) { SkPathBuilder selectionRectPath; selectionRectPath.moveTo(convert_vec2(camSpaceSelection.triangle[0].p[0])); selectionRectPath.lineTo(convert_vec2(camSpaceSelection.triangle[0].p[1])); selectionRectPath.lineTo(convert_vec2(camSpaceSelection.triangle[0].p[2])); selectionRectPath.lineTo(convert_vec2(camSpaceSelection.triangle[1].p[2])); selectionRectPath.close(); SkPaint p{SkColor4f{0.3f, 0.6f, 0.9f, 0.4f}}; canvas->drawPath(selectionRectPath.detach(), p); if(drawP.is_actual_selection_tool(drawP.drawTool->get_type())) { if(transformOpHappening == TransformOperation::NONE || transformOpHappening == TransformOperation::ROTATE || transformOpHappening == TransformOperation::ROTATE_RELOCATE_CENTER) drawP.draw_drag_circle(canvas, rotateData.centerHandlePoint, {0.9f, 0.5f, 0.1f, 1.0f}, drawData); if(transformOpHappening == TransformOperation::NONE || transformOpHappening == TransformOperation::ROTATE) drawP.draw_drag_circle(canvas, rotateData.handlePoint, {0.9f, 0.5f, 0.1f, 1.0f}, drawData, ROTATION_POINT_RADIUS_MULTIPLIER); if(transformOpHappening == TransformOperation::NONE || transformOpHappening == TransformOperation::SCALE) drawP.draw_drag_circle(canvas, scaleData.handlePoint, {0.1f, 0.9f, 0.9f, 1.0f}, drawData); } } }