#include "vwm_building.h" #include #include "vwm_math.h" #include #include #include #include #include namespace BuildingControlUnit { namespace VWM { static NGL::INGLLogger &logger() { return TrainingAreaBase::getLogger("VWM.Building"); } static inline bool isEffectNegligible(double effect) { return (effect < Settings::minimumRemainEffectThreshold()); } static inline bool isEffectNegligible(double RW, double ZF, double TF) { return isEffectNegligible(ZF * RW * TF); } static inline double calcProtectionOfWallAfterHit(double SG_alt, double RW, double ZF, double TF) { const double SG_diff = ZF * RW * TF; if (isEffectNegligible(SG_diff)) return SG_alt; return std::max(0.0, SG_alt - SG_diff); } void WallExclusionZoneBase::setInitalProtection(double initialProtection, bool bResetProtection) { m_initialProtection = initialProtection; if (bResetProtection) m_protection = initialProtection; } void WallExclusionZoneBase::updateProtection(const double RW, const double ZF, const double TF) { const double SG_alt = m_protection; const double SG_neu = calcProtectionOfWallAfterHit(SG_alt, RW, ZF, TF); m_protection = SG_neu; } void Room::setFortifyFactor(double factorNew, bool bRecalcValues) { const double factorOld = m_fortifyFactor; if (qFuzzyIsNull(factorOld) || qFuzzyIsNull(factorNew)) return; m_fortifyFactor = factorNew; if (bRecalcValues) { for (OuterWall &wall : m_outerWalls) { wall.setProtection(wall.protection() / factorOld * factorNew); wall.setInitalProtection(wall.initialProtection() / factorOld * factorNew, false); for (ExclusionZone &exZone : wall.exclusionZones()) { exZone.setProtection(exZone.protection() / factorOld * factorNew); exZone.setInitalProtection(exZone.initialProtection() / factorOld * factorNew, false); } } } } double Room::damagePercent() const { if (m_outerWalls.isEmpty()) return 0; double currentProtection = 0.0; double initialProtection = 0.0; for (const OuterWall &wall : std::as_const(m_outerWalls)) { currentProtection += wall.protection(); initialProtection += wall.initialProtection(); for (const ExclusionZone &exZone : std::as_const(wall.exclusionZones())) { currentProtection += exZone.protection(); initialProtection += exZone.initialProtection(); } } if (qFuzzyIsNull(initialProtection)) return 100; return (1.0 - currentProtection / initialProtection) * 100.0; } double Room::calcRemainingEffect(const HitPosition &hitPos, const Charge &ammu) const { const double SG_alt = protectionForHitPosition(hitPos); const double ZF = ammu.destructiveFactor; const double WF = ammu.penetrationEffect; const double RW = calcRemainingEffect(SG_alt, ZF, WF); return RW; } void Room::updateProtectionDirectHit(const HitPosition &hitPos, const Charge &ammu) { Q_ASSERT(hitPos.state != HitPosition::State::NoHit); const double SG_alt = protectionForHitPosition(hitPos); const double ZF = ammu.destructiveFactor; const double SG_neu = calcProtectionOfWallAfterHit(SG_alt, 1.0, ZF, 1.0); setProtectionForHitPosition(hitPos, SG_neu); } static bool isDirectHitOuterWall(const HitPosition &hitPos, int roomId, int idxOw) { if (hitPos.state != HitPosition::State::OuterWall) return false; return hitPos.roomId == roomId && hitPos.outerWallIdx == idxOw; } static bool isDirectHitExlusionZone(const HitPosition &hitPos, int roomId, int idxOw, int idxEz) { if (hitPos.state != HitPosition::State::ExclusionZone) return false; return hitPos.roomId == roomId && hitPos.outerWallIdx == idxOw && hitPos.exclusionZoneIdx == idxEz; } void Room::updateProtectionIndirectHit(const HitPosition &hitPos, const Charge &ammu, double remainingEffect) { updateProtectionNeighbourHit(hitPos, ammu, remainingEffect, 1.0); } bool Room::updateProtectionNeighbourHit(const HitPosition &hitPos, const Charge &ammu, double remainingEffect, double transitionFactor) { const double ZF = ammu.destructiveFactor; const double RW = remainingEffect; const double TF = transitionFactor; DEBUG_LOG(logger()) << "Effect for room " << m_id << " (RW="<< RW << ", ZF=" << ZF << " , TF=" << TF << ") ; effect=" << RW*ZF*TF << " -> damagePercent VOR= " << this->damagePercent(); if (isEffectNegligible(RW, ZF, TF)) { DEBUG_LOG(logger()) << "Effect is Negligible for room " << m_id; for (int idxOw = 0; idxOw < m_outerWalls.size(); ++idxOw) { OuterWall &ow = m_outerWalls[idxOw]; DEBUG_LOG(logger()) << "room " << m_id << "[ow=" << idxOw << "] new protection = " << ow.protection() << " (" << ow.protectionPercent() << "%)"; } return false; } const char *hitReason = (hitPos.roomId == m_id ? "indirect hit" : "neighbour hit"); for (int idxOw = 0; idxOw < m_outerWalls.size(); ++idxOw) { OuterWall &ow = m_outerWalls[idxOw]; if (!isDirectHitOuterWall(hitPos, m_id, idxOw)) { ow.updateProtection(RW, ZF, TF); DEBUG_LOG(logger()) << "room " << m_id << "[ow=" << idxOw << "] new protection = " << ow.protection() << " (" << ow.protectionPercent() << "%) - " << hitReason; } for (int idxEz = 0; idxEz < ow.exclusionZones().size(); ++idxEz) { if (!isDirectHitExlusionZone(hitPos, m_id, idxOw, idxEz)) { ExclusionZone &ez = ow.exclusionZones()[idxEz]; ez.updateProtection(RW, ZF, TF); DEBUG_LOG(logger()) << "room " << m_id << "[ow=" << idxOw << "][ez=" << idxEz << "] new protection = " << ez.protection() << " (" << ez.protectionPercent() << "%) - " << hitReason; } } } DEBUG_LOG(logger()) << "Effect for room " << m_id << " -> damagePercent NACH = " << this->damagePercent(); return true; } double Room::protectionForHitPosition(const HitPosition &hitPos) const { const OuterWall &ow = m_outerWalls.at(hitPos.outerWallIdx); if (hitPos.state == HitPosition::State::OuterWall) return ow.protection(); return ow.exclusionZones().at(hitPos.exclusionZoneIdx).protection(); } void Room::setProtectionForHitPosition(const HitPosition &hitPos, double SG_neu) { Q_ASSERT(hitPos.state != HitPosition::State::NoHit); OuterWall &ow = m_outerWalls[hitPos.outerWallIdx]; if (hitPos.state == HitPosition::State::OuterWall) { ow.setProtection(SG_neu); DEBUG_LOG(logger()) << "room " << m_id << "[ow=" << hitPos.outerWallIdx << "] new protection = " << ow.protection() << " (" << ow.protectionPercent() << "%)" << " - direct hit"; } else { ExclusionZone &ez = ow.exclusionZones()[hitPos.exclusionZoneIdx]; ez.setProtection(SG_neu); DEBUG_LOG(logger()) << "room " << m_id << "[ow=" << hitPos.outerWallIdx << "][ez=" << hitPos.exclusionZoneIdx << "] new protection = " << ez.protection() << " (" << ez.protectionPercent() << "%)" << " - direct hit"; } } double Room::calcRemainingEffect(double SG_alt, double ZF, double WF) { if (SG_alt < 0.0001) return 1.0; if (ZF < 0.0001) return 0; double D_Wand = 0.0; if (SG_alt < ZF * 0.95) D_Wand = ZF * 0.95 - SG_alt; const double D_Zufall = ZF * WF * 0.05 / SG_alt; double RW = 100.0 * (D_Wand + D_Zufall) / ZF; if (RW > 1.0) RW = 1.0; return RW; } Building::Building() : m_lastHitPosition(HitPosition()) {} int Building::checkPolygons() { int numberOfDuplicates = 0; for (const Room &room : std::as_const(m_rooms)) { for (int owIdx = 0; owIdx < room.outerWalls().size(); ++owIdx) { const OuterWall &ow = room.outerWalls().at(owIdx); for (int elemIdx = 0; elemIdx < ow.elements().size(); ++elemIdx) { const WallElement &wallElement = ow.elements().at(elemIdx); numberOfDuplicates += checkDublicatePointsInPolygon(room.id(), owIdx, elemIdx, "QuterWall", wallElement); } } } return numberOfDuplicates; } int Building::cleanupPolygons() { int numberOfRemoved = 0; for (Room &room : m_rooms) { for (int owIdx = 0; owIdx < room.outerWalls().size(); ++owIdx) { OuterWall &ow = room.outerWalls()[owIdx]; for (int elemIdx = 0; elemIdx < ow.elements().size(); ++elemIdx) { WallElement &wallElement = ow.elements()[elemIdx]; numberOfRemoved += cleanupDublicatePointsInPolygon(room.id(), owIdx, elemIdx, "room", wallElement); bool bMakePlanarDone = false; MakePlanar makePlanar(wallElement); QVector candidate = makePlanar.calculate(&bMakePlanarDone); cleanupDublicatePointsInPolygon(room.id(), owIdx, elemIdx, "room", candidate); if (candidate.size() < 4) { NOTICE_LOG(logger()) << "wall was made planar: Building '" << m_name << "' -> " << " in room " << room.id() << " OuterWall[" << owIdx << "], element[" << elemIdx << "] " << " makePlanar and cleanupDublicatePointsInPolygon would generate short polygon, size=" << candidate.size(); for (auto point : std::as_const(candidate)) NOTICE_LOG(logger()) << " point= " << point; bMakePlanarDone = false; } else { wallElement = candidate; } if (bMakePlanarDone) NOTICE_LOG(logger()) << "wall was made planar: Building '" << m_name << "' -> " << " in room " << room.id() << " OuterWall[" << owIdx << "], element[" << elemIdx << "] " << " smallestDim=" << (int)makePlanar.getSmallestDim() << " smallestRange=" << makePlanar.getSmallestRange(); } } } return numberOfRemoved; } int Building::cleanupDublicatePointsInPolygon(int roomId, int idx, int subIdx, const char *type, QVector &points) const { QVector idxToRemove; idxToRemove.reserve(points.size()); for (int i = 0; i < points.size(); ++i) { const auto &p1 = points.at(i); for (int j = i + 1; j < points.size(); ++j) { auto p2 = points.at(j); if (qFuzzyCompare(p1, p2)) { DEBUG_LOG(logger()) << "Building '" << m_name << "' -> " << "Polygon point " << i << " (" << p1 << ") and " << j << " (" << p2 << ") in room " << roomId << ", " << type << "[" << idx << "], element[" << subIdx << "] are the same! -> REMOVE"; if (!idxToRemove.contains(j)) idxToRemove.push_back(j); } } } std::sort(idxToRemove.begin(), idxToRemove.end(), std::greater()); int numberOfRemoved = idxToRemove.size(); for (auto idxRemove : idxToRemove) points.removeAt(idxRemove); return numberOfRemoved; } Room Building::room(int roomId) { auto it = m_rooms.find(roomId); if (it == m_rooms.end()) return {}; return *it; } int Building::damagePercent() const { double roomDamagePercent = 0.0; for (const Room &room : m_rooms) roomDamagePercent += room.damagePercent(); return qRound(roomDamagePercent / m_rooms.size()); } QVector Building::roomStates() const { QVector ret; ret.reserve(m_rooms.size() / 2); for (const Room &room : m_rooms) { const int dmgPercent = qRound(room.damagePercent()); HitRoom hr; hr.roomId = room.id(); hr.damagePercent = dmgPercent; hr.remainingEffectPercent = qRound(room.remainingEffect() * 100); hr.fortifyFactor = room.fortifyFactor(); hr.directHit = room.isDirectHit(); ret += hr; DEBUG_LOG(logger()) << "HitRoom " << hr.roomId << " damage: " << hr.damagePercent << "%, remainEffect: " << hr.remainingEffectPercent << "%" << ", remainingEffectChargeTypeId: " << hr.remainingEffectChargeTypeId << "%" << ", fortifyFactor: " << hr.fortifyFactor << ", isDirectHit:" << room.isDirectHit(); } return ret; } DamageState Building::damageState() const { if (Building::damagePercent() >= Settings::killThreshold()) return DamageState::Destroyed; return DamageState::NoDamage; } void Building::init() { for (Room &room : m_rooms) initRoom(room); } void Building::destroyAllRooms() { for (Room &room : m_rooms) destroyRoom(room); } void Building::setRoomState(DamageState damageState, const QVector &afffectedRooms) { for (const int roomId : afffectedRooms) { auto it = m_rooms.find(roomId); if (it == m_rooms.end()) { WARNING_LOG(logger()) << "Could not find room with id " << roomId; continue; } if (damageState == DamageState::Destroyed) destroyRoom(it.value()); else initRoom(it.value()); } } void Building::setFortifyInfo(const QVector &foritfyInfos) { for (const FortifyInfo &info : foritfyInfos) { auto it = m_rooms.find(info.roomId); if (it == m_rooms.end()) { WARNING_LOG(logger()) << "Could not find room with id " << info.roomId; continue; } Room &room = it.value(); room.setFortifyFactor(info.fortifyFactor); } } void Building::initRoom(Room &room) const { const double BGPL = m_buildingProtectionLevel; const double OWPL = m_outerWallProtectionLevel; const double EZPL = m_exclusionZoneProtectionLevel; room.setRemainingEffect(0); room.setDirectHit(false); const double fortifyFactor = room.fortifyFactor(); if (room.outerWalls().isEmpty()) { room.outerWalls() += OuterWall(); } for (OuterWall &ow : room.outerWalls()) { if (ow.isDummyWall()) { ow.setInitalProtection(250 * fortifyFactor, true); } else { const double OWPF = ow.protectiveFactor(); const double SG_0_Wall = BGPL * OWPL * OWPF * fortifyFactor; ow.setInitalProtection(SG_0_Wall, true); for (ExclusionZone &ez : ow.exclusionZones()) { const double EZPF = ez.protectiveFactor(); const double SG_0_EZ = BGPL * EZPL * EZPF * fortifyFactor; ez.setInitalProtection(SG_0_EZ, true); } } } } void Building::destroyRoom(Room &room) { room.setRemainingEffect(1); for (OuterWall &ow : room.outerWalls()) { ow.setProtection(0); for (ExclusionZone &ez : ow.exclusionZones()) ez.setProtection(0); } } bool Building::hit(const LtsUtils::QPoint3D &pos, const LtsUtils::QPoint3D &dir, Charge charge, HitPosition *preCalculatedHitPosition) { for (Room &room : m_rooms) room.setDirectHit(false); if (preCalculatedHitPosition) { m_lastHitPosition = *preCalculatedHitPosition; } else { m_lastHitPosition = Math::getHitPosition(pos, dir, m_rooms); } const bool bBelowGround = isLastHitPositionBelowGroundHack(); if (bBelowGround) { INFO_LOG(logger()) << " XXXX CES belowGround, reduce charge: " << charge; charge.destructiveFactor *= 0.25; charge.penetrationEffect *= 0.25; INFO_LOG(logger()) << " XXXX CES belowGround, reduce charge: " << charge; } INFO_LOG(logger()) << "XXXXX HitPosition: state=" << m_lastHitPosition.state << ", m_name=" << m_name << ", m_modelId=" << m_modelId << ", roomId=" << m_lastHitPosition.roomId << ", outerWallIndex=" << m_lastHitPosition.outerWallIdx << ", exclusionZoneIndex=" << m_lastHitPosition.exclusionZoneIdx << ", bBelowGround=" << bBelowGround; if (m_lastHitPosition.state == HitPosition::State::NoHit) return false; Room &hitRoom = m_rooms[m_lastHitPosition.roomId]; if (!hitRoom.isValid()) { ERROR_LOG(logger()) << "Internal error in hit calculation - could not find room with id " << m_lastHitPosition.roomId; return false; } hitRoom.setDirectHit(true); for (Room &room : m_rooms) room.setRemainingEffect(0); const double remainingEffect = hitRoom.calcRemainingEffect(m_lastHitPosition, charge); hitRoom.setRemainingEffect(remainingEffect); hitRoom.updateProtectionDirectHit(m_lastHitPosition, charge); hitRoom.updateProtectionIndirectHit(m_lastHitPosition, charge, remainingEffect); const QMap &neighbours = hitRoom.neighbours(); QMap neighboursToCheck; QSet currentGradeNeighbours; for (const NeighbourRoom &neighbour : neighbours) { neighboursToCheck.insert(neighbour.id(), NeighboursToCheck(neighbour, hitRoom.remainingEffect())); currentGradeNeighbours.insert(neighbour.id()); } QSet lowerGradeNeighbours; lowerGradeNeighbours.insert(hitRoom.id()); auto ret = checkHitNeighbours(neighboursToCheck, charge, currentGradeNeighbours, lowerGradeNeighbours); while (!ret.isEmpty()) { ret = checkHitNeighbours(ret, charge, currentGradeNeighbours, lowerGradeNeighbours); } return true; } bool Building::isLastHitPositionBelowGroundHack() const { return (m_name.contains(QStringLiteral("BN_Kanaele"))) || (m_name.contains(QStringLiteral("U_Bahn")) && m_lastHitPosition.roomId < 2300) ; } QMap Building::checkHitNeighbours(const QMap &neighbours, const Charge &charge, QSet ¤tGradeNeighbours, QSet &lowerGradeNeighbours) { QMap neighboursToCheck; QSet newCurrentGradeNeighbours; for (const NeighboursToCheck &neighbourRoom : neighbours) { const int neighbourRoomId = neighbourRoom.room.id(); if (lowerGradeNeighbours.contains(neighbourRoomId)) continue; auto itRoom = m_rooms.find(neighbourRoomId); if (itRoom == m_rooms.end()) { WARNING_LOG(logger()) << "Could not find room with id " << neighbourRoomId; continue; } Room &neighbour = itRoom.value(); double TF = Settings::transitionMultiplier() * neighbourRoom.room.transitionFactor(); if (TF > 1.0) TF = 1.0; if (!neighbour.updateProtectionNeighbourHit(m_lastHitPosition, charge, neighbourRoom.remainEffect, TF)) continue; const double oldRe = neighbour.remainingEffect(); neighbour.setRemainingEffect(neighbourRoom.remainEffect * TF); DEBUG_LOG(logger()) << "room " << neighbourRoomId << " remaining effect old: " << oldRe << " -> new: " << neighbour.remainingEffect(); for (const NeighbourRoom &grade2Neighbour : std::as_const(neighbour.neighbours())) { if (lowerGradeNeighbours.contains(grade2Neighbour.id()) || currentGradeNeighbours.contains(grade2Neighbour.id())) continue; auto it = neighboursToCheck.find(grade2Neighbour.id()); if (it == neighboursToCheck.end()) { neighboursToCheck.insert(grade2Neighbour.id(), NeighboursToCheck(grade2Neighbour, neighbour.remainingEffect())); } else { const NeighboursToCheck &curEntry = it.value(); if (neighbour.remainingEffect() > curEntry.remainEffect) neighboursToCheck.insert(grade2Neighbour.id(), NeighboursToCheck(grade2Neighbour, neighbour.remainingEffect())); } newCurrentGradeNeighbours.insert(grade2Neighbour.id()); } } lowerGradeNeighbours += currentGradeNeighbours; currentGradeNeighbours = newCurrentGradeNeighbours; return neighboursToCheck; } QString Building::buildingProperties() const { return m_buildingProperties; } void Building::setBuildingProperties(const QString &buildingProperties) { m_buildingProperties = buildingProperties; } QString Building::segmentProperties(const QString &hostname) const { return m_segmentProperties.value(hostname); } void Building::setSegmentProperties(const QString &hostname, const QString &properties) { m_segmentProperties.insert(hostname, properties); } int Building::checkDublicatePointsInPolygon(int roomId, int idx, int subIdx, const char *type, const QVector &points) const { int numberOfDuplicates = 0; for (int i = 0; i < points.size(); ++i) { const auto &p1 = points.at(i); for (int j = i + 1; j < points.size(); ++j) { const auto &p2 = points.at(j); if (qFuzzyCompare(p1, p2)) { ++numberOfDuplicates; WARNING_LOG(logger()) << "Building '" << m_name << "' -> " << "Polygon point " << i << " (" << p1 << ") and " << j << " (" << p2 << ") in room " << roomId << ", " << type << "[" << idx << "], element[" << subIdx << "] are the same! (size=" << points.size() << ")"; } } } if (numberOfDuplicates > 0) for (auto point : points) WARNING_LOG(logger()) << "Building '" << m_name << "' -> " << "point =" << point << " (same points)"; return numberOfDuplicates; } QString Building::toString(const QVector &cont) { QString str; for (auto item : cont) str += QString(QStringLiteral("%1;")).arg(item.room.id()); return str; } QString Building::toString(const QMap &cont) { QString str; for (auto item : cont) str += QString(QStringLiteral("%1;")).arg(item.room.id()); return str; } QString Building::toString(const QSet &cont) { QString str; for (auto item : cont) str += QString(QStringLiteral("%1;")).arg(item); return str; } MakePlanar::MakePlanar(const QVector &poly) : m_poly(poly) { ranges.resize(3); } QVector MakePlanar::calculate(bool *bMakePlanarDone) { if (ratioOfSmallestAndBiggestDimension() >= 10.0 && getRange(m_smallestDim).range() < 0.20 ) { makePlanar(); if (bMakePlanarDone) { *bMakePlanarDone = true; } } return m_poly; } double MakePlanar::ratioOfSmallestAndBiggestDimension() { if (m_poly.size() < 4) return -1; MyRange &rangeX = ranges[static_cast(DIMENSION::X)]; MyRange &rangeY = ranges[static_cast(DIMENSION::Y)]; MyRange &rangeZ = ranges[static_cast(DIMENSION::Z)]; rangeX.min = m_poly.first().x(); rangeX.max = m_poly.first().x(); rangeY.min = m_poly.first().y(); rangeY.max = m_poly.first().y(); rangeZ.min = m_poly.first().z(); rangeZ.max = m_poly.first().z(); for (const LtsUtils::QPoint3D &pnt : std::as_const(m_poly)) { rangeX.min = std::min(rangeX.min, pnt.x()); rangeX.max = std::max(rangeX.max, pnt.x()); rangeY.min = std::min(rangeY.min, pnt.y()); rangeY.max = std::max(rangeY.max, pnt.y()); rangeZ.min = std::min(rangeZ.min, pnt.z()); rangeZ.max = std::max(rangeZ.max, pnt.z()); } if (rangeY.range() < rangeX.range()) m_smallestDim = DIMENSION::Y; if (rangeZ.range() < getRange(m_smallestDim).range()) m_smallestDim = DIMENSION::Z; if (rangeY.range() > rangeX.range()) m_biggistDim = DIMENSION::Y; if (rangeZ.range() > getRange(m_biggistDim).range()) m_biggistDim = DIMENSION::Z; if (!qFuzzyIsNull(getRange(m_smallestDim).range())) m_ratio = getRange(m_biggistDim).range() / getRange(m_smallestDim).range(); return m_ratio; } void MakePlanar::makePlanar() { const double newValue = (getRange(m_smallestDim).min + getRange(m_smallestDim).max) / 2.0; switch (m_smallestDim) { case DIMENSION::X: for (LtsUtils::QPoint3D &pnt_ref : m_poly) pnt_ref.rx() = newValue; break; case DIMENSION::Y: for (LtsUtils::QPoint3D &pnt_ref : m_poly) pnt_ref.ry() = newValue; break; case DIMENSION::Z: for (LtsUtils::QPoint3D &pnt_ref : m_poly) pnt_ref.rz() = newValue; break; case DIMENSION::Undefined: { } } } void wallElementToBottomLeftTopRight3D(const WallElement &wallElement, LtsUtils::QPoint3D &bottomLeftOut, LtsUtils::QPoint3D &topRightOut) { const LtsUtils::QPoint3D &frontItem = wallElement.front(); bottomLeftOut = frontItem; topRightOut = frontItem; for (auto coord : std::as_const(wallElement)) { if (coord.x() < bottomLeftOut.x()) bottomLeftOut.setX(coord.x()); if (coord.y() < bottomLeftOut.y()) bottomLeftOut.setY(coord.y()); if (coord.z() < bottomLeftOut.z()) bottomLeftOut.setZ(coord.z()); if (coord.x() > topRightOut.x()) topRightOut.setX(coord.x()); if (coord.y() > topRightOut.y()) topRightOut.setY(coord.y()); if (coord.z() > topRightOut.z()) topRightOut.setZ(coord.z()); } } LtsUtils::QPoint3D centerOfWallElement3D(const WallElement &wallElement) { LtsUtils::QPoint3D bottomLeftOut, topRightOut; wallElementToBottomLeftTopRight3D(wallElement, bottomLeftOut, topRightOut); const LtsUtils::QPoint3D ¢er = (bottomLeftOut + topRightOut) / 2; return center; } } }