/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 1998-2000, Matthes Bender * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2009-2016, The OpenClonk Team and contributors * * Distributed under the terms of the ISC license; see accompanying file * "COPYING" for details. * * "Clonk" is a registered trademark of Matthes Bender, used with permission. * See accompanying file "TRADEMARK" for details. * * To redistribute this file separately, substitute the full license texts * for the above references. */ /* Handles viewport editing in console mode */ #include "C4Include.h" #include "editor/C4EditCursor.h" #include "editor/C4Console.h" #include "object/C4Def.h" #include "object/C4Object.h" #include "game/C4Application.h" #include "lib/C4Random.h" #include "gui/C4MouseControl.h" #include "landscape/C4Landscape.h" #include "landscape/C4Texture.h" #include "graphics/C4GraphicsResource.h" #include "game/C4Game.h" #include "object/C4GameObjects.h" #include "control/C4GameControl.h" #include "script/C4AulExec.h" #ifdef WITH_QT_EDITOR #include "editor/C4ConsoleQtShapes.h" #endif #include "lib/StdMesh.h" #ifdef _WIN32 #include "res/resource.h" #endif StdStrBuf C4EditCursorSelection::GetDataString() const { StdStrBuf Output; // Compose info text by selected object(s) int32_t obj_count = size(); switch (obj_count) { // No selection case 0: Output = LoadResStr("IDS_CNS_NOOBJECT"); break; // One selected object case 1: { C4Object *obj = GetObject(); if (obj) Output.Take(obj->GetDataString()); else Output.Take(front().GetDataString()); break; } // Multiple selected objects default: Output.Format(LoadResStr("IDS_CNS_MULTIPLEOBJECTS"), obj_count); break; } return Output; } C4Object *C4EditCursorSelection::GetObject(int32_t index) const { // Get indexed C4Object * in list C4Object *obj; for (const C4Value &v : (*this)) if ((obj = v.getObj())) if (!index--) return obj; return nullptr; } C4Object *C4EditCursorSelection::GetLastObject() const { C4Object *obj, *last = nullptr; for (const C4Value &v : (*this)) if ((obj = v.getObj())) last = obj; return last; } void C4EditCursorSelection::ConsolidateEmpty() { // remove nullptred entries that may happen because objects got deleted this->remove(C4VNull); } bool C4EditCursorSelection::ClearPointers(C4Object *obj) { bool found = false; for (C4Value &v : (*this)) if (obj == v.getObj()) { found = true; v.Set0(); } if (found) ConsolidateEmpty(); return found; } bool C4EditCursorSelection::IsContained(C4PropList *obj) const { for (const C4Value &v : (*this)) if (obj == v.getPropList()) return true; return false; } int32_t C4EditCursorSelection::ObjectCount() const { // count only C4Object * int32_t count = 0; for (const C4Value &v : *this) if (v.getObj()) ++count; return count; } C4EditCursor::C4EditCursor() #ifdef WITH_QT_EDITOR : shapes(new C4ConsoleQtShapes()) #endif { Default(); } C4EditCursor::~C4EditCursor() { Clear(); } void C4EditCursor::Execute() { // drawing switch (Mode) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeEdit: // Hold selection if (Hold) EMMoveObject(fShiftWasDown ? EMMO_MoveForced : EMMO_Move, Fix0, Fix0, nullptr, &selection); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeDraw: switch (Console.ToolsDlg.Tool) { case C4TLS_Fill: if (Hold) if (!Game.HaltCount) if (Console.Editing) ApplyToolFill(); break; } break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } if (!::Game.iTick35 || ::Console.EditCursor.IsSelectionInvalidated()) { selection.ConsolidateEmpty(); Console.PropertyDlgUpdate(selection, false); } } bool C4EditCursor::Init() { #ifdef USE_WIN32_WINDOWS if (!(hMenu = LoadMenu(Application.GetInstance(),MAKEINTRESOURCE(IDR_CONTEXTMENUS)))) return false; #endif Console.UpdateModeCtrls(Mode); return true; } void C4EditCursor::ClearPointers(C4Object *pObj) { if (Target==pObj) Target=nullptr; if (selection.ClearPointers(pObj)) OnSelectionChanged(); } bool C4EditCursor::Move(float iX, float iY, float iZoom, DWORD dwKeyState) { // alt check bool fAltIsDown = (dwKeyState & MK_ALT) != 0; if (fAltIsDown != fAltWasDown) { if ((fAltWasDown = fAltIsDown)) AltDown(); else AltUp(); } // shift check bool fShiftIsDown = (dwKeyState & MK_SHIFT) != 0; if(fShiftIsDown != fShiftWasDown) fShiftWasDown = fShiftIsDown; // Offset movement float xoff = iX-X; float yoff = iY-Y; X = iX; Y = iY; Zoom = iZoom; // Drag rotation/scale of object if (DragTransform) { C4Object *obj = selection.GetObject(); if (obj) { int32_t new_rot = (DragRot0 + int32_t(float(X - X2)*Zoom)) % 360; if (new_rot < 0) new_rot += 360; if (fShiftIsDown) new_rot = (new_rot + 23) / 45 * 45; int32_t new_con = DragCon0 + int32_t(float(Y2 - Y)*Zoom*(FullCon / 200)); int32_t con_step = FullCon / 5; if (fShiftIsDown) new_con = (new_con + con_step/2) / con_step * con_step; if (!obj->Def->Oversize) new_con = std::min(new_con, FullCon); new_con = std::max(new_con, fShiftIsDown ? 1 : con_step); bool any_change = false; if (obj->Def->Rotateable) if (new_rot != DragRotLast) any_change = true; if (obj->Def->GrowthType) if (new_con != DragConLast) any_change = true; if (any_change) { EMMoveObject(EMMO_Transform, itofix(new_rot, 1), itofix(new_con, FullCon/100), obj, nullptr); DragRotLast = new_rot; DragConLast = new_con; } } } switch (Mode) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeEdit: #ifdef WITH_QT_EDITOR shapes->MouseMove(X, Y, Hold, 3.0f/Zoom, !!(dwKeyState & MK_SHIFT), !!(dwKeyState & MK_CONTROL)); #endif // Hold if (!DragFrame && Hold && !DragShape && !DragTransform) { MoveSelection(ftofix(xoff),ftofix(yoff)); UpdateDropTarget(dwKeyState); } // Update target // Shift always indicates a target outside the current selection else { Target = (dwKeyState & MK_SHIFT) ? selection.GetLastObject() : nullptr; do { Target = Game.FindObject(nullptr,X,Y,0,0,OCF_NotContained, Target); } while ((dwKeyState & MK_SHIFT) && Target && selection.IsContained(Target)); } break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeCreateObject: // Drop target for contained object creation UpdateDropTarget(dwKeyState); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeDraw: switch (Console.ToolsDlg.Tool) { case C4TLS_Brush: if (Hold) ApplyToolBrush(); break; case C4TLS_Line: case C4TLS_Rect: break; } break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } // Update UpdateStatusBar(); return true; } bool C4EditCursor::Move(DWORD new_key_state) { // Move at last position with new key state return Move(X, Y, Zoom, new_key_state); } void C4EditCursor::UpdateStatusBar() { int32_t X=this->X, Y=this->Y; StdStrBuf str; switch (Mode) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModePlay: if (::MouseControl.GetCaption()) str.CopyUntil(::MouseControl.GetCaption(),'|'); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeEdit: str.Format("%i/%i (%s)",X,Y,Target ? (Target->GetName()) : LoadResStr("IDS_CNS_NOTHING") ); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeCreateObject: str.Format(LoadResStr("IDS_CNS_CREATESTATUS"), creator_def ? (creator_def->GetName()) : LoadResStr("IDS_CNS_NOTHING")); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeDraw: str.Format("%i/%i (fg: %s, bg: %s)",X,Y, MatValid(::Landscape.GetMat(X,Y)) ? ::MaterialMap.Map[::Landscape.GetMat(X,Y)].Name : LoadResStr("IDS_CNS_NOTHING"), MatValid(::Landscape.GetBackMat(X,Y)) ? ::MaterialMap.Map[::Landscape.GetBackMat(X,Y)].Name : LoadResStr("IDS_CNS_NOTHING") ); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } Console.DisplayInfoText(C4ConsoleGUI::CONSOLE_Cursor, str); } void C4EditCursor::OnSelectionChanged(bool by_objectlist) { ::Console.PropertyDlgUpdate(selection, true); if (!by_objectlist) ::Console.ObjectListDlg.Update(selection); } void C4EditCursor::AddToSelection(C4PropList *add_proplist) { if (!add_proplist) return; C4Object *add_obj = add_proplist->GetObject(); if (add_obj && !add_obj->Status) return; if (selection.IsContained(add_proplist)) return; // add object to selection and do script callback selection.push_back(C4VPropList(add_proplist)); } bool C4EditCursor::RemoveFromSelection(C4PropList *remove_proplist) { if (!remove_proplist) return false; C4Object *remove_obj = remove_proplist->GetObject(); if (remove_obj && !remove_obj->Status) return false; // remove object from selection and do script callback if (!selection.IsContained(remove_proplist)) return false; selection.remove(C4VPropList(remove_proplist)); return true; } void C4EditCursor::ClearSelection(C4PropList *next_selection) { // remove everything from selection selection.clear(); } bool C4EditCursor::LeftButtonDown(DWORD dwKeyState) { // Hold Hold=true; switch (Mode) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeEdit: // Click on shape? #ifdef WITH_QT_EDITOR if (shapes->MouseDown(X, Y, 3.0f / Zoom, !!(dwKeyState & MK_SHIFT), !!(dwKeyState & MK_CONTROL))) { DragShape = true; break; } #endif if (dwKeyState & MK_CONTROL) { // Toggle target if (Target) if (!RemoveFromSelection(Target)) AddToSelection(Target); } else { // Click rotate/scale marker? if (IsHoveringTransformMarker()) { DragTransform = true; X2 = X; Y2 = Y; C4Object *dragged_obj = selection.GetObject(); DragRot0 = DragRotLast = dragged_obj->GetR(); DragCon0 = DragConLast = dragged_obj->GetCon(); break; } // Click on unselected: select single if (Target) { bool found = false; for (C4Value &obj : selection) { if(obj.getObj() && obj.getObj()->At(X, Y)) { found = true; break; } } if(!found) // means loop didn't break { ClearSelection(Target); AddToSelection(Target); } } // Click on nothing: drag frame if (!Target) { ClearSelection(); DragFrame=true; X2=X; Y2=Y; } } OnSelectionChanged(); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeCreateObject: ApplyCreateObject(!!(dwKeyState & MK_CONTROL)); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeDraw: switch (Console.ToolsDlg.Tool) { case C4TLS_Brush: ApplyToolBrush(); break; case C4TLS_Line: DragLine=true; X2=X; Y2=Y; break; case C4TLS_Rect: DragFrame=true; X2=X; Y2=Y; break; case C4TLS_Fill: if (Game.HaltCount) { Hold=false; Console.Message(LoadResStr("IDS_CNS_FILLNOHALT")); return false; } break; case C4TLS_Picker: ApplyToolPicker(); break; } break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } DropTarget=nullptr; return true; } bool C4EditCursor::RightButtonDown(DWORD dwKeyState) { switch (Mode) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeEdit: if ( (dwKeyState & MK_CONTROL) == 0) { // Check whether cursor is on anything in the selection bool fCursorIsOnSelection = false; for (C4Value &obj : selection) { if (obj.getObj() && obj.getObj()->At(X,Y)) { fCursorIsOnSelection = true; break; } } if (!fCursorIsOnSelection) { // Click on unselected if (Target && !selection.IsContained(Target)) { ClearSelection(Target); AddToSelection(Target); } // Click on nothing if (!Target) ClearSelection(); } } OnSelectionChanged(); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } return true; } bool C4EditCursor::LeftButtonUp(DWORD dwKeyState) { // Finish edit/tool switch (Mode) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeEdit: if (DragFrame) FrameSelection(); if (DropTarget) PutContents(); break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case C4CNS_ModeDraw: switch (Console.ToolsDlg.Tool) { case C4TLS_Line: if (DragLine) ApplyToolLine(); break; case C4TLS_Rect: if (DragFrame) ApplyToolRect(); break; } break; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } // Release #ifdef WITH_QT_EDITOR shapes->MouseUp(X, Y, !!(dwKeyState & MK_SHIFT), !!(dwKeyState & MK_CONTROL)); #endif Hold=false; DragFrame=false; DragLine=false; DragShape = false; DragTransform = false; DropTarget=nullptr; // Update UpdateStatusBar(); return true; } bool C4EditCursor::KeyDown(C4KeyCode KeyCode, DWORD dwKeyState) { // alt check bool fAltIsDown = (dwKeyState & MK_ALT) != 0; fAltIsDown = fAltIsDown || KeyCode == K_ALT_L || KeyCode == K_ALT_R; if (fAltIsDown != fAltWasDown) { if ((fAltWasDown = fAltIsDown)) AltDown(); else AltUp(); } // shift check bool fShiftIsDown = (dwKeyState & MK_SHIFT) != 0; fShiftIsDown = fShiftIsDown || KeyCode == K_SHIFT_L || KeyCode == K_SHIFT_R; if(fShiftIsDown != fShiftWasDown) fShiftWasDown = fShiftIsDown; return true; } bool C4EditCursor::KeyUp(C4KeyCode KeyCode, DWORD dwKeyState) { // alt check bool fAltIsDown = (dwKeyState & MK_ALT) != 0; fAltIsDown = fAltIsDown && !( KeyCode == K_ALT_L || KeyCode == K_ALT_R); if (fAltIsDown != fAltWasDown) { if ((fAltWasDown = fAltIsDown)) AltDown(); else AltUp(); } // shift check bool fShiftIsDown = (dwKeyState & MK_SHIFT) != 0; fShiftIsDown = fShiftIsDown && !(KeyCode == K_SHIFT_L || KeyCode == K_SHIFT_R); if(fShiftIsDown != fShiftWasDown) fShiftWasDown = fShiftIsDown; return true; } #ifdef USE_WIN32_WINDOWS bool SetMenuItemEnable(HMENU hMenu, WORD id, bool fEnable) { return !!EnableMenuItem(hMenu,id,MF_BYCOMMAND | MF_ENABLED | ( fEnable ? 0 : MF_GRAYED)); } bool SetMenuItemText(HMENU hMenu, WORD id, const char *szText) { MENUITEMINFOW minfo; ZeroMem(&minfo,sizeof(minfo)); minfo.cbSize = sizeof(minfo); minfo.fMask = MIIM_ID | MIIM_TYPE | MIIM_DATA; minfo.fType = MFT_STRING; minfo.wID = id; StdBuf td = GetWideCharBuf(szText); minfo.dwTypeData = getMBufPtr(td); minfo.cch = wcslen(minfo.dwTypeData); return !!SetMenuItemInfoW(hMenu,id,false,&minfo); } #endif bool C4EditCursor::RightButtonUp(DWORD dwKeyState) { Target=nullptr; #ifndef WITH_QT_EDITOR DoContextMenu(dwKeyState); #endif // Update UpdateStatusBar(); return true; } bool C4EditCursor::Delete() { if (!EditingOK()) return false; EMMoveObject(EMMO_Remove, Fix0, Fix0, nullptr, &selection); if (::Control.isCtrlHost()) { OnSelectionChanged(); } return true; } bool C4EditCursor::OpenPropTools() { switch (Mode) { case C4CNS_ModeEdit: case C4CNS_ModePlay: Console.PropertyDlgOpen(); Console.PropertyDlgUpdate(selection, false); break; case C4CNS_ModeDraw: Console.ToolsDlg.Open(); break; } return true; } bool C4EditCursor::Duplicate() { EMMoveObject(EMMO_Duplicate, Fix0, Fix0, nullptr, &selection); return true; } void C4EditCursor::PerformDuplication(int32_t *object_numbers, int32_t object_count, bool local_call) { if (!object_count) return; // Remember last OEI so duplicated objects can be determined int32_t prev_oei = C4PropListNumbered::GetEnumerationIndex(); // Get serialized objects C4RefCntPointer object_numbers_c4v = new C4ValueArray(); object_numbers_c4v->SetSize(object_count); int32_t total_object_count = 0; for (int32_t i = 0; i < object_count; ++i) { C4Object *obj = ::Objects.SafeObjectPointer(object_numbers[i]); if (!obj) continue; total_object_count = obj->AddObjectAndContentsToArray(object_numbers_c4v, total_object_count); } object_numbers_c4v->SetSize(total_object_count); int32_t objects_file_handle = ::ScriptEngine.CreateUserFile(); C4AulParSet pars(C4VInt(objects_file_handle), C4VArray(object_numbers_c4v.Get())); C4Value result_c4v(::ScriptEngine.GetPropList()->Call(PSF_SaveScenarioObjects, &pars)); bool result = !!result_c4v; if (result_c4v.GetType() == C4V_Nil) { // Function returned nil: This usually means there was a script error during object writing. // It could also mean the scripter overloaded global func SaveScenarioObjects and returned nil. // In either case, no the objects file will contain garbage so no objects were duplicated. LogF("ERROR: No valid result from global func " PSF_SaveScenarioObjects ". Regular object duplication failed."); } else { // Function completed successfully (returning true or false) C4AulUserFile *file = ::ScriptEngine.GetUserFile(objects_file_handle); if (!result || !file || !file->GetFileLength()) { // Nothing written? Then we don't have objects. // That's OK; not an error. } else { // Create copy of objects by executing duplication script StdStrBuf data = file->GrabFileContents(); AulExec.DirectExec(&::ScriptEngine, data.getData(), "object duplication", false, nullptr, true); } } ::ScriptEngine.CloseUserFile(objects_file_handle); // Did duplication work? bool any_duplicates = false; for (C4Object *obj : ::Objects) { if (obj->Number > prev_oei) { any_duplicates = true; break; } } // If duplication created no objects, the user probably tried to copy a non-saved object // Just copy the old way then if (!any_duplicates) { PerformDuplicationLegacy(object_numbers, object_count, local_call); return; } // Update local client status: Put new objects into selection if (local_call) { selection.clear(); int64_t X_all = 0, Y_all = 0, n_selected = 0; float old_x = X, old_y = Y; for (C4Object *obj : ::Objects) { if (obj->Number > prev_oei) { selection.push_back(C4VObj(obj)); X_all += obj->GetX(); Y_all += obj->GetY(); ++n_selected; } } // Reset EditCursor pos to center of duplicated objects, so they will be dragged along with the cursor if (n_selected) { X = X_all / n_selected; Y = Y_all / n_selected; } SetHold(true); OnSelectionChanged(); // Ensure duplicated objects moved to the cursor without extra mouse movement // Move with shift key pressed to allow initial shift of HorizontalFixed items DWORD last_key_state = MK_SHIFT; if (fAltWasDown) last_key_state |= MK_ALT; bool shift_was_down = fShiftWasDown; Move(old_x, old_y, Zoom, last_key_state); fShiftWasDown = shift_was_down; } } void C4EditCursor::PerformDuplicationLegacy(int32_t *pObjects, int32_t iObjectNum, bool fLocalCall) { // Old-style object copying: Just create new objects at old object position with same prototype C4Object *pOldObj, *pObj; for (int i = 0; iGetPrototype(), pOldObj, pOldObj->Owner, pOldObj->GetX(), pOldObj->GetY()); if (pObj && pObj->Status) { // local call? adjust selection then // do callbacks for all clients for sync reasons if (fLocalCall) selection.push_back(C4VObj(pObj)); C4AulParSet pars(C4VObj(pObj)); if (pOldObj->Status) pOldObj->Call(PSF_EditCursorDeselection, &pars); if (pObj->Status) pObj->Call(PSF_EditCursorSelection); } } // update status if (fLocalCall) { for (int i = 0; i(1.0f, 1.0f / cgo.Zoom); float offX, offY, newzoom; cobj->GetDrawPosition(cgo, offX, offY, newzoom); ZoomDataStackItem zdsi(cgo.X, cgo.Y, newzoom); if (select_mark_color) { FLOAT_RECT frame = { offX + cobj->Shape.x, offX + cobj->Shape.x + cobj->Shape.Wdt, offY + cobj->Shape.y, offY + cobj->Shape.y + cobj->Shape.Hgt }; DrawSelectMark(cgo, frame, line_width, select_mark_color); } if (highlight) { uint32_t dwOldMod = cobj->ColorMod; uint32_t dwOldBlitMode = cobj->BlitMode; cobj->ColorMod = 0xffffffff; cobj->BlitMode = C4GFXBLIT_CLRSFC_MOD2 | C4GFXBLIT_ADDITIVE; if (cobj->pMeshInstance) cobj->pMeshInstance->SetFaceOrdering(StdSubMeshInstance::FO_NearestToFarthest); cobj->Draw(cgo, -1); cobj->DrawTopFace(cgo, -1); if (cobj->pMeshInstance) cobj->pMeshInstance->SetFaceOrderingForClrModulation(cobj->ColorMod); cobj->ColorMod = dwOldMod; cobj->BlitMode = dwOldBlitMode; } // Transformer knob if (draw_transform_marker) { float transform_marker_x = 0.0f, transform_marker_y = 0.0f; if (HasTransformMarker(&transform_marker_x, &transform_marker_y, cgo.Zoom)) { transform_marker_x += offX; transform_marker_y += offY; float sz = float(::GraphicsResource.fctTransformKnob.Hgt) / cgo.Zoom; C4Facet transform_target_sfc(cgo.Surface, transform_marker_x-sz/2, transform_marker_y-sz/2, sz, sz); ::GraphicsResource.fctTransformKnob.Draw(transform_target_sfc); // Transform knob while dragging if (DragTransform) { pDraw->SetBlitMode(C4GFXBLIT_ADDITIVE); transform_target_sfc.X += X - X2; transform_target_sfc.Y += Y - Y2; ::GraphicsResource.fctTransformKnob.Draw(transform_target_sfc); pDraw->ResetBlitMode(); } } } } bool C4EditCursor::HasTransformMarker(float *x, float *y, float zoom) const { // Single selection only (assume obj is in selection) if (selection.size() != 1) return false; C4Object *obj = selection.GetObject(); if (!obj) return false; // Show knob only for objects that can be scaled or rotated if (!obj->Def->GrowthType && !obj->Def->Rotateable) return false; // Show knob only if the shape has a certain minimum size in either extent (so small objects can still be moved) float vis_wdt = float(obj->Shape.Wdt) * zoom; float vis_hgt = float(obj->Shape.Wdt) * zoom; if (vis_wdt < ::GraphicsResource.fctTransformKnob.Hgt && vis_hgt < ::GraphicsResource.fctTransformKnob.Hgt) return false; // It's visible: Put it to the bottom of the shape without the shape expansion through rotation *x = 0; *y = float(obj->Def->Shape.y + obj->Def->Shape.Hgt) * obj->GetCon() / FullCon - float(::GraphicsResource.fctTransformKnob.Hgt) / (zoom*2); return true; } void C4EditCursor::Draw(C4TargetFacet &cgo) { ZoomDataStackItem zdsi(cgo.X, cgo.Y, cgo.Zoom); float line_width = std::max(1.0f, 1.0f / cgo.Zoom); #ifdef WITH_QT_EDITOR // Draw shapes of selection shapes->Draw(cgo); #endif // Draw selection marks for (C4Value &obj : selection) { C4Object *cobj = obj.getObj(); if (!cobj) continue; DrawObject(cgo, cobj, 0xffffffff, fShiftWasDown, true); // highlight selection if shift is pressed } // Draw drag frame if (DragFrame) pDraw->DrawFrameDw(cgo.Surface, std::min(X, X2) + cgo.X - cgo.TargetX, std::min(Y, Y2) + cgo.Y - cgo.TargetY, std::max(X, X2) + cgo.X - cgo.TargetX, std::max(Y, Y2) + cgo.Y - cgo.TargetY, 0xffffffff, line_width); // Draw drag line if (DragLine) pDraw->DrawLineDw(cgo.Surface, X + cgo.X - cgo.TargetX, Y + cgo.Y - cgo.TargetY, X2 + cgo.X - cgo.TargetX, Y2 + cgo.Y - cgo.TargetY, 0xffffffff, line_width); // Draw drop target if (DropTarget) ::GraphicsResource.fctMouseCursor.DrawX(cgo.Surface, DropTarget->GetX() + cgo.X - cgo.TargetX - ::GraphicsResource.fctMouseCursor.Wdt / 2 / cgo.Zoom, DropTarget->GetY() + DropTarget->Shape.y + cgo.Y - cgo.TargetY - ::GraphicsResource.fctMouseCursor.Hgt / cgo.Zoom, float(::GraphicsResource.fctMouseCursor.Wdt) / cgo.Zoom, float(::GraphicsResource.fctMouseCursor.Hgt) / cgo.Zoom, C4MC_Cursor_DropInto); // Draw paint circle if (Mode == C4CNS_ModeDraw && has_mouse_hover && ::Console.ToolsDlg.Grade>0 && ::Console.ToolsDlg.IsGradedTool()) { // shadow for recognition on white background/material pDraw->DrawCircleDw(cgo.Surface, X + cgo.X - cgo.TargetX + 1.0f/cgo.Zoom, Y + cgo.Y - cgo.TargetY + 1.0f / cgo.Zoom, ::Console.ToolsDlg.Grade, 0xff000000, line_width); // actual circle pDraw->DrawCircleDw(cgo.Surface, X + cgo.X - cgo.TargetX, Y + cgo.Y - cgo.TargetY, ::Console.ToolsDlg.Grade, 0xffffffff, line_width); } // Draw creator preview if (Mode == C4CNS_ModeCreateObject && has_mouse_hover && creator_def) { C4TargetFacet cgo_creator; cgo_creator.Set(cgo.Surface, X + cgo.X - cgo.TargetX, Y + cgo.Y - cgo.TargetY, creator_def->Shape.Wdt, creator_def->Shape.Hgt, 0, 0, cgo.Zoom, 0, 0); if (!creator_overlay) { creator_overlay.reset(new C4GraphicsOverlay()); creator_overlay->SetAsBase(&creator_def->Graphics, C4GFXBLIT_ADDITIVE); } creator_overlay->Draw(cgo_creator, nullptr, NO_OWNER); } // Draw object highlight C4Object *highlight = highlighted_object.getObj(); if (highlight) DrawObject(cgo, highlight, 0xffff8000, true, false); // highlight selection if shift is pressed } void C4EditCursor::DrawSelectMark(C4Facet &cgo, FLOAT_RECT frame, float width, uint32_t color) { if ((cgo.Wdt<1) || (cgo.Hgt<1)) return; if (!cgo.Surface) return; const float EDGE_WIDTH = 2.f; unsigned char c[4] = { static_cast((color >> 16) & 0xff), static_cast((color >> 8) & 0xff), static_cast((color >> 0) & 0xff), static_cast((color >> 24) & 0xff) }; const C4BltVertex vertices[] = { { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.left + EDGE_WIDTH, frame.top, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.left, frame.top, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.left, frame.top, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.left, frame.top+EDGE_WIDTH, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.left+EDGE_WIDTH, frame.bottom-1, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.left, frame.bottom-1, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.left, frame.bottom-1, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.left, frame.bottom-1-EDGE_WIDTH, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.right-1-EDGE_WIDTH, frame.top, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.right-1, frame.top, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.right-1, frame.top, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.right-1, frame.top+EDGE_WIDTH, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.right-1-EDGE_WIDTH, frame.bottom-1, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.right-1, frame.bottom-1, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.right-1, frame.bottom-1, 0.f }, { 0.f, 0.f, { c[0], c[1], c[2], c[3] }, frame.right-1, frame.bottom-1-EDGE_WIDTH, 0.f }, }; const unsigned int n_vertices = sizeof(vertices) / sizeof(vertices[0]); pDraw->PerformMultiLines(cgo.Surface, vertices, n_vertices, width, nullptr); } void C4EditCursor::MoveSelection(C4Real XOff, C4Real YOff) { EMMoveObject(fShiftWasDown ? EMMO_MoveForced : EMMO_Move, XOff, YOff, nullptr, &selection); } void C4EditCursor::FrameSelection() { ClearSelection(); for (C4Object *cobj : Objects) { if (cobj->Status && cobj->OCF & OCF_NotContained) { if (Inside(cobj->GetX(),std::min(X,X2),std::max(X,X2)) && Inside(cobj->GetY(),std::min(Y,Y2),std::max(Y,Y2))) AddToSelection(cobj); } } OnSelectionChanged(); } bool C4EditCursor::In(const char *szText) { ::Console.RegisterRecentInput(szText, C4Console::MRU_Object); EMMoveObject(EMMO_Script, Fix0, Fix0, nullptr, &selection, szText); selection.ConsolidateEmpty(); ::Console.PropertyDlgUpdate(selection, true); return true; } void C4EditCursor::Default() { fAltWasDown=false; fShiftWasDown=false; Mode = C4CNS_ModeEdit; X=Y=X2=Y2=0; Target=DropTarget=nullptr; #ifdef USE_WIN32_WINDOWS hMenu=nullptr; #endif Hold=DragFrame=DragLine=DragShape=DragTransform=false; selection.clear(); creator_def = nullptr; creator_overlay = nullptr; has_mouse_hover = false; selection_invalid = false; DragRot0 = DragRotLast = 0; DragCon0 = DragConLast = FullCon; } void C4EditCursor::Clear() { #ifdef USE_WIN32_WINDOWS if (hMenu) DestroyMenu(hMenu); hMenu=nullptr; #endif #ifdef WITH_DEBUG_MODE ObjselectDelItems(); #endif selection.clear(); Console.PropertyDlgUpdate(selection, false); creator_overlay.reset(nullptr); #ifdef WITH_QT_EDITOR shapes->ClearShapes(); // Should really be empty already #endif } bool C4EditCursor::SetMode(int32_t iMode) { // Store focus #ifdef USE_WIN32_WINDOWS HWND hFocus=GetFocus(); #endif // Update console buttons (always) Console.UpdateModeCtrls(iMode); // No change if (iMode==Mode) return true; // Set mode Mode = iMode; // Update prop tools by mode switch (Mode) { case C4CNS_ModeEdit: case C4CNS_ModePlay: case C4CNS_ModeCreateObject: Console.ToolsDlgClose(); break; case C4CNS_ModeDraw: Console.PropertyDlgClose(); break; } if (Mode == C4CNS_ModePlay) { ::MouseControl.ShowCursor(); } else { OpenPropTools(); ::MouseControl.HideCursor(); } // Restore focus #ifdef USE_WIN32_WINDOWS SetFocus(hFocus); #endif // Done return true; } bool C4EditCursor::ToggleMode() { if (!EditingOK()) return false; // Step through modes int32_t iNewMode; switch (Mode) { case C4CNS_ModePlay: iNewMode=C4CNS_ModeEdit; break; #ifdef WITH_QT_EDITOR case C4CNS_ModeEdit: iNewMode=C4CNS_ModeCreateObject; break; #else case C4CNS_ModeEdit: iNewMode = C4CNS_ModeDraw; break; #endif case C4CNS_ModeCreateObject: iNewMode = C4CNS_ModeDraw; break; case C4CNS_ModeDraw: iNewMode=C4CNS_ModePlay; break; default: iNewMode=C4CNS_ModePlay; break; } // Set new mode SetMode(iNewMode); return true; } void C4EditCursor::ApplyCreateObject(bool container) { if (!EditingOK()) return; if (!creator_def) return; if (container && !DropTarget) return; // execute/send control EMControl(CID_EMMoveObj, C4ControlEMMoveObject::CreateObject(creator_def->id, ftofix(X), ftofix(Y), container ? DropTarget : nullptr)); } void C4EditCursor::ApplyToolBrush() { if (!EditingOK(true)) return; C4ToolsDlg *pTools=&Console.ToolsDlg; // execute/send control EMControl(CID_EMDrawTool, new C4ControlEMDrawTool(EMDT_Brush, ::Landscape.GetMode(), X,Y,0,0, pTools->Grade, pTools->Material, pTools->Texture, pTools->BackMaterial, pTools->BackTexture)); } void C4EditCursor::ApplyToolLine() { if (!EditingOK(true)) return; C4ToolsDlg *pTools=&Console.ToolsDlg; // execute/send control EMControl(CID_EMDrawTool, new C4ControlEMDrawTool(EMDT_Line, ::Landscape.GetMode(), X,Y,X2,Y2, pTools->Grade, pTools->Material,pTools->Texture, pTools->BackMaterial, pTools->BackTexture)); } void C4EditCursor::ApplyToolRect() { if (!EditingOK(true)) return; C4ToolsDlg *pTools=&Console.ToolsDlg; // execute/send control EMControl(CID_EMDrawTool, new C4ControlEMDrawTool(EMDT_Rect, ::Landscape.GetMode(), X,Y,X2,Y2, pTools->Grade, pTools->Material, pTools->Texture, pTools->BackMaterial, pTools->BackTexture)); } void C4EditCursor::ApplyToolFill() { if (!EditingOK(true)) return; C4ToolsDlg *pTools=&Console.ToolsDlg; // execute/send control EMControl(CID_EMDrawTool, new C4ControlEMDrawTool(EMDT_Fill, ::Landscape.GetMode(), X,Y,0,Y2, pTools->Grade, pTools->Material, nullptr, nullptr, nullptr)); } void C4EditCursor::AppendMenuItem(int num, const StdStrBuf & label) { #ifdef USE_WIN32_WINDOWS itemsObjselect[num].ItemId = IDM_VPORTDYN_FIRST + num; if (num) AppendMenu(GetSubMenu(hMenu,0), MF_STRING, IDM_VPORTDYN_FIRST + num, label.GetWideChar()); else AppendMenu(GetSubMenu(hMenu,0), MF_SEPARATOR, IDM_VPORTDYN_FIRST, nullptr); #endif } bool C4EditCursor::DoContextMenu(DWORD dwKeyState) { bool fObjectSelected = !!selection.GetObject(); #ifdef USE_WIN32_WINDOWS POINT point; GetCursorPos(&point); HMENU hContext = GetSubMenu(hMenu,0); SetMenuItemEnable(hContext, IDM_VIEWPORT_DELETE, fObjectSelected && Console.Editing); SetMenuItemEnable(hContext, IDM_VIEWPORT_DUPLICATE, fObjectSelected && Console.Editing); SetMenuItemEnable(hContext, IDM_VIEWPORT_CONTENTS, fObjectSelected && selection.GetObject()->Contents.ObjectCount() && Console.Editing); SetMenuItemText(hContext,IDM_VIEWPORT_DELETE,LoadResStr("IDS_MNU_DELETE")); SetMenuItemText(hContext,IDM_VIEWPORT_DUPLICATE,LoadResStr("IDS_MNU_DUPLICATE")); SetMenuItemText(hContext,IDM_VIEWPORT_CONTENTS,LoadResStr("IDS_MNU_CONTENTS")); #endif // Add selection and custom command entries for any objects at the cursor ObjselectDelItems(); // clear previous entries C4FindObjectAtPoint pFO(X,Y); C4ValueArray * atcursor; atcursor = pFO.FindMany(::Objects, ::Objects.Sectors); // needs freeing (single object ptr) int itemcount = atcursor->GetSize(); if(itemcount > 0) { // Count required entries for all objects and their custom commands int entrycount = itemcount; for (int i_item = 0; i_item < itemcount; ++i_item) { C4Object *pObj = (*atcursor)[i_item].getObj(); assert(pObj); C4ValueArray *custom_commands = pObj->GetPropertyArray(P_EditCursorCommands); if (custom_commands) entrycount += custom_commands->GetSize(); } #ifdef USE_WIN32_WINDOWS // If too many entries would be shown, add a "..." in the end const int maxentries = 25; // Maximum displayed objects. if you raise it, also change note with IDM_VPORTDYN_FIRST in resource.h bool has_too_many_entries = (entrycount > maxentries); if (has_too_many_entries) entrycount = maxentries + 1; #else const int maxentries = std::numeric_limits::max(); #endif itemsObjselect.resize(entrycount + 1); // +1 for a separator // Add a separator bar itemsObjselect[0].Object = nullptr; itemsObjselect[0].Command.Clear(); itemsObjselect[0].EditCursor = this; AppendMenuItem(0, StdStrBuf()); // Add all objects int i_entry = 0; for (int i_item = 0; i_item < itemcount; ++i_item) { ++i_entry; if (i_entry >= maxentries) break; // Add selection entry C4Object *obj = (*atcursor)[i_item].getObj(); assert(obj); itemsObjselect[i_entry].Object = obj; itemsObjselect[i_entry].Command.Clear(); itemsObjselect[i_entry].EditCursor = this; AppendMenuItem(i_entry, FormatString("%s #%i (%i/%i)", obj->GetName(), obj->Number, obj->GetX(), obj->GetY())); // Add custom command entries C4ValueArray *custom_commands = obj->GetPropertyArray(P_EditCursorCommands); if (custom_commands) for (int i_cmd = 0; i_cmd < custom_commands->GetSize(); ++i_cmd) { ++i_entry; if (i_entry >= maxentries) break; const C4Value &cmd = custom_commands->GetItem(i_cmd); StdStrBuf custom_command_szstr; C4AulFunc *custom_command; C4String *custom_command_string; // Custom command either by string or by function pointer if ((custom_command = cmd.getFunction())) custom_command_szstr.Format("%s()", custom_command->GetName()); else if ((custom_command_string = cmd.getStr())) custom_command_szstr.Copy(custom_command_string->GetData()); // copy just in case script get reloaded inbetween if (custom_command_szstr.getLength()) { itemsObjselect[i_entry].Object = obj; itemsObjselect[i_entry].Command.Take(custom_command_szstr); itemsObjselect[i_entry].EditCursor = this; AppendMenuItem(i_entry, FormatString("%s->%s", obj->GetName(), custom_command_szstr.getData())); } } } #ifdef USE_WIN32_WINDOWS if (has_too_many_entries) { AppendMenu(hContext, MF_GRAYED, IDM_VPORTDYN_FIRST + maxentries + 1, L"..."); itemsObjselect[maxentries + 1].ItemId = IDM_VPORTDYN_FIRST + maxentries + 1; itemsObjselect[maxentries + 1].Object = nullptr; itemsObjselect[maxentries + 1].Command.Clear(); } #endif } delete atcursor; #ifdef USE_WIN32_WINDOWS int32_t iItem = TrackPopupMenu( hContext, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_LEFTBUTTON | TPM_NONOTIFY, point.x,point.y, 0, Console.hWindow, nullptr); switch (iItem) { case IDM_VIEWPORT_DELETE: Delete(); break; case IDM_VIEWPORT_DUPLICATE: Duplicate(); break; case IDM_VIEWPORT_CONTENTS: GrabContents(); break; case 0: break; default: for(std::vector::iterator it = itemsObjselect.begin() + 1; it != itemsObjselect.end(); ++it) if(it->ItemId == iItem) { if (it->Command.getLength()) DoContextObjCommand(it->Object, it->Command.getData()); else DoContextObjsel(it->Object, (dwKeyState & MK_SHIFT) == 0); break; } break; } ObjselectDelItems(); #endif return true; } void C4EditCursor::GrabContents() { // Set selection C4Object *pFrom; if (!( pFrom = selection.GetObject() )) return; ClearSelection(); for (C4Object *cont : pFrom->Contents) AddToSelection(cont); OnSelectionChanged(); Hold=true; // Exit all objects EMMoveObject(EMMO_Exit, Fix0, Fix0, nullptr, &selection); } void C4EditCursor::UpdateDropTarget(DWORD dwKeyState) { // A drop target is set if holding down control either while moving an object or in object creation mode DropTarget=nullptr; if (dwKeyState & MK_CONTROL) if (selection.GetObject() || (Mode == C4CNS_ModeCreateObject && creator_def)) for (C4Object *cobj : Objects) { if (cobj->Status) if (!cobj->Contained) if (Inside(X-(cobj->GetX()+cobj->Shape.x),0,cobj->Shape.Wdt-1)) if (Inside(Y-(cobj->GetY()+cobj->Shape.y),0,cobj->Shape.Hgt-1)) if (!selection.IsContained(cobj)) { DropTarget=cobj; break; } } } void C4EditCursor::PutContents() { if (!DropTarget) return; EMMoveObject(EMMO_Enter, Fix0, Fix0, DropTarget, &selection); } C4Object *C4EditCursor::GetTarget() { return Target; } bool C4EditCursor::EditingOK(bool for_landscape_drawing) { if (!Console.Editing) { Hold=false; Console.Message(LoadResStr("IDS_CNS_NONETEDIT")); return false; } return true; } int32_t C4EditCursor::GetMode() { return Mode; } void C4EditCursor::ToolFailure() { C4ToolsDlg *pTools=&Console.ToolsDlg; Hold=false; Console.Message(FormatString(LoadResStr("IDS_CNS_NOMATDEF"),pTools->Material,pTools->Texture).getData()); } void C4EditCursor::ApplyToolPicker() { int32_t iMaterial; BYTE byIndex; switch (::Landscape.GetMode()) { case LandscapeMode::Static: { bool material_set = false; int32_t x = X/::Landscape.GetMapZoom(); int32_t y = Y/::Landscape.GetMapZoom(); // Material-texture from map if ((byIndex = ::Landscape.GetMapIndex(x, y))) { const C4TexMapEntry *pTex = ::TextureMap.GetEntry(byIndex); if (pTex && pTex->GetMaterialName() && *pTex->GetMaterialName()) { const BYTE byIndexBkg = Landscape.GetBackMapIndex(x, y); Console.ToolsDlg.SelectMaterial(pTex->GetMaterialName()); Console.ToolsDlg.SelectTexture(pTex->GetTextureName()); // Set background index if GUI backend supports it if (Console.ToolsDlg.ModeBack) { const C4TexMapEntry *pBgTex = ::TextureMap.GetEntry(byIndexBkg); if (pBgTex && !pBgTex->isNull()) { Console.ToolsDlg.SelectBackMaterial(pBgTex->GetMaterialName()); Console.ToolsDlg.SelectBackTexture(pBgTex->GetTextureName()); } else { Console.ToolsDlg.SelectBackMaterial(C4TLS_MatSky); } } else { Console.ToolsDlg.SetIFT(byIndexBkg != 0); } material_set = true; } } // default to sky, because invalid materials are always rendered as sky if (!material_set) Console.ToolsDlg.SelectMaterial(C4TLS_MatSky); break; } case LandscapeMode::Exact: // Material only from landscape if (MatValid(iMaterial=GBackMat(X,Y))) { Console.ToolsDlg.SelectMaterial(::MaterialMap.Map[iMaterial].Name); Console.ToolsDlg.SetIFT(Landscape.GetBackPix(X, Y) != 0); } else Console.ToolsDlg.SelectMaterial(C4TLS_MatSky); break; } Hold=false; } void C4EditCursor::EMMoveObject(C4ControlEMObjectAction eAction, C4Real tx, C4Real ty, C4Object *pTargetObj, const C4EditCursorSelection *pObjs, const char *szScript) { // construct object list int32_t iObjCnt = 0; int32_t *pObjIDs = nullptr; if (pObjs && (iObjCnt = pObjs->ObjectCount())) { pObjIDs = new int32_t [iObjCnt]; // fill int32_t i = 0; for (const C4Value &vobj : *pObjs) { C4Object *obj = vobj.getObj(); if (!obj) continue; if (obj && obj->Status) pObjIDs[i++] = obj->Number; else pObjIDs[i++] = 0; } } // execute control EMControl(CID_EMMoveObj, new C4ControlEMMoveObject(eAction, tx, ty, pTargetObj, iObjCnt, pObjIDs, szScript)); } void C4EditCursor::EMControl(C4PacketType eCtrlType, C4ControlPacket *pCtrl) { ::Control.DoInput(eCtrlType, pCtrl, CDT_Decide); } void C4EditCursor::ObjselectDelItems() { if(!itemsObjselect.size()) return; std::vector::iterator it = itemsObjselect.begin(); while(it != itemsObjselect.end()) { #if defined(USE_WIN32_WINDOWS) if(!it->ItemId) { ++it; continue; } HMENU hContext = GetSubMenu(hMenu,0); DeleteMenu(hContext, it->ItemId, MF_BYCOMMAND); #endif ++it; } itemsObjselect.resize(0); } bool C4EditCursor::AltDown() { // alt only has an effect in draw mode (picker) if (Mode == C4CNS_ModeDraw) { Console.ToolsDlg.SetAlternateTool(); } // key not processed - allow further usages of Alt return false; } bool C4EditCursor::AltUp() { if (Mode == C4CNS_ModeDraw) { Console.ToolsDlg.ResetAlternateTool(); } // key not processed - allow further usages of Alt return false; } void C4EditCursor::DoContextObjsel(C4Object * obj, bool clear) { if (clear) ClearSelection(obj); AddToSelection(obj); OnSelectionChanged(); } void C4EditCursor::DoContextObjCommand(C4Object * obj, const char *cmd) { // Command going through queue for sync if (!obj || !cmd) return; In(FormatString("Object(%d)->%s", obj->Number, cmd).getData()); } bool C4EditCursor::GetCurrentSelectionPosition(int32_t *x, int32_t *y) { C4Object *obj = selection.GetObject(); if (!obj || !obj->Status) return false; *x = obj->GetX(); *y = obj->GetY(); return true; } void C4EditCursor::SetHighlightedObject(C4Object *new_highlight) { highlighted_object = C4VObj(new_highlight); } bool C4EditCursor::IsHoveringTransformMarker() const { float trf_marker_x, trf_marker_y; if (HasTransformMarker(&trf_marker_x, &trf_marker_y, Zoom)) { C4Object *obj = selection.GetObject(); float dx = (float(X - obj->GetX()) - trf_marker_x) * Zoom; float dy = (float(Y - obj->GetY()) - trf_marker_y) * Zoom; if (dx*dx + dy*dy <= ::GraphicsResource.fctTransformKnob.Hgt * ::GraphicsResource.fctTransformKnob.Hgt / 4) return true; } return false; }