Beispiel #1
0
Layer* Sprite::indexToLayer(LayerIndex index) const
{
  if (index < LayerIndex(0))
    return NULL;

  int index_count = -1;
  return index2layer(folder(), index, &index_count);
}
Beispiel #2
0
static LayerIndex layer2index(const Layer* layer, const Layer* find_layer, int* index_count)
{
  if (layer == find_layer)
    return LayerIndex(*index_count);
  else {
    (*index_count)++;

    if (layer->isFolder()) {
      int found;

      LayerConstIterator it = static_cast<const LayerFolder*>(layer)->getLayerBegin();
      LayerConstIterator end = static_cast<const LayerFolder*>(layer)->getLayerEnd();

      for (; it != end; ++it) {
        if ((found = layer2index(*it, find_layer, index_count)) >= 0)
          return LayerIndex(found);
      }
    }

    return LayerIndex(-1);
  }
}
Beispiel #3
0
void RemoveLayerCommand::onExecute(Context* context)
{
  std::string layer_name;
  ContextWriter writer(context);
  Document* document(writer.document());
  Layer* layer(writer.layer());
  {
    UndoTransaction undoTransaction(writer.context(), "Remove Layer");

    // TODO the range of selected layer should be in the DocumentLocation.
    Timeline::Range range = App::instance()->getMainWindow()->getTimeline()->range();
    if (range.enabled()) {
      Sprite* sprite = writer.sprite();

      // TODO indexes in timeline are inverted!! fix that for a future release
      for (LayerIndex layer = sprite->countLayers() - LayerIndex(range.layerBegin()+1),
             end = sprite->countLayers() - LayerIndex(range.layerEnd()+2);
           layer != end; --layer) {
        document->getApi().removeLayer(sprite->indexToLayer(layer));
      }
    }
    else {
      layer_name = layer->getName();
      document->getApi().removeLayer(layer);
    }

    undoTransaction.commit();
  }
  update_screen_for_document(document);

  StatusBar::instance()->invalidate();
  if (!layer_name.empty())
    StatusBar::instance()->showTip(1000, "Layer `%s' removed", layer_name.c_str());
  else
    StatusBar::instance()->showTip(1000, "Layers removed");
}
Beispiel #4
0
void UIContext::onGetActiveSite(Site* site) const
{
  DocumentView* view = activeView();
  if (view) {
    view->getSite(site);
  }
  // Default/dummy site (maybe for batch/command line mode)
  else if (!isUIAvailable()) {
    if (Document* doc = m_lastSelectedDoc) {
      site->document(doc);
      site->sprite(doc->sprite());
      site->layer(doc->sprite()->indexToLayer(LayerIndex(0)));
      site->frame(0);
    }
  }
}
Beispiel #5
0
void GotoPreviousLayerCommand::onExecute(Context* context)
{
  const ContextReader reader(context);
  const Sprite* sprite = reader.sprite();
  Site site = *reader.site();

  if (site.layerIndex() > 0)
    site.layerIndex(site.layerIndex().previous());
  else
    site.layerIndex(LayerIndex(sprite->countLayers()-1));

  // Flash the current layer
  ASSERT(current_editor != NULL && "Current editor cannot be null when we have a current sprite");
  current_editor->setLayer(site.layer());
  current_editor->flashCurrentLayer();

  updateStatusBar(site);
}
static DocumentRange drop_range_op(
  Document* doc, Op op, const DocumentRange& from,
  DocumentRangePlace place, const DocumentRange& to)
{
  if (place != kDocumentRangeBefore &&
      place != kDocumentRangeAfter) {
    ASSERT(false);
    throw std::invalid_argument("Invalid 'place' argument");
  }

  Sprite* sprite = doc->sprite();

  // Check noop/trivial/do nothing cases, i.e., move a range to the same place.
  // Also check invalid cases, like moving a Background layer.
  switch (from.type()) {
    case DocumentRange::kCels:
      if (from == to)
        return from;
      break;
    case DocumentRange::kFrames:
      if (op == Move) {
        if ((to.frameBegin() >= from.frameBegin() && to.frameEnd() <= from.frameEnd()) ||
            (place == kDocumentRangeBefore && to.frameBegin() == from.frameEnd()+1) ||
            (place == kDocumentRangeAfter && to.frameEnd() == from.frameBegin()-1))
          return from;
      }
      break;
    case DocumentRange::kLayers:
      if (op == Move) {
        if ((to.layerBegin() >= from.layerBegin() && to.layerEnd() <= from.layerEnd()) ||
            (place == kDocumentRangeBefore && to.layerBegin() == from.layerEnd()+1) ||
            (place == kDocumentRangeAfter && to.layerEnd() == from.layerBegin()-1))
          return from;

        // We cannot move the background
        for (LayerIndex i = from.layerBegin(); i <= from.layerEnd(); ++i)
          if (sprite->indexToLayer(i)->isBackground())
            throw std::runtime_error("The background layer cannot be moved");

        // Before background
        if (place == kDocumentRangeBefore) {
          Layer* background = sprite->indexToLayer(to.layerBegin());
          if (background && background->isBackground())
            throw std::runtime_error("You cannot move something below the background layer");
        }
      }
      break;
  }

  const char* undoLabel = NULL;
  switch (op) {
    case Move: undoLabel = "Move Range"; break;
    case Copy: undoLabel = "Copy Range"; break;
    default:
      ASSERT(false);
      throw std::invalid_argument("Invalid 'op' argument");
  }
  DocumentRange resultRange;

  {
    const app::Context* context = static_cast<app::Context*>(doc->context());
    const ContextReader reader(context);
    ContextWriter writer(reader);
    Transaction transaction(writer.context(), undoLabel, ModifyDocument);
    DocumentApi api = doc->getApi(transaction);

    // TODO Try to add the range with just one call to DocumentApi
    // methods, to avoid generating a lot of SetCelFrame undoers (see
    // DocumentApi::setCelFramePosition).

    switch (from.type()) {

      case DocumentRange::kCels:
        {
          std::vector<Layer*> layers;
          sprite->getLayersList(layers);

          int srcLayerBegin, srcLayerStep, srcLayerEnd;
          int dstLayerBegin, dstLayerStep;
          frame_t srcFrameBegin, srcFrameStep, srcFrameEnd;
          frame_t dstFrameBegin, dstFrameStep;

          if (to.layerBegin() <= from.layerBegin()) {
            srcLayerBegin = from.layerBegin();
            srcLayerStep = 1;
            srcLayerEnd = from.layerEnd()+1;
            dstLayerBegin = to.layerBegin();
            dstLayerStep = 1;
          }
          else {
            srcLayerBegin = from.layerEnd();
            srcLayerStep = -1;
            srcLayerEnd = from.layerBegin()-1;
            dstLayerBegin = to.layerEnd();
            dstLayerStep = -1;
          }

          if (to.frameBegin() <= from.frameBegin()) {
            srcFrameBegin = from.frameBegin();
            srcFrameStep = frame_t(1);
            srcFrameEnd = from.frameEnd()+1;
            dstFrameBegin = to.frameBegin();
            dstFrameStep = frame_t(1);
          }
          else {
            srcFrameBegin = from.frameEnd();
            srcFrameStep = frame_t(-1);
            srcFrameEnd = from.frameBegin()-1;
            dstFrameBegin = to.frameEnd();
            dstFrameStep = frame_t(-1);
          }

          for (int srcLayerIdx = srcLayerBegin,
                 dstLayerIdx = dstLayerBegin; srcLayerIdx != srcLayerEnd; ) {
            for (frame_t srcFrame = srcFrameBegin,
                   dstFrame = dstFrameBegin; srcFrame != srcFrameEnd; ) {
              LayerImage* srcLayer = static_cast<LayerImage*>(layers[srcLayerIdx]);
              LayerImage* dstLayer = static_cast<LayerImage*>(layers[dstLayerIdx]);

              switch (op) {
                case Move: api.moveCel(srcLayer, srcFrame, dstLayer, dstFrame); break;
                case Copy: api.copyCel(srcLayer, srcFrame, dstLayer, dstFrame); break;
              }

              srcFrame += srcFrameStep;
              dstFrame += dstFrameStep;
            }
            srcLayerIdx += srcLayerStep;
            dstLayerIdx += dstLayerStep;
          }

          resultRange = to;
        }
        break;

      case DocumentRange::kFrames:
        {
          frame_t srcFrameBegin = 0, srcFrameStep, srcFrameEnd = 0;
          frame_t dstFrameBegin = 0, dstFrameStep;

          switch (op) {

            case Move:
              if (place == kDocumentRangeBefore) {
                if (to.frameBegin() <= from.frameBegin()) {
                  srcFrameBegin = from.frameBegin();
                  srcFrameStep = frame_t(1);
                  srcFrameEnd = from.frameEnd()+1;
                  dstFrameBegin = to.frameBegin();
                  dstFrameStep = frame_t(1);
                }
                else {
                  srcFrameBegin = from.frameEnd();
                  srcFrameStep = frame_t(-1);
                  srcFrameEnd = from.frameBegin()-1;
                  dstFrameBegin = to.frameBegin();
                  dstFrameStep = frame_t(-1);
                }
              }
              else if (place == kDocumentRangeAfter) {
                if (to.frameEnd() <= from.frameBegin()) {
                  srcFrameBegin = from.frameBegin();
                  srcFrameStep = frame_t(1);
                  srcFrameEnd = from.frameEnd()+1;
                  dstFrameBegin = to.frameEnd()+1;
                  dstFrameStep = frame_t(1);
                }
                else {
                  srcFrameBegin = from.frameEnd();
                  srcFrameStep = frame_t(-1);
                  srcFrameEnd = from.frameBegin()-1;
                  dstFrameBegin = to.frameEnd()+1;
                  dstFrameStep = frame_t(-1);
                }
              }
              break;

            case Copy:
              if (place == kDocumentRangeBefore) {
                if (to.frameBegin() <= from.frameBegin()) {
                  srcFrameBegin = from.frameBegin();
                  srcFrameStep = frame_t(2);
                  srcFrameEnd = from.frameBegin() + 2*from.frames();
                  dstFrameBegin = to.frameBegin();
                  dstFrameStep = frame_t(1);
                }
                else {
                  srcFrameBegin = from.frameEnd();
                  srcFrameStep = frame_t(-1);
                  srcFrameEnd = from.frameBegin()-1;
                  dstFrameBegin = to.frameBegin();
                  dstFrameStep = frame_t(0);
                }
              }
              else if (place == kDocumentRangeAfter) {
                if (to.frameEnd() <= from.frameBegin()) {
                  srcFrameBegin = from.frameBegin();
                  srcFrameStep = frame_t(2);
                  srcFrameEnd = from.frameBegin() + 2*from.frames();
                  dstFrameBegin = to.frameEnd()+1;
                  dstFrameStep = frame_t(1);
                }
                else {
                  srcFrameBegin = from.frameEnd();
                  srcFrameStep = frame_t(-1);
                  srcFrameEnd = from.frameBegin()-1;
                  dstFrameBegin = to.frameEnd()+1;
                  dstFrameStep = frame_t(0);
                }
              }
              break;
          }

          for (frame_t srcFrame = srcFrameBegin,
                 dstFrame = dstFrameBegin; srcFrame != srcFrameEnd; ) {
            switch (op) {
              case Move: api.moveFrame(sprite, srcFrame, dstFrame); break;
              case Copy: api.copyFrame(sprite, srcFrame, dstFrame); break;
            }
            srcFrame += srcFrameStep;
            dstFrame += dstFrameStep;
          }

          if (place == kDocumentRangeBefore) {
            resultRange.startRange(LayerIndex::NoLayer, frame_t(to.frameBegin()), from.type());
            resultRange.endRange(LayerIndex::NoLayer, frame_t(to.frameBegin()+from.frames()-1));
          }
          else if (place == kDocumentRangeAfter) {
            resultRange.startRange(LayerIndex::NoLayer, frame_t(to.frameEnd()+1), from.type());
            resultRange.endRange(LayerIndex::NoLayer, frame_t(to.frameEnd()+1+from.frames()-1));
          }

          if (op == Move && from.frameBegin() < to.frameBegin())
            resultRange.displace(0, -from.frames());
        }
        break;

      case DocumentRange::kLayers:
        {
          std::vector<Layer*> layers;
          sprite->getLayersList(layers);

          if (layers.empty())
            break;

          switch (op) {

            case Move:
              if (place == kDocumentRangeBefore) {
                for (LayerIndex i = from.layerBegin(); i <= from.layerEnd(); ++i) {
                  api.restackLayerBefore(
                    layers[i],
                    layers[to.layerBegin()]);
                }
              }
              else if (place == kDocumentRangeAfter) {
                for (LayerIndex i = from.layerEnd(); i >= from.layerBegin(); --i) {
                  api.restackLayerAfter(
                    layers[i],
                    layers[to.layerEnd()]);
                }
              }
              break;

            case Copy:
              if (place == kDocumentRangeBefore) {
                for (LayerIndex i = from.layerBegin(); i <= from.layerEnd(); ++i) {
                  api.duplicateLayerBefore(
                    layers[i],
                    layers[to.layerBegin()]);
                }
              }
              else if (place == kDocumentRangeAfter) {
                for (LayerIndex i = from.layerEnd(); i >= from.layerBegin(); --i) {
                  api.duplicateLayerAfter(
                    layers[i],
                    layers[to.layerEnd()]);
                }
              }
              break;
          }

          if (place == kDocumentRangeBefore) {
            resultRange.startRange(LayerIndex(to.layerBegin()), frame_t(-1), from.type());
            resultRange.endRange(LayerIndex(to.layerBegin()+from.layers()-1), frame_t(-1));
          }
          else if (place == kDocumentRangeAfter) {
            resultRange.startRange(LayerIndex(to.layerEnd()+1), frame_t(-1), from.type());
            resultRange.endRange(LayerIndex(to.layerEnd()+1+from.layers()-1), frame_t(-1));
          }

          if (op == Move && from.layerBegin() < to.layerBegin())
            resultRange.displace(-from.layers(), 0);
        }
        break;
    }

    transaction.commit();
  }

  return resultRange;
}
Beispiel #7
0
LayerIndex Site::layerIndex() const
{
  return (m_sprite && m_layer ?
          m_sprite->layerToIndex(m_layer): LayerIndex());
}
Beispiel #8
0
void ObjectBase::onSimulation(float timeStep) {
    Point2 delta = m_velocityRelativeToParent * timeStep;

    if (! m_collidesWithCells || (delta.squaredLength() < 1e-6f)) {
        // Case without collisions
        m_frameRelativeToJoint.translation += delta;

    } else {
        if (! isRoot()) {
            report(format("ObjectBase::collidesWithCells must be false for non-root objects. (%d)", m_id.c_str()), ReportLevel::ERROR);
            m_frameRelativeToJoint.translation += delta;
            return;
        }
        // Collision case

        const float speed = m_velocityRelativeToParent.length();

        // Algorithm: Find all tiles within the maximum extent of the object's
        // movement (i.e., ignoring direction) plus its radius. Reduce to
        // the edges (line segments) of non-traversable tiles.

        // A static object with default radius can hit four tiles;
        // one that is moving might typically hit nine, and there is no
        // upper bound.
        Map::SmallEdgeArray edgesInDisk;

        const Frame2D& frame = this->frame();
        m_world.map->getEdgesInDisk(frame.translation, delta.length() + m_collisionRadius, m_elevation, LayerIndex(1), edgesInDisk);

        // Transform the edges into the parent's object space
        const Frame2D& parentFrame = (isRoot() ? Frame2D() : m_world.objectTable.valueFromKey(m_parent)->frame());
        for (int e = 0; e < edgesInDisk.size(); ++e) {
            LineSegment2D& edge = edgesInDisk[e];
            debugDrawEdgeArray.append(edge); // TODO: Remove
            edge = parentFrame.bringFromParentSpace(edge);
        }

        const int maxIterations = 20;

        // Check for collisions with the map edges, adjusting velocity into the unblocked direction each time
        int iterations;
        for (iterations = 0; (iterations < maxIterations) && (timeStep > 1e-6); ++iterations) {

            // Find the first collision
            float firstCollisionTime = finf();
            Point2 firstCollisionLocation;
            for (int e = 0; e < edgesInDisk.size(); ++e) {
                const LineSegment2D& edge = edgesInDisk[e];

                // Recall that everything is in body space, now
                Point2 collisionLocation;
                const float collisionTime = movingDiskFixedLineSegmentCollisionTime(m_frameRelativeToJoint.translation, m_collisionRadius, m_velocityRelativeToParent, edge, collisionLocation);

                if (collisionTime < inf()) {
                    debugDrawPointArray.append(collisionLocation);
                }

                if (collisionTime < firstCollisionTime) {
                    firstCollisionTime     = collisionTime;
                    firstCollisionLocation = collisionLocation;
                }
            }

            // Resolve the collision if it happens before the end of the time step
            if (firstCollisionTime < timeStep) {
                // Advance to just before the collision
                firstCollisionTime =  max(firstCollisionTime - 1e-5f, 0.0f);
                m_frameRelativeToJoint.translation += m_velocityRelativeToParent * firstCollisionTime;
                timeStep -= firstCollisionTime;

                const Vector2 normal = (m_frameRelativeToJoint.translation - firstCollisionLocation).directionOrZero();

                // Alter velocity at the collision by removing the component of the velocity along the collision normal
                m_velocityRelativeToParent -= normal * min(normal.dot(m_velocityRelativeToParent), 0.0f);

                // Restore full speed, deflecting movement
                m_velocityRelativeToParent = m_velocityRelativeToParent.directionOrZero() * speed;

                if (m_velocityRelativeToParent.squaredLength() < 1e-6f) {
                    // Unable to proceed with movement because there is no velocity left
                    timeStep = 0;
                }
            } else {
                // Go to the end of the time step
                firstCollisionTime = timeStep;
                m_frameRelativeToJoint.translation += m_velocityRelativeToParent * timeStep;
                timeStep = 0;
            }
        }

        if (iterations == maxIterations) {
            report("Hit maximum number of iterations in ObjectBase::onSimulation collision resolution.", ReportLevel::WARNING);
        }
    }
}
Beispiel #9
0
void Weaver::weave(MeshGroup* meshgroup)
{   
    wireFrame.meshgroup = meshgroup;
    
    const coord_t maxz = meshgroup->max().z;

    const Settings& mesh_group_settings = Application::getInstance().current_slice->scene.current_mesh_group->settings;
    const coord_t initial_layer_thickness = mesh_group_settings.get<coord_t>("layer_height_0");
    const coord_t connection_height = mesh_group_settings.get<coord_t>("wireframe_height");
    const size_t layer_count = (maxz - initial_layer_thickness) / connection_height + 1;
    std::vector<AdaptiveLayer> layer_thicknesses;

    log("Layer count: %i\n", layer_count);

    std::vector<cura::Slicer*> slicerList;

    for(Mesh& mesh : meshgroup->meshes)
    {
        constexpr bool variable_layer_heights = false;
        cura::Slicer* slicer = new cura::Slicer(&mesh, connection_height, layer_count, variable_layer_heights, &layer_thicknesses);
        slicerList.push_back(slicer);
    }

    LayerIndex starting_layer_idx;
    { // find first non-empty layer
        for (starting_layer_idx = 0; starting_layer_idx < LayerIndex(layer_count); starting_layer_idx++)
        {
            Polygons parts;
            for (cura::Slicer* slicer : slicerList)
                parts.add(slicer->layers[starting_layer_idx].polygons);  
            
            if (parts.size() > 0)
                break;
        }
        if (starting_layer_idx > 0)
        {
            logWarning("First %i layers are empty!\n", starting_layer_idx);
        }
    }

    log("Chainifying layers...\n");
    {
        int starting_z = -1;
        for (cura::Slicer* slicer : slicerList)
            wireFrame.bottom_outline.add(slicer->layers[starting_layer_idx].polygons);

        Application::getInstance().communication->sendPolygons(PrintFeatureType::OuterWall, wireFrame.bottom_outline, 1, 1, 1);
        
        if (slicerList.empty()) //Wait, there is nothing to slice.
        {
            wireFrame.z_bottom = 0;
        }
        else
        {
            wireFrame.z_bottom = slicerList[0]->layers[starting_layer_idx].z;
        }
        
        Point starting_point_in_layer;
        if (wireFrame.bottom_outline.size() > 0)
        {
            starting_point_in_layer = (wireFrame.bottom_outline.max() + wireFrame.bottom_outline.min()) / 2;
        }
        else
        {
            starting_point_in_layer = (Point(0,0) + meshgroup->max() + meshgroup->min()) / 2;
        }
        
        Progress::messageProgressStage(Progress::Stage::INSET_SKIN, nullptr);
        for (LayerIndex layer_idx = starting_layer_idx + 1; layer_idx < LayerIndex(layer_count); layer_idx++)
        {
            Progress::messageProgress(Progress::Stage::INSET_SKIN, layer_idx+1, layer_count); // abuse the progress system of the normal mode of CuraEngine
            
            Polygons parts1;
            for (cura::Slicer* slicer : slicerList)
                parts1.add(slicer->layers[layer_idx].polygons);

            
            Polygons chainified;

            chainify_polygons(parts1, starting_point_in_layer, chainified);

            Application::getInstance().communication->sendPolygons(PrintFeatureType::OuterWall, chainified, 1, 1, 1);

            if (chainified.size() > 0)
            {
                if (starting_z == -1) starting_z = slicerList[0]->layers[layer_idx-1].z;
                wireFrame.layers.emplace_back();
                WeaveLayer& layer = wireFrame.layers.back();
                
                layer.z0 = slicerList[0]->layers[layer_idx-1].z - starting_z;
                layer.z1 = slicerList[0]->layers[layer_idx].z - starting_z;
                
                layer.supported = chainified;
                
                starting_point_in_layer = layer.supported.back().back();
            }
        }
    }

    log("Finding horizontal parts...\n");
    {
        Progress::messageProgressStage(Progress::Stage::SUPPORT, nullptr);
        for (unsigned int layer_idx = 0; layer_idx < wireFrame.layers.size(); layer_idx++)
        {
            Progress::messageProgress(Progress::Stage::SUPPORT, layer_idx+1, wireFrame.layers.size()); // abuse the progress system of the normal mode of CuraEngine
            
            WeaveLayer& layer = wireFrame.layers[layer_idx];
            
            Polygons empty;
            Polygons& layer_above = (layer_idx+1 < wireFrame.layers.size())? wireFrame.layers[layer_idx+1].supported : empty;
            
            createHorizontalFill(layer, layer_above);
        }
    }
    // at this point layer.supported still only contains the polygons to be connected
    // when connecting layers, we further add the supporting polygons created by the roofs

    log("Connecting layers...\n");
    {
        Polygons* lower_top_parts = &wireFrame.bottom_outline;
        int last_z = wireFrame.z_bottom;
        for (unsigned int layer_idx = 0; layer_idx < wireFrame.layers.size(); layer_idx++) // use top of every layer but the last
        {
            WeaveLayer& layer = wireFrame.layers[layer_idx];
            
            connect_polygons(*lower_top_parts, last_z, layer.supported, layer.z1, layer);
            layer.supported.add(layer.roofs.roof_outlines);
            lower_top_parts = &layer.supported;
            
            last_z = layer.z1;
        }
    }


    { // roofs:
        if (!wireFrame.layers.empty()) //If there are no layers, create no roof.
        {
            WeaveLayer& top_layer = wireFrame.layers.back();
            Polygons to_be_supported; // empty for the top layer
            fillRoofs(top_layer.supported, to_be_supported, -1, top_layer.z1, top_layer.roofs);
        }
    }
    
    
    { // bottom:
        if (!wireFrame.layers.empty()) //If there are no layers, create no bottom.
        {
            Polygons to_be_supported; // is empty for the bottom layer, cause the order of insets doesn't really matter (in a sense everything is to be supported)
            fillRoofs(wireFrame.bottom_outline, to_be_supported, -1, wireFrame.layers.front().z0, wireFrame.bottom_infill);
        }
    }
    
}
Beispiel #10
0
LayerIndex Sprite::countLayers() const
{
  return LayerIndex(getFolder()->getLayersCount());
}
Beispiel #11
0
Layer* Sprite::layer(int layerIndex) const
{
  return indexToLayer(LayerIndex(layerIndex));
}
Beispiel #12
0
LayerIndex Sprite::lastLayer() const
{
  return LayerIndex(folder()->getLayersCount()-1);
}
Beispiel #13
0
LayerIndex Sprite::firstLayer() const
{
  return LayerIndex(0);
}
Beispiel #14
0
TEST_F(SettingsTest, AddSettingLayerIndex)
{
    settings.add("test_setting", "4");
    EXPECT_EQ(LayerIndex(3), settings.get<LayerIndex>("test_setting")) << "LayerIndex settings start counting from 0, so subtract one.";
}
Beispiel #15
0
TEST_F(SettingsTest, AddSettingLayerIndexNegative)
{
    settings.add("test_setting", "-10");
    EXPECT_EQ(LayerIndex(-11), settings.get<LayerIndex>("test_setting")) << "LayerIndex settings still subtract 1 even in negative layers.";
}