diff --git a/about.md b/about.md index c127a6f..54b70c6 100644 --- a/about.md +++ b/about.md @@ -22,6 +22,6 @@ Pre-2.206 versions of BetterEdit relied on the now-defunct BetterSave mod to cre ## Support -BetterEdit is first and foremost **a passion project**, however it is also being developed by a poor student living on his own. If you would like to support development and help me out, I have a Ko-fi! +BetterEdit is first and foremost **a passion project**, however it is also being developed by a poor student living on his own. If you would like to support development and help me out, I have a Ko-fi! Supporters get Early Access to features. I also plan on adding extra advanced features for Supporters in the future! [Link to my Ko-fi](https://ko-fi.com/hjfod) diff --git a/changelog.md b/changelog.md index f34a1ea..79c9646 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # BetterEdit 6 +## v6.8.0-beta.1 + * Release candidate + ## v6.8.0-alpha.7 * Add Scale Snapping * Add Rotation Snapping and Lock Object Position During Rotation diff --git a/mod.json b/mod.json index 9435092..f316d46 100644 --- a/mod.json +++ b/mod.json @@ -1,6 +1,6 @@ { "geode": "3.7.1", - "version": "6.8.0-alpha.7", + "version": "6.8.0-beta.1", "gd": { "win": "2.206", "mac": "2.206", diff --git a/src/features/CopyToClipboard.cpp b/src/features/CopyToClipboard.cpp new file mode 100644 index 0000000..89070ba --- /dev/null +++ b/src/features/CopyToClipboard.cpp @@ -0,0 +1,44 @@ +#include +#include +#include + +using namespace geode::prelude; + +static bool isProbablyObjectString(std::string_view str) { + // check if it starts with [0-9]+, + size_t ix = 0; + for (auto c : str) { + if (!(c == ',' && ix > 0) && !(c >= '0' && c <= '9')) { + return false; + } + if (c == ',' && ix > 0) { + return true; + } + ix += 1; + } + return false; +} + +class $modify(EditorUI) { + // Hook these instead of copyObjects and pasteObjects so something like + // onDuplicate doesn't get overwritten + void doCopyObjects(bool idk) { + EditorUI::doCopyObjects(idk); + if (Mod::get()->template getSettingValue("copy-paste-from-clipboard")) { + clipboard::write(GameManager::get()->m_editorClipboard); + } + } + void doPasteObjects(bool idk) { + if ( + Mod::get()->template getSettingValue("copy-paste-from-clipboard") && + isProbablyObjectString(clipboard::read()) + ) { + GameManager::get()->m_editorClipboard = clipboard::read(); + EditorUI::doPasteObjects(idk); + Notification::create("Pasted Objects from Clipboard", NotificationIcon::Info)->show(); + } + else { + EditorUI::doPasteObjects(idk); + } + } +}; diff --git a/src/features/EditMixedValues.cpp b/src/features/EditMixedValues.cpp new file mode 100644 index 0000000..3b34e70 --- /dev/null +++ b/src/features/EditMixedValues.cpp @@ -0,0 +1,270 @@ +#include +#include + +using namespace geode::prelude; + +template +class MixedValuesHandler final { +protected: + std::vector m_targets; + typename T::Type m_mixedSource; + CCTextInputNode* m_input; + +public: + void setup(GameObject* obj, CCArray* objs, CCTextInputNode* input) { + m_input = input; + m_targets.clear(); + if (obj) { + if constexpr (std::is_same_v) { + m_targets.push_back(obj); + } + else { + if (auto o = typeinfo_cast(obj)) { + m_targets.push_back(o); + } + } + } + for (auto o : CCArrayExt(objs)) { + if constexpr (std::is_same_v) { + m_targets.push_back(o); + } + else { + if (auto e = typeinfo_cast(o)) { + m_targets.push_back(e); + } + } + } + if (!m_targets.empty()) { + m_mixedSource = T::get(m_targets.front()); + } + // Disable input until explicitly unmixed via button + // This is because A) i'm too lazy to parse "Mix+N" strings and + // B) this makes it clear how to unmix + if (this->isMixed()) { + input->setMouseEnabled(false); + input->setTouchEnabled(false); + + auto unmixSpr = ButtonSprite::create("Unmix", "goldFont.fnt", "GJ_button_05.png", .8f); + unmixSpr->setScale(.3f); + auto unmixBtn = CCMenuItemExt::createSpriteExtra( + unmixSpr, [this](auto) { + this->override(T::DEFAULT_VALUE); + } + ); + auto menu = CCMenu::create(); + menu->setID("unmix-menu"_spr); + menu->ignoreAnchorPointForPosition(false); + menu->setContentSize({ 25, 15 }); + menu->addChildAtPosition(unmixBtn, Anchor::Center); + input->getParent()->addChildAtPosition(menu, Anchor::Bottom, ccp(0, 0), false); + } + this->updateLabel(); + } + bool isMixed() const { + if (m_targets.empty()) { + return false; + } + auto value = T::get(m_targets.front()); + for (auto obj : m_targets) { + if (T::get(obj) != value) { + return true; + } + } + return false; + } + void override(typename T::Type value) const { + m_input->setMouseEnabled(true); + m_input->setTouchEnabled(true); + m_input->getParent()->removeChildByID("unmix-menu"_spr); + + for (auto obj : m_targets) { + T::set(obj, clamp(value, T::MIN_VALUE, T::MAX_VALUE)); + } + this->updateLabel(); + } + void parse(std::string const& value) const { + this->override(numFromString(value).unwrapOr(0)); + } + void increment(typename T::Type value) const { + for (auto obj : m_targets) { + auto val = clamp(T::get(obj) + value, T::MIN_VALUE, T::MAX_VALUE); + if (T::SKIP_ZERO && val == 0) { + val = value > 0 ? 1 : -1; + } + T::set(obj, val); + } + this->updateLabel(); + } + void nextFree() const { + std::set usedLayers; + for (auto obj : m_targets) { + usedLayers.insert(T::get(obj)); + } + typename T::Type nextFree; + for (nextFree = T::MIN_VALUE; nextFree < T::MAX_VALUE; nextFree += 1) { + if (!usedLayers.contains(nextFree)) { + break; + } + } + this->override(nextFree); + } + void updateLabel() const { + if (m_targets.empty()) return; + if (this->isMixed()) { + auto value = T::get(m_targets.front()) - m_mixedSource; + m_input->setString(value == 0 ? "Mixed" : fmt::format("Mix{:+}", value)); + } + else { + m_input->setString(fmt::format("{}", T::get(m_targets.front()))); + } + } +}; + +#define MIXABLE_GAME_OBJECT_PROP(name_, gty_, ty_, val_, min_, max_, def_, skip_zero_, prop_) \ + struct name_ final { \ + using Type = ty_; \ + using GameObjectType = gty_; \ + static constexpr Type MIXED_VALUE = val_; \ + static constexpr Type MIN_VALUE = min_; \ + static constexpr Type MAX_VALUE = max_; \ + static constexpr Type DEFAULT_VALUE = def_; \ + static constexpr bool SKIP_ZERO = skip_zero_; \ + static Type get(GameObjectType* obj) { return obj->prop_; } \ + static void set(GameObjectType* obj, Type value) { obj->prop_ = value; } \ + }; + +MIXABLE_GAME_OBJECT_PROP(GOEL, GameObject, short, -1, 0, std::numeric_limits::max(), 0, false, m_editorLayer); +MIXABLE_GAME_OBJECT_PROP(GOEL2, GameObject, short, -1, 0, std::numeric_limits::max(), 0, false, m_editorLayer2); +MIXABLE_GAME_OBJECT_PROP(GOZO, GameObject, int, -1000, -999, 999, 2, true, m_zOrder); +MIXABLE_GAME_OBJECT_PROP(EGOOV, EffectGameObject, int, -1, 0, std::numeric_limits::max(), 0, false, m_ordValue); +MIXABLE_GAME_OBJECT_PROP(EGOCV, EffectGameObject, int, -1, 0, std::numeric_limits::max(), 0, false, m_channelValue); + +class $modify(SetGroupIDLayer) { + struct Fields { + MixedValuesHandler editorLayerHandler; + MixedValuesHandler editorLayer2Handler; + MixedValuesHandler zOrderHandler; + MixedValuesHandler channelOrderHandler; + MixedValuesHandler channelHandler; + }; + + $override + bool init(GameObject* obj, CCArray* objs) { + if (!SetGroupIDLayer::init(obj, objs)) + return false; + + m_fields->editorLayerHandler.setup(obj, objs, m_editorLayerInput); + m_fields->editorLayer2Handler.setup(obj, objs, m_editorLayer2Input); + m_fields->zOrderHandler.setup(obj, objs, m_zOrderInput); + if (m_orderInput) m_fields->channelOrderHandler.setup(obj, objs, m_orderInput); + if (m_channelInput) m_fields->channelHandler.setup(obj, objs, m_channelInput); + + return true; + } + + $override + void onArrow(int tag, int increment) { + if (tag == m_editorLayerInput->getTag()) { + m_fields->editorLayerHandler.increment(increment); + } + else if (tag == m_editorLayer2Input->getTag()) { + m_fields->editorLayer2Handler.increment(increment); + } + else if (tag == m_zOrderInput->getTag()) { + m_fields->zOrderHandler.increment(increment); + } + else if (m_orderInput && tag == m_orderInput->getTag()) { + m_fields->channelOrderHandler.increment(increment); + } + else if (m_channelInput && tag == m_channelInput->getTag()) { + m_fields->channelHandler.increment(increment); + } + else { + SetGroupIDLayer::onArrow(tag, increment); + } + } + + $override + void onNextFreeEditorLayer1(CCObject* sender) { + if (0) SetGroupIDLayer::onNextFreeEditorLayer1(sender); + m_fields->editorLayerHandler.nextFree(); + } + $override + void onNextFreeEditorLayer2(CCObject* sender) { + if (0) SetGroupIDLayer::onNextFreeEditorLayer2(sender); + m_fields->editorLayerHandler.nextFree(); + } + $override + void onNextFreeOrderChannel(CCObject* sender) { + if (0) SetGroupIDLayer::onNextFreeOrderChannel(sender); + m_fields->channelOrderHandler.nextFree(); + } + + $override + void updateEditorLayerID() { + if (0) SetGroupIDLayer::updateEditorLayerID(); + } + $override + void updateEditorLabel() { + if (0) SetGroupIDLayer::updateEditorLabel(); + } + $override + void updateEditorLayerID2() { + if (0) SetGroupIDLayer::updateEditorLayerID2(); + } + $override + void updateEditorLabel2() { + if (0) SetGroupIDLayer::updateEditorLabel2(); + } + $override + void updateZOrder() { + if (0) SetGroupIDLayer::updateZOrder(); + } + $override + void updateZOrderLabel() { + if (0) SetGroupIDLayer::updateZOrderLabel(); + } + $override + void updateOrderChannel() { + if (0) SetGroupIDLayer::updateOrderChannel(); + } + $override + void updateOrderChannelLabel() { + if (0) SetGroupIDLayer::updateOrderChannelLabel(); + } + $override + void updateEditorOrder() { + if (0) SetGroupIDLayer::updateEditorOrder(); + } + $override + void updateEditorOrderLabel() { + if (0) SetGroupIDLayer::updateEditorOrderLabel(); + } + + // Skip textChanged resetting the value to 0 if the input string is 'Mixed' + $override + virtual void textChanged(CCTextInputNode* input) { + auto str = std::string(input->getString()); + if (str.size() && (str[0] == 'm' || str[0] == 'M')) { + return; + } + if (input == m_editorLayerInput) { + m_fields->editorLayerHandler.parse(input->getString()); + } + else if (input == m_editorLayer2Input) { + m_fields->editorLayer2Handler.parse(input->getString()); + } + else if (input == m_zOrderInput) { + m_fields->zOrderHandler.parse(input->getString()); + } + else if (input == m_orderInput) { + m_fields->channelOrderHandler.parse(input->getString()); + } + else if (input == m_channelInput) { + m_fields->channelHandler.parse(input->getString()); + } + else { + SetGroupIDLayer::textChanged(input); + } + } +}; diff --git a/src/features/HSVPreview.cpp b/src/features/HSVPreview.cpp new file mode 100644 index 0000000..606532b --- /dev/null +++ b/src/features/HSVPreview.cpp @@ -0,0 +1,124 @@ +#include +#include +#include +#include + +using namespace geode::prelude; + +static ccColor3B getRealizedColor(int channelID, size_t depth = 0) { + // Just in case there's a circular color channel dependency + if (depth > 10) { + return ccWHITE; + } + auto channel = LevelEditorLayer::get()->m_levelSettings->m_effectManager->getColorAction(channelID); + if (!channel) { + return ccWHITE; + } + if (channel->m_copyID) { + return GameToolbox::transformColor(getRealizedColor(channel->m_copyID, depth + 1), channel->m_copyHSV); + } + return channel->m_fromColor; +} + +class $modify(HSVWidgetWithPreview, ConfigureHSVWidget) { + struct Fields { + struct ColorPreview final { + CCSprite* origColor; + CCSprite* destColor; + // function so it can be automatically resourced if it changes + std::function srcChannel; + }; + std::optional preview; + }; + + $override + void updateLabels() { + ConfigureHSVWidget::updateLabels(); + if (m_fields->preview) { + auto prev = *m_fields->preview; + auto color = getRealizedColor(prev.srcChannel()); + prev.origColor->setColor(color); + prev.destColor->setColor(GameToolbox::transformColor(color, m_hsv)); + } + } + + void setSourceChannel(std::function channel) { + auto orig = static_cast(this->getChildByID("original-color"_spr)); + if (!orig) { + orig = CCSprite::createWithSpriteFrameName("whiteSquare60_001.png"); + orig->setID("original-color"_spr); + orig->setPosition(ccp(-120, 15 * .6f)); // no ids or layouts,..... + orig->setScale(.6f); + this->addChild(orig); + } + auto dest = static_cast(this->getChildByID("destination-color"_spr)); + if (!dest) { + dest = CCSprite::createWithSpriteFrameName("whiteSquare60_001.png"); + dest->setID("destination-color"_spr); + dest->setPosition(ccp(-120, -15 * .6f)); + dest->setScale(.6f); + this->addChild(dest); + } + m_fields->preview.emplace(Fields::ColorPreview { + .origColor = orig, + .destColor = dest, + .srcChannel = channel, + }); + this->updateLabels(); + } +}; + +static std::optional HSV_SOURCE_COLOR; +class $modify(CustomizeObjectLayer) { + $override + void onHSV(CCObject* sender) { + HSV_SOURCE_COLOR = this->getActiveMode(true); + CustomizeObjectLayer::onHSV(sender); + } +}; +class $modify(HSVWidgetPopup) { + $override + bool init(ccHSVValue hsv, HSVWidgetDelegate* delegate, gd::string p2) { + if (!HSVWidgetPopup::init(hsv, delegate, p2)) + return false; + + if (HSV_SOURCE_COLOR && m_widget) { + static_cast(m_widget)->setSourceChannel([c = *HSV_SOURCE_COLOR] { + return c; + }); + HSV_SOURCE_COLOR = std::nullopt; + } + + return true; + } +}; +class $modify(ColorSelectPopup) { + $override + bool init(EffectGameObject* obj, CCArray* objs, ColorAction* action) { + if (!ColorSelectPopup::init(obj, objs, action)) + return false; + + if (m_hsvWidget) { + if (obj) { + static_cast(m_hsvWidget)->setSourceChannel([obj] { + return obj->m_copyColorID; + }); + } + else if (action) { + static_cast(m_hsvWidget)->setSourceChannel([action] { + return action->m_colorID; + }); + } + } + + return true; + } + + $override + void onUpdateCopyColor(CCObject* sender) { + ColorSelectPopup::onUpdateCopyColor(sender); + if (m_hsvWidget) { + static_cast(m_hsvWidget)->updateLabels(); + } + } +}; diff --git a/src/features/ImprovedScaleAndRotate.cpp b/src/features/ImprovedScaleAndRotate.cpp new file mode 100644 index 0000000..8d4fe39 --- /dev/null +++ b/src/features/ImprovedScaleAndRotate.cpp @@ -0,0 +1,444 @@ +#include +#include +#include +#include +#include + +using namespace geode::prelude; + +// i hate touch prioi hate touch prioi hate touch prioi hate touch prioi hate +// touch prioi hate touch prioi hate touch prioi hate touch prioi hate touch +// aka fix that will make you audibly say "kill yourself" +class $modify(SuperExtraEvenMoreForcePrioUI, EditorUI) { + struct Fields { + std::unordered_set forceTouchPrio; + }; + + $override + bool ccTouchBegan(CCTouch* touch, CCEvent* event) { + for (auto input : m_fields->forceTouchPrio) { + if (input->isVisible() && CCRect( + input->getPosition() - input->getScaledContentSize() / 2, + input->getScaledContentSize() + ).containsPoint(input->getParent()->convertTouchToNodeSpace(touch))) { + return input->ccTouchBegan(touch, event); + } + } + return EditorUI::ccTouchBegan(touch, event); + } +}; + +// Returns radians +static float angleOfPointOnCircle(CCPoint const& point) { + return atan2f(point.y, point.x) * (180.f / std::numbers::pi_v); +} +static CCPoint pointOnCircle(float degrees, float radius) { + return ccp( + cosf(degrees * std::numbers::pi_v / 180.f) * radius, + sinf(degrees * std::numbers::pi_v / 180.f) * radius + ); +} + +static void labelLockSpr(CCNode* spr, const char* text) { + auto labelOff = CCLabelBMFont::create(text, "bigFont.fnt"); + labelOff->setScale(.2f); + spr->addChildAtPosition(labelOff, Anchor::Bottom); +} +static void labelLockToggle(CCMenuItemToggler* toggle, const char* text) { + labelLockSpr(toggle->m_offButton, text); + labelLockSpr(toggle->m_onButton, text); +} + +class ScaleControlSnapLines : public CCNode { +protected: + bool init() { + if (!CCNode::init()) + return false; + + this->setID("snap-lines"_spr); + this->setContentSize(ccp(210, 11)); + this->setAnchorPoint(ccp(.5f, .5f)); + + // Smaller slider option + // The conditionals here are horrendous but eh I was lazy + bool big = GameManager::get()->getGameVariable("0112"); + for (size_t i = (big ? 2 : 3); i < (big ? 16 : 8); i += 1) { + auto x = m_obContentSize.width / (big ? 15 : 6) * (i - (big ? 1 : 2)); + auto spr = CCSprite::createWithSpriteFrameName( + (i % 4) ? "slider-tick-small.png"_spr : "slider-tick.png"_spr + ); + spr->setOpacity((i % 4) ? 105 : 255); + this->addChildAtPosition(spr, Anchor::Left, ccp(x, 0)); + } + + return true; + } + +public: + static ScaleControlSnapLines* create() { + auto ret = new ScaleControlSnapLines(); + if (ret && ret->init()) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; + } +}; + +class $modify(GJScaleControl) { + $override + bool init() { + if (!GJScaleControl::init()) + return false; + + m_scaleXLabel->setPositionX(-30); + m_scaleYLabel->setPositionX(-30); + m_scaleLabel->setPositionX(-25); + + auto inputX = TextInput::create(50, "Num"); + inputX->setScale(.8f); + inputX->setID("input-x"_spr); + inputX->setPosition(30, m_scaleXLabel->getPositionY()); + inputX->setCommonFilter(CommonFilter::Float); + inputX->setCallback([this, inputX](auto const& str) { + if (auto scale = numFromString(str)) { + m_delegate->scaleXChanged(*scale, m_scaleLocked); + m_sliderX->setValue(this->valueFromScale(*scale)); + } + }); + this->addChild(inputX); + + auto inputY = TextInput::create(50, "Num"); + inputY->setScale(.8f); + inputY->setID("input-y"_spr); + inputY->setPosition(30, m_scaleYLabel->getPositionY()); + inputY->setCommonFilter(CommonFilter::Float); + inputY->setCallback([this, inputY](auto const& str) { + if (auto scale = numFromString(str)) { + m_delegate->scaleYChanged(*scale, m_scaleLocked); + m_sliderY->setValue(this->valueFromScale(*scale)); + } + }); + this->addChild(inputY); + + auto inputXY = TextInput::create(50, "Num"); + inputXY->setScale(.8f); + inputXY->setID("input-xy"_spr); + inputXY->setPosition(25, m_scaleLabel->getPositionY()); + inputXY->setCommonFilter(CommonFilter::Float); + inputXY->setCallback([this, inputXY](auto const& str) { + if (auto scale = numFromString(str)) { + m_delegate->scaleXYChanged(*scale, *scale, m_scaleLocked); + m_sliderXY->setValue(this->valueFromScale(*scale)); + } + }); + this->addChild(inputXY); + + auto menu = m_scaleLockButton->getParent(); + menu->setID("lock-menu"_spr); + menu->setContentWidth(50); + + labelLockSpr(m_scaleLockButton->getNormalImage(), "Pos"); + + auto snapBtn = CCMenuItemToggler::create( + CCSprite::createWithSpriteFrameName("warpLockOffBtn_001.png"), + CCSprite::createWithSpriteFrameName("warpLockOnBtn_001.png"), + this, nullptr + ); + labelLockToggle(snapBtn, "Snap"); + snapBtn->setID("snap-lock"_spr); + menu->addChild(snapBtn); + + menu->setLayout(RowLayout::create()); + + m_sliderX->addChild(ScaleControlSnapLines::create()); + m_sliderX->m_touchLogic->setZOrder(1); + m_sliderY->addChild(ScaleControlSnapLines::create()); + m_sliderY->m_touchLogic->setZOrder(1); + m_sliderXY->addChild(ScaleControlSnapLines::create()); + m_sliderXY->m_touchLogic->setZOrder(1); + + return true; + } + + $override + void loadValues(GameObject* obj, CCArray* objs, gd::unordered_map& states) { + GJScaleControl::loadValues(obj, objs, states); + + this->updateInput(this->getInputX()); + this->updateInput(this->getInputY()); + this->updateInput(this->getInputXY()); + + auto ui = static_cast(m_delegate); + ui->m_fields->forceTouchPrio.insert(this->getInputX()->getInputNode()); + ui->m_fields->forceTouchPrio.insert(this->getInputY()->getInputNode()); + ui->m_fields->forceTouchPrio.insert(this->getInputXY()->getInputNode()); + } + $override + void updateLabelX(float scale) { + GJScaleControl::updateLabelX(scale); + this->updateInput(this->getInputX()); + } + $override + void updateLabelY(float scale) { + GJScaleControl::updateLabelY(scale); + this->updateInput(this->getInputY()); + } + $override + void updateLabelXY(float scale) { + GJScaleControl::updateLabelXY(scale); + this->updateInput(this->getInputXY()); + } + + // Why is m_scaleLockButton not a CCMenuItemToggler?? + $override + void onToggleLockScale(CCObject* sender) { + GJScaleControl::onToggleLockScale(sender); + labelLockSpr(m_scaleLockButton->getNormalImage(), "Pos"); + } + + $override + void ccTouchMoved(CCTouch* touch, CCEvent* event) { + // Call original without the delegate so it doesn't fire an unnecessary + // angleChanged event + auto delegate = m_delegate; + m_delegate = nullptr; + GJScaleControl::ccTouchMoved(touch, event); + m_delegate = delegate; + + this->getInputX()->defocus(); + this->getInputY()->defocus(); + this->getInputXY()->defocus(); + + auto scaleX = this->scaleFromValue(m_sliderX->getThumb()->getValue()); + auto scaleY = this->scaleFromValue(m_sliderY->getThumb()->getValue()); + auto ratio = scaleY / scaleX; + if (auto lock = this->getSnapLock(); lock && lock->isToggled()) { + scaleX = roundf(scaleX / .25f) * .25f; + scaleY = roundf(scaleY / .25f) * .25f; + } + + if (m_scaleButtonType == 0) { + m_delegate->scaleXChanged(scaleX, m_scaleLocked); + m_sliderX->setValue(this->valueFromScale(scaleX)); + this->getInputX()->setString(numToString(scaleX, 3)); + } + else if (m_scaleButtonType == 1) { + m_delegate->scaleYChanged(scaleY, m_scaleLocked); + m_sliderY->setValue(this->valueFromScale(scaleY)); + this->getInputY()->setString(numToString(scaleY, 3)); + } + else { + float scale = scaleX; + if (scaleX < scaleY) { + scale = scaleY; + m_delegate->scaleXYChanged(scaleY / ratio, scaleY, m_scaleLocked); + } + else { + m_delegate->scaleXYChanged(scaleX, scaleX * ratio, m_scaleLocked); + } + m_sliderXY->setValue(this->valueFromScale(scale)); + this->getInputXY()->setString(numToString(scale, 3)); + } + } + + void updateInput(TextInput* input) { + CCLabelBMFont* label; + Slider* slider; + const char* text; + if (input->getID() == "input-x"_spr) { + label = m_scaleXLabel; + slider = m_sliderX; + text = "Scale X:"; + } + else if (input->getID() == "input-y"_spr) { + label = m_scaleYLabel; + slider = m_sliderY; + text = "Scale Y:"; + } + else { + label = m_scaleLabel; + slider = m_sliderXY; + text = "Scale:"; + } + input->setVisible(label->isVisible()); + if (label->isVisible()) { + label->setString(text); + label->setScale(.5f); + input->setString(numToString(this->scaleFromValue(slider->getThumb()->getValue()))); + } + } + + TextInput* getInputX() { + return static_cast(this->getChildByID("input-x"_spr)); + } + TextInput* getInputY() { + return static_cast(this->getChildByID("input-y"_spr)); + } + TextInput* getInputXY() { + return static_cast(this->getChildByID("input-xy"_spr)); + } + CCMenuItemToggler* getSnapLock() { + return static_cast(this->querySelector( + "hjfod.betteredit/lock-menu hjfod.betteredit/snap-lock" + )); + } +}; + +class $modify(InputRotationControl, GJRotationControl) { + $override + bool init() { + if (!GJRotationControl::init()) + return false; + + // todo: you can place blocks through these + auto input = TextInput::create(50, "Num"); + input->setScale(.8f); + input->setID("input-angle"_spr); + input->setPosition(110, 0); + input->setCommonFilter(CommonFilter::Float); + input->setCallback([this, input](auto const& str) { + if (auto angle = numFromString(str)) { + m_delegate->angleChangeBegin(); + m_delegate->angleChanged(*angle); + m_delegate->angleChangeEnded(); + this->setThumbValue(*angle); + } + }); + this->addChild(input); + + auto menu = CCMenu::create(); + menu->setContentWidth(50); + menu->setID("lock-menu"_spr); + + auto posBtn = CCMenuItemToggler::create( + CCSprite::createWithSpriteFrameName("warpLockOffBtn_001.png"), + CCSprite::createWithSpriteFrameName("warpLockOnBtn_001.png"), + this, nullptr + ); + labelLockToggle(posBtn, "Pos"); + posBtn->setID("pos-lock"_spr); + menu->addChild(posBtn); + + auto snapBtn = CCMenuItemToggler::create( + CCSprite::createWithSpriteFrameName("warpLockOffBtn_001.png"), + CCSprite::createWithSpriteFrameName("warpLockOnBtn_001.png"), + this, nullptr + ); + labelLockToggle(snapBtn, "Snap"); + snapBtn->setID("snap-lock"_spr); + menu->addChild(snapBtn); + + menu->setLayout(RowLayout::create()); + menu->setPosition(110, 35); + this->addChild(menu); + + return true; + } + + $override + void draw() { + GJRotationControl::draw(); + // Draw large ticks every 45° and small ticks every 15° + for (size_t i = 0; i < 24; i += 1) { + glLineWidth((i % 3) ? 1 : 2); + float len = (i % 3) ? 2.5f : 5; + float angle = i * 15; + ccDrawLine(pointOnCircle(angle, 60 - len), pointOnCircle(angle, 60 + len)); + } + } + + $override + void ccTouchMoved(CCTouch* touch, CCEvent* event) { + // Call original without the delegate so it doesn't fire an unnecessary + // angleChanged event + auto delegate = m_delegate; + m_delegate = nullptr; + GJRotationControl::ccTouchMoved(touch, event); + m_delegate = delegate; + + auto input = this->getInput(); + input->defocus(); + + auto angle = this->getThumbValue(); + if (auto lock = this->getSnapLock(); lock && lock->isToggled()) { + angle = roundf(this->getThumbValue() / 15) * 15; + m_controlSprite->setPosition(pointOnCircle(-angle + 90, 60)); + } + m_delegate->angleChanged(angle); + input->setString(numToString(angle, 3)); + } + + void myLoadValues(std::vector const& objs) { + if (objs.empty()) return; + auto angle = objs.front()->getRotation(); + this->setThumbValue(angle); + this->getInput()->setString(numToString(angle, 3)); + + auto ui = static_cast(m_delegate); + ui->m_fields->forceTouchPrio.insert(this->getInput()->getInputNode()); + } + + float getThumbValue() const { + return -angleOfPointOnCircle(m_controlPosition) + 90; + } + void setThumbValue(float value) { + value = -value + 90; + m_controlPosition = pointOnCircle(value, 60); + m_controlSprite->setPosition(m_controlPosition); + } + + TextInput* getInput() { + return static_cast(this->getChildByID("input-angle"_spr)); + } + CCMenuItemToggler* getSnapLock() { + return static_cast(this->querySelector( + "hjfod.betteredit/lock-menu hjfod.betteredit/snap-lock" + )); + } + CCMenuItemToggler* getPosLock() { + return static_cast(this->querySelector( + "hjfod.betteredit/lock-menu hjfod.betteredit/pos-lock" + )); + } +}; + +class $modify(EditorUI) { + struct Fields { + bool lockRotationPosition = false; + }; + + $override + void activateRotationControl(CCObject* sender) { + EditorUI::activateRotationControl(sender); + static_cast(m_rotationControl)->myLoadValues(::getSelectedObjects(this)); + } + // Make angleChanged be absolute rotation instead of relative + $override + void angleChanged(float angle) { + if (0) { return EditorUI::angleChanged(angle); } + CCArray* objs = nullptr; + bool lockPos = static_cast(m_rotationControl)->getPosLock()->isToggled(); + if (m_selectedObjects && m_selectedObjects->count()) { + objs = m_selectedObjects; + } + else if (m_selectedObject) { + objs = CCArray::createWithObject(m_selectedObject); + m_pivotPoint = m_selectedObject->getPosition(); + } + if (objs) { + auto orig = static_cast(objs->firstObject())->getRotation(); + m_fields->lockRotationPosition = lockPos; + this->rotateObjects(objs, -orig + angle, m_pivotPoint); + m_fields->lockRotationPosition = false; + } + } + + $override + void moveObject(GameObject* obj, CCPoint amount) { + if (!m_fields->lockRotationPosition) { + EditorUI::moveObject(obj, amount); + } + } +}; diff --git a/support.md b/support.md index 641c7b0..e862764 100644 --- a/support.md +++ b/support.md @@ -3,9 +3,7 @@ BetterEdit is first and foremost a passion project, but if you would lik [Link to my Ko-fi](https://ko-fi.com/hjfod) -I'm also planning on making a paid Pro version in the future with some cool extra features :) - -However, please don't donate just for the purpose of getting Pro! While I fully intended to make it, I know from past experience that something might come in the way, and I do not want to receive money for a product until I can guarantee it exists. +Supporters get Early Access to features. I also plan on adding extra advanced features for Supporters! ## Do not donate any money you need yourself.