/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2013, 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. */ /* Editable shapes in the viewports (like e.g. AI guard range rectangles) */ #include "C4Include.h" #include "editor/C4Console.h" #include "editor/C4ConsoleQtState.h" #include "editor/C4ConsoleQtShapes.h" #include "graphics/C4FacetEx.h" #include "object/C4Object.h" /* Generic shape */ C4ConsoleQtShape::C4ConsoleQtShape(C4Object *for_obj, C4PropList *props, const class C4PropertyDelegateShape *parent_delegate) : is_relative(true), dragging_border(-1), border_color(0xffff0000), parent_delegate(parent_delegate) { rel_obj.SetPropList(for_obj); if (props) { is_relative = props->GetPropertyBool(P_Relative, is_relative); border_color = props->GetPropertyInt(P_Color) | 0xff000000; } } uint32_t C4ConsoleQtShape::GetBorderColor(int32_t border_index, bool dragging_border_is_bitmask) const { // Return shape color, or dragged border color if index is the border currently being dragged if (IsDragging()) if ((dragging_border == border_index) || (dragging_border_is_bitmask && (dragging_border & border_index))) return 0xffffffff; return border_color; } int32_t C4ConsoleQtShape::AbsX(int32_t rel_x) const { if (is_relative) { C4Object *obj = rel_obj.getObj(); if (obj) rel_x += obj->GetX(); } return rel_x; } int32_t C4ConsoleQtShape::AbsY(int32_t rel_y) const { if (is_relative) { C4Object *obj = rel_obj.getObj(); if (obj) rel_y += obj->GetY(); } return rel_y; } /* Rectangular shape*/ C4ConsoleQtRect::C4ConsoleQtRect(C4Object *for_obj, C4PropList *props, const C4Value &val, const class C4PropertyDelegateShape *parent_delegate) : C4ConsoleQtShape(for_obj, props, parent_delegate), left(0), top(0), right(10), bottom(10), store_as_proplist(false), properties_lowercase(false) { // Def props if (props) { C4String *storage = props->GetPropertyStr(P_Storage); if (storage) { if (storage->GetData() == "proplist") properties_lowercase = store_as_proplist = true; else if (storage->GetData() == "Proplist") store_as_proplist = true; } } // Expect rect to be given as proplist with properties X, Y, Wdt, Hgt or array with elements [left,top,width,height] if (store_as_proplist) { C4PropList *vprops = val.getPropList(); if (vprops) { left = vprops->GetPropertyInt(properties_lowercase ? P_x : P_X); top = vprops->GetPropertyInt(properties_lowercase ? P_y : P_Y); right = left + vprops->GetPropertyInt(properties_lowercase ? P_wdt : P_Wdt) - 1; bottom = top + vprops->GetPropertyInt(properties_lowercase ? P_hgt : P_Hgt) - 1; } } else { C4ValueArray *varr = val.getArray(); if (varr && varr->GetSize() >= 4) { left = varr->GetItem(0).getInt(); top = varr->GetItem(1).getInt(); right = left + varr->GetItem(2).getInt() - 1; // right/bottom borders are drawn inclusively bottom = top + varr->GetItem(3).getInt() - 1; } } } bool C4ConsoleQtRect::IsHit(int32_t x, int32_t y, int32_t hit_range, Qt::CursorShape *drag_cursor, int32_t *drag_border) { // Current border pos int32_t left = AbsX(this->left), top = AbsY(this->top); int32_t right = AbsX(this->right), bottom = AbsY(this->bottom); // Distance to each border int32_t dleft = Abs(left - x); int32_t dtop = Abs(top - y); int32_t dright = Abs(right - x); int32_t dbottom = Abs(bottom - y); // In box at all? if (x < left - hit_range || y < top - hit_range || x > right + hit_range || y > bottom + hit_range) return false; // Border hit? bool hit_left = (dleft <= hit_range && dleft < dright); bool hit_top = (dtop <= hit_range && dtop < dbottom); bool hit_right = (!hit_left && dright <= hit_range); bool hit_bottom = (!hit_top && dbottom <= hit_range); // Compose cursor and drag border int32_t idrag_border = (hit_left * CNAT_Left) + (hit_top * CNAT_Top) + (hit_right * CNAT_Right) + (hit_bottom * CNAT_Bottom); if (idrag_border) *drag_border = idrag_border; if (hit_left || hit_right) if (hit_top || hit_bottom) *drag_cursor = (hit_left == hit_top) ? Qt::SizeFDiagCursor : Qt::SizeBDiagCursor; else *drag_cursor = Qt::SizeHorCursor; else if (hit_top || hit_bottom) *drag_cursor = Qt::SizeVerCursor; return !!idrag_border; } void C4ConsoleQtRect::Draw(class C4TargetFacet &cgo, float line_width) { float left = float(AbsX(this->left)) + cgo.X - cgo.TargetX; float top = float(AbsY(this->top)) + cgo.Y - cgo.TargetY; float right = float(AbsX(this->right)) + cgo.X - cgo.TargetX; float bottom = float(AbsY(this->bottom)) + cgo.Y - cgo.TargetY; pDraw->DrawLineDw(cgo.Surface, left, top, right, top, GetBorderColor(CNAT_Top, true), line_width); pDraw->DrawLineDw(cgo.Surface, right, top, right, bottom, GetBorderColor(CNAT_Right, true), line_width); pDraw->DrawLineDw(cgo.Surface, right, bottom, left, bottom, GetBorderColor(CNAT_Bottom, true), line_width); pDraw->DrawLineDw(cgo.Surface, left, bottom, left, top, GetBorderColor(CNAT_Left, true), line_width); } void C4ConsoleQtRect::Drag(int32_t x, int32_t y, int32_t dx, int32_t dy) { if (dragging_border & CNAT_Left) left += dx; if (dragging_border & CNAT_Top) top += dy; if (dragging_border & CNAT_Right) right += dx; if (dragging_border & CNAT_Bottom) bottom += dy; if (left > right) std::swap(left, right); if (top > bottom) std::swap(top, bottom); } C4Value C4ConsoleQtRect::GetValue() const { // Return array or proplist: Convert left/top/right/bottom (inclusive) to left/top/width/height if (store_as_proplist) { C4PropList *pos_proplist = new C4PropListScript(); pos_proplist->SetProperty(properties_lowercase ? P_x : P_X, C4VInt(left)); pos_proplist->SetProperty(properties_lowercase ? P_y : P_Y, C4VInt(top)); pos_proplist->SetProperty(properties_lowercase ? P_wdt : P_Wdt, C4VInt(right - left + 1)); pos_proplist->SetProperty(properties_lowercase ? P_hgt : P_Hgt, C4VInt(bottom - top + 1)); return C4VPropList(pos_proplist); } else { C4ValueArray *pos_array = new C4ValueArray(4); pos_array->SetItem(0, C4VInt(left)); pos_array->SetItem(1, C4VInt(top)); pos_array->SetItem(2, C4VInt(right - left + 1)); pos_array->SetItem(3, C4VInt(bottom - top + 1)); return C4VArray(pos_array); } } /* Circle shape */ C4ConsoleQtCircle::C4ConsoleQtCircle(class C4Object *for_obj, C4PropList *props, const C4Value &val, const class C4PropertyDelegateShape *parent_delegate) : C4ConsoleQtShape(for_obj, props, parent_delegate), radius(10), cx(0), cy(0), can_move_center(false) { if (props) { can_move_center = props->GetPropertyBool(P_CanMoveCenter); } // If center is moveable, expect value as [radius, center_x, center_y] // Otherwise just radius if (can_move_center) { C4ValueArray *aval = val.getArray(); if (aval && aval->GetSize() == 3) { radius = aval->GetItem(0).getInt(); cx = aval->GetItem(1).getInt(); cy = aval->GetItem(2).getInt(); } } else { radius = val.getInt(); } } bool C4ConsoleQtCircle::IsHit(int32_t x, int32_t y, int32_t hit_range, Qt::CursorShape *drag_cursor, int32_t *drag_border) { // Get relative circle center pos x -= AbsX(cx); y -= AbsY(cy); int32_t r = x*x + y*y; // Is on circle border? (Higher priority than center to allow resizing circle from 0 radius) if (Inside(r, (radius - hit_range)*(radius - hit_range), (radius + hit_range)*(radius + hit_range))) { // Cursor by position on 60 deg circle segments if (x * 58 / 100 / (y+!y)) // tan(30) ~= 0.58 *drag_cursor = Qt::CursorShape::SizeHorCursor; else if (y * 58 / 100 / (x+!x)) *drag_cursor = Qt::CursorShape::SizeVerCursor; else if (x*y > 0) *drag_cursor = Qt::CursorShape::SizeFDiagCursor; else *drag_cursor = Qt::CursorShape::SizeBDiagCursor; *drag_border = 0; return true; } // Circle center? if (can_move_center && r <= hit_range*hit_range) { *drag_cursor = Qt::CursorShape::SizeAllCursor; *drag_border = 1; return true; } return false; } void C4ConsoleQtCircle::Draw(class C4TargetFacet &cgo, float line_width) { // Circle pDraw->DrawCircleDw(cgo.Surface, AbsX(cx) + cgo.X - cgo.TargetX, AbsY(cy) + cgo.Y - cgo.TargetY, radius, GetBorderColor(0, false), line_width); // Center if moveable if (can_move_center) pDraw->DrawCircleDw(cgo.Surface, AbsX(cx) + cgo.X - cgo.TargetX, AbsY(cy) + cgo.Y - cgo.TargetY, line_width*3, GetBorderColor(1, false), line_width); } void C4ConsoleQtCircle::C4ConsoleQtCircle::Drag(int32_t x, int32_t y, int32_t dx, int32_t dy) { if (dragging_border == 0) { x -= AbsX(cx); y -= AbsY(cy); radius = int32_t(sqrt(double(x*x + y*y))); } else if (dragging_border == 1) { cx += dx; cy += dy; } } C4Value C4ConsoleQtCircle::GetValue() const { // Return single value for non-center-adjustable circles; return [radius, cx, cy] otherwise if (can_move_center) { C4ValueArray *pos_array = new C4ValueArray(3); pos_array->SetItem(0, C4VInt(radius)); pos_array->SetItem(1, C4VInt(cx)); pos_array->SetItem(2, C4VInt(cy)); return C4VArray(pos_array); } else { return C4VInt(radius); } } /* Point shape */ C4ConsoleQtPoint::C4ConsoleQtPoint(class C4Object *for_obj, C4PropList *props, const C4Value &val, const class C4PropertyDelegateShape *parent_delegate) : C4ConsoleQtShape(for_obj, props, parent_delegate), cx(0), cy(0) { // Expect value as [x,y] C4ValueArray *aval = val.getArray(); if (aval && aval->GetSize() == 2) { cx = aval->GetItem(0).getInt(); cy = aval->GetItem(1).getInt(); } } bool C4ConsoleQtPoint::IsHit(int32_t x, int32_t y, int32_t hit_range, Qt::CursorShape *drag_cursor, int32_t *drag_border) { // Get relative circle center pos x -= AbsX(cx); y -= AbsY(cy); int32_t r = x*x + y*y; // Hits point? if (r <= hit_range*hit_range*6) { *drag_cursor = Qt::CursorShape::SizeAllCursor; *drag_border = 0; return true; } return false; } void C4ConsoleQtPoint::Draw(class C4TargetFacet &cgo, float line_width) { // Circle with cross inside uint32_t clr = GetBorderColor(0, false); float d = line_width * 3; float dc = sqrtf(2) * d; int32_t x = AbsX(cx) + cgo.X - cgo.TargetX; int32_t y = AbsY(cy) + cgo.Y - cgo.TargetY; pDraw->DrawLineDw(cgo.Surface, x - d, y - d, x + d, y + d, clr, line_width); pDraw->DrawLineDw(cgo.Surface, x - d, y + d, x + d, y - d, clr, line_width); pDraw->DrawCircleDw(cgo.Surface, x, y, dc, clr, line_width); } void C4ConsoleQtPoint::Drag(int32_t x, int32_t y, int32_t dx, int32_t dy) { cx += dx; cy += dy; } C4Value C4ConsoleQtPoint::GetValue() const { // Return [cx, cy] C4ValueArray *pos_array = new C4ValueArray(2); pos_array->SetItem(0, C4VInt(cx)); pos_array->SetItem(1, C4VInt(cy)); return C4VArray(pos_array); } /* Shape list */ C4ConsoleQtShape *C4ConsoleQtShapes::CreateShape(class C4Object *for_obj, C4PropList *props, const C4Value &val, const class C4PropertyDelegateShape *parent_delegate) { C4String *type = props->GetPropertyStr(P_Type); if (!type) return nullptr; C4ConsoleQtShape *shape = nullptr; if (type->GetData() == "rect") shape = new C4ConsoleQtRect(for_obj, props, val, parent_delegate); else if (type->GetData() == "circle") shape = new C4ConsoleQtCircle(for_obj, props, val, parent_delegate); else if (type->GetData() == "point") shape = new C4ConsoleQtPoint(for_obj, props, val, parent_delegate); return shape; } void C4ConsoleQtShapes::AddShape(C4ConsoleQtShape *shape) { if (shape) shapes.emplace_back(shape); } void C4ConsoleQtShapes::RemoveShape(C4ConsoleQtShape *shape) { // Remove from list and currently moving shape shapes.remove_if([shape](auto &it) { return it.get() == shape; }); if (dragging_shape == shape) dragging_shape = NULL; } void C4ConsoleQtShapes::ClearShapes() { shapes.clear(); dragging_shape = NULL; drag_cursor = Qt::CursorShape::ArrowCursor; } void C4ConsoleQtShapes::Draw(C4TargetFacet &cgo) { // Draw all shapes with at least 1px line width ZoomDataStackItem zdsi(cgo.X, cgo.Y, cgo.Zoom); float line_width = std::max(1.0f, 1.0f / cgo.Zoom); for (auto &shape : shapes) shape->Draw(cgo, line_width); } bool C4ConsoleQtShapes::MouseDown(float x, float y, float hit_range) { // Check for shape hit and start dragging if a shape is in hit range int32_t hit_range_int = std::max(int32_t(hit_range + 0.5f), 1); // Using integer hit ranges for now // Ensure no leftover other shape if (dragging_shape) MouseUp(x, y); int32_t drag_border=-1; for (auto &shape : shapes) { if (shape->IsHit(x, y, hit_range_int, &drag_cursor, &drag_border)) { dragging_shape = shape.get(); dragging_shape->StartDragging(drag_border); drag_x = x; drag_y = y; return true; } } return false; } void C4ConsoleQtShapes::MouseMove(float x, float y, bool left_down, float hit_range) { // Check for shape hit and start dragging if a shape is in hit range int32_t hit_range_int = std::max(int32_t(hit_range + 0.5f), 1); // Using integer hit ranges for now // move down move: Execute shape dragging (full pixels only) if (dragging_shape && left_down) { int32_t dx = int32_t(round(x - drag_x)), dy = int32_t(round(y - drag_y)); if (dx || dy) { drag_x += dx; drag_y += dy; dragging_shape->Drag(drag_x, drag_y, dx, dy); } } else if (!left_down) { // Just moving around: Update cursor drag_cursor = Qt::CursorShape::ArrowCursor; int32_t ignored; for (auto &shape : shapes) if (shape->IsHit(x, y, hit_range_int, &drag_cursor, &ignored)) break; } else { // Regular move: Reset drag cursor drag_cursor = Qt::CursorShape::ArrowCursor; } } void C4ConsoleQtShapes::MouseUp(float x, float y) { // Stop dragging if (dragging_shape) { dragging_shape->emit ShapeDragged(); dragging_shape->StopDragging(); dragging_shape = NULL; drag_cursor = Qt::CursorShape::ArrowCursor; } } /* Shape pointer holder class */ void C4ConsoleQtShapeHolder::Clear() { if (shape) { ::Console.EditCursor.GetShapes()->RemoveShape(shape); shape = nullptr; } } void C4ConsoleQtShapeHolder::Set(C4ConsoleQtShape *new_shape) { Clear(); shape = new_shape; if (shape) ::Console.EditCursor.GetShapes()->AddShape(shape); }