You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3309 lines
158 KiB
C++
3309 lines
158 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include "MainWindow.h"
|
|
|
|
// AZ
|
|
#include <AzCore/Serialization/SerializeContext.h>
|
|
#include <AzCore/Serialization/Utils.h>
|
|
#include <AzCore/Component/ComponentApplicationBus.h>
|
|
#include <AzCore/Component/TransformBus.h>
|
|
#include <AzCore/std/smart_ptr/make_shared.h>
|
|
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
|
#include <AzQtComponents/Buses/ShortcutDispatch.h>
|
|
#include <AzToolsFramework/API/ComponentEntityObjectBus.h>
|
|
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
|
|
#include <AzToolsFramework/Commands/EntityStateCommand.h>
|
|
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
|
|
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
|
|
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
|
|
#include <AzToolsFramework/PropertyTreeEditor/PropertyTreeEditor.h>
|
|
#include <AzToolsFramework/ToolsComponents/EditorDisabledCompositionBus.h>
|
|
#include <AzToolsFramework/ToolsComponents/EditorPendingCompositionBus.h>
|
|
#include <AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.hxx>
|
|
#include <AzToolsFramework/Undo/UndoSystem.h>
|
|
#include <IEditor.h>
|
|
|
|
// Gradient Signal
|
|
#include <GradientSignal/Ebuses/GradientPreviewContextRequestBus.h>
|
|
|
|
// Vegetation
|
|
#include <Vegetation/Editor/EditorVegetationComponentTypeIds.h>
|
|
|
|
// Qt
|
|
#include <QApplication>
|
|
#include <QStringList>
|
|
#include <QTimer>
|
|
#include <QVBoxLayout>
|
|
|
|
// GraphCanvas
|
|
#include <GraphCanvas/Components/NodePropertyDisplay/NodePropertyDisplay.h>
|
|
#include <GraphCanvas/Editor/EditorDockWidgetBus.h>
|
|
#include <GraphCanvas/Widgets/EditorContextMenu/ContextMenus/SceneContextMenu.h>
|
|
#include <GraphCanvas/Widgets/GraphCanvasEditor/GraphCanvasEditorCentralWidget.h>
|
|
#include <GraphCanvas/Widgets/GraphCanvasEditor/GraphCanvasEditorDockWidget.h>
|
|
|
|
// GraphModel
|
|
#include <GraphModel/Integration/NodePalette/GraphCanvasNodePaletteItems.h>
|
|
#include <GraphModel/Integration/NodePalette/StandardNodePaletteItem.h>
|
|
#include <GraphModel/Integration/ReadOnlyDataInterface.h>
|
|
#include <GraphModel/Model/Connection.h>
|
|
#include <GraphModel/Model/Slot.h>
|
|
|
|
// Landscape Canvas
|
|
#include <Editor/Core/Core.h>
|
|
#include <Editor/Core/GraphContext.h>
|
|
#include <Editor/Menus/LayerExtenderContextMenu.h>
|
|
#include <Editor/Menus/NodeContextMenu.h>
|
|
#include <Editor/Menus/SceneContextMenuActions.h>
|
|
#include <Editor/Nodes/Areas/AreaBlenderNode.h>
|
|
#include <Editor/Nodes/Areas/BlockerAreaNode.h>
|
|
#include <Editor/Nodes/Areas/MeshBlockerAreaNode.h>
|
|
#include <Editor/Nodes/Areas/SpawnerAreaNode.h>
|
|
#include <Editor/Nodes/AreaFilters/AltitudeFilterNode.h>
|
|
#include <Editor/Nodes/AreaFilters/DistanceBetweenFilterNode.h>
|
|
#include <Editor/Nodes/AreaFilters/DistributionFilterNode.h>
|
|
#include <Editor/Nodes/AreaFilters/ShapeIntersectionFilterNode.h>
|
|
#include <Editor/Nodes/AreaFilters/SlopeFilterNode.h>
|
|
#include <Editor/Nodes/AreaFilters/SurfaceMaskDepthFilterNode.h>
|
|
#include <Editor/Nodes/AreaFilters/SurfaceMaskFilterNode.h>
|
|
#include <Editor/Nodes/AreaModifiers/PositionModifierNode.h>
|
|
#include <Editor/Nodes/AreaModifiers/RotationModifierNode.h>
|
|
#include <Editor/Nodes/AreaModifiers/ScaleModifierNode.h>
|
|
#include <Editor/Nodes/AreaModifiers/SlopeAlignmentModifierNode.h>
|
|
#include <Editor/Nodes/AreaSelectors/AssetWeightSelectorNode.h>
|
|
#include <Editor/Nodes/Gradients/AltitudeGradientNode.h>
|
|
#include <Editor/Nodes/Gradients/ConstantGradientNode.h>
|
|
#include <Editor/Nodes/Gradients/FastNoiseGradientNode.h>
|
|
#include <Editor/Nodes/Gradients/ImageGradientNode.h>
|
|
#include <Editor/Nodes/Gradients/PerlinNoiseGradientNode.h>
|
|
#include <Editor/Nodes/Gradients/RandomNoiseGradientNode.h>
|
|
#include <Editor/Nodes/Gradients/ShapeAreaFalloffGradientNode.h>
|
|
#include <Editor/Nodes/Gradients/SlopeGradientNode.h>
|
|
#include <Editor/Nodes/Gradients/SurfaceMaskGradientNode.h>
|
|
#include <Editor/Nodes/GradientModifiers/DitherGradientModifierNode.h>
|
|
#include <Editor/Nodes/GradientModifiers/GradientMixerNode.h>
|
|
#include <Editor/Nodes/GradientModifiers/InvertGradientModifierNode.h>
|
|
#include <Editor/Nodes/GradientModifiers/LevelsGradientModifierNode.h>
|
|
#include <Editor/Nodes/GradientModifiers/PosterizeGradientModifierNode.h>
|
|
#include <Editor/Nodes/GradientModifiers/SmoothStepGradientModifierNode.h>
|
|
#include <Editor/Nodes/GradientModifiers/ThresholdGradientModifierNode.h>
|
|
#include <Editor/Nodes/Shapes/BoxShapeNode.h>
|
|
#include <Editor/Nodes/Shapes/CapsuleShapeNode.h>
|
|
#include <Editor/Nodes/Shapes/CompoundShapeNode.h>
|
|
#include <Editor/Nodes/Shapes/CylinderShapeNode.h>
|
|
#include <Editor/Nodes/Shapes/DiskShapeNode.h>
|
|
#include <Editor/Nodes/Shapes/PolygonPrismShapeNode.h>
|
|
#include <Editor/Nodes/Shapes/SphereShapeNode.h>
|
|
#include <Editor/Nodes/Shapes/TubeShapeNode.h>
|
|
#include <Editor/Nodes/UI/GradientPreviewThumbnailItem.h>
|
|
#include <EditorLandscapeCanvasComponent.h>
|
|
|
|
namespace LandscapeCanvasEditor
|
|
{
|
|
static const int NODE_OFFSET_X_PIXELS = 350;
|
|
static const int NODE_OFFSET_Y_PIXELS = 450;
|
|
static constexpr int InvalidSlotIndex = -1;
|
|
static const char* PreviewEntityElementName = "PreviewEntity";
|
|
static const char* GradientIdElementName = "GradientId";
|
|
static const char* ShapeEntityIdElementName = "ShapeEntityId";
|
|
static const char* VegetationAreaEntityIdElementName = "element";
|
|
|
|
static IEditor* GetLegacyEditor()
|
|
{
|
|
IEditor* editor = nullptr;
|
|
AzToolsFramework::EditorRequestBus::BroadcastResult(
|
|
editor, &AzToolsFramework::EditorRequestBus::Events::GetEditor);
|
|
return editor;
|
|
}
|
|
|
|
struct NodePoint
|
|
{
|
|
NodePoint* parent = nullptr;
|
|
GraphModel::NodePtr node = nullptr;
|
|
AZ::EntityId vegetationEntityId;
|
|
AZStd::vector<NodePoint*> children;
|
|
};
|
|
|
|
NodePoint* FindNodePoint(const AZStd::vector<NodePoint*>& points, const AZStd::unordered_map<AZ::EntityId, GraphModel::NodePtrList>& nodeWrappings, GraphModel::NodePtr node)
|
|
{
|
|
for (auto it : points)
|
|
{
|
|
if (it->node == node)
|
|
{
|
|
return it;
|
|
}
|
|
// Wrapped nodes don't get their own NodePoint, so if we find a wrapper node
|
|
// we need to check if any of its wrapped nodes match the node we are
|
|
// looking for as well, since they will be in the same position as
|
|
// their wrapper node parent
|
|
else if (it->node->GetNodeType() == GraphModel::NodeType::WrapperNode)
|
|
{
|
|
const AZ::EntityId& entityId = it->vegetationEntityId;
|
|
auto nodeWrapIt = nodeWrappings.find(entityId);
|
|
if (nodeWrapIt != nodeWrappings.end())
|
|
{
|
|
const auto& wrappedNodes = nodeWrapIt->second;
|
|
for (auto wrappedNode : wrappedNodes)
|
|
{
|
|
if (wrappedNode == node)
|
|
{
|
|
return it;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
AZ::Vector2 PlaceNodes(const AZ::EntityId& sceneId, NodePoint* point, AZ::Vector2 offset)
|
|
{
|
|
if (!point)
|
|
{
|
|
return offset;
|
|
}
|
|
|
|
if (point->node)
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(sceneId, &GraphModelIntegration::GraphControllerRequests::AddNode, point->node, offset);
|
|
offset.SetX(offset.GetX() + NODE_OFFSET_X_PIXELS);
|
|
}
|
|
|
|
size_t numChildren = point->children.size();
|
|
if (numChildren)
|
|
{
|
|
for (int i = 0; i < numChildren; ++i)
|
|
{
|
|
// Update the y-coordinate of our offset from any nodes placed by our child so that
|
|
// any subsequent nodes will be placed below them
|
|
AZ::Vector2 childOffset = PlaceNodes(sceneId, point->children[i], offset);
|
|
offset.SetY(childOffset.GetY());
|
|
|
|
// Start a new "row" if this node has any more children that need room
|
|
if (i < numChildren - 1)
|
|
{
|
|
offset.SetY(offset.GetY() + NODE_OFFSET_Y_PIXELS);
|
|
}
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
AZ::TypeId PickComponentTypeIdToAdd(const AzToolsFramework::ComponentPaletteUtil::ComponentDataTable& componentDataTable)
|
|
{
|
|
using namespace AzToolsFramework;
|
|
|
|
static const QStringList preferredCategories = {
|
|
"Vegetation",
|
|
"Atom"
|
|
};
|
|
|
|
// There are a couple of cases where we prefer certain categories of Components
|
|
// to be added over others (e.g. a Vegetation Shape Reference instead of actual LmbrCentral shapes),
|
|
// so if those there are components in those categories, then choose them first.
|
|
// Otherwise, just pick the first one in the list.
|
|
ComponentPaletteUtil::ComponentDataTable::const_iterator categoryIt;
|
|
for (const auto& categoryName : preferredCategories)
|
|
{
|
|
categoryIt = componentDataTable.find(categoryName);
|
|
if (categoryIt != componentDataTable.end())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (categoryIt == componentDataTable.end())
|
|
{
|
|
categoryIt = componentDataTable.begin();
|
|
}
|
|
|
|
AZ_Assert(categoryIt->second.size(), "No components found that satisfy the missing required service(s).");
|
|
|
|
const auto& componentPair = categoryIt->second.begin();
|
|
return componentPair->second->m_typeId;
|
|
}
|
|
|
|
AZ::Entity* CloneSingleEntity(AZ::Entity* entity)
|
|
{
|
|
// Helper method for cloning a single Entity. This Entity won't be registered with the Editor context
|
|
// since we are just going to save it for copy/paste purposes.
|
|
AZ::SliceComponent::EntityIdToEntityIdMap cloneEntityIdMap;
|
|
AZ::EntityUtils::SerializableEntityContainer sourceObjects;
|
|
sourceObjects.m_entities.push_back(entity);
|
|
|
|
AZ::EntityUtils::SerializableEntityContainer* clonedObjects = AZ::IdUtils::Remapper<AZ::EntityId>::CloneObjectAndGenerateNewIdsAndFixRefs(&sourceObjects, cloneEntityIdMap);
|
|
if (!clonedObjects)
|
|
{
|
|
AZ_Error("EditorEntityContext", false, "Failed to clone source entities.");
|
|
return nullptr;
|
|
}
|
|
|
|
AzToolsFramework::EntityList clonedEntities = clonedObjects->m_entities;
|
|
|
|
// We need to clean this up ourselves because CloneObjectAndGenerateNewIdsAndFixRefs assumes the caller takes ownership
|
|
delete clonedObjects;
|
|
|
|
AZ_Assert(clonedEntities.size() == 1, "Expected to only clone a single Entity");
|
|
return clonedEntities[0];
|
|
}
|
|
|
|
CustomEntityPropertyEditor::CustomEntityPropertyEditor(QWidget* parent)
|
|
: AzToolsFramework::EntityPropertyEditor(parent)
|
|
{
|
|
}
|
|
|
|
void CustomEntityPropertyEditor::CloseInspectorWindow()
|
|
{
|
|
// Override this to be empty, since our custom instance of this pinned inspector
|
|
// doesn't need to be closed when the context resets
|
|
}
|
|
|
|
QString CustomEntityPropertyEditor::GetEntityDetailsLabelText() const
|
|
{
|
|
return QObject::tr("Select a node to show its properties in the inspector.");
|
|
}
|
|
|
|
CustomNodeInspectorDockWidget::CustomNodeInspectorDockWidget(QWidget* parent)
|
|
: AzQtComponents::StyledDockWidget(parent)
|
|
{
|
|
QVBoxLayout* layout = new QVBoxLayout();
|
|
|
|
// Our custom Node Inspector is just a Pinned Inspector that by default is
|
|
// pointed to an invalid EntityId, so it won't follow the Editor selection
|
|
m_propertyEditor = new CustomEntityPropertyEditor(this);
|
|
m_propertyEditor->SetOverrideEntityIds({ AZ::EntityId() });
|
|
layout->addWidget(m_propertyEditor);
|
|
|
|
QWidget* host = new QWidget(this);
|
|
host->setLayout(layout);
|
|
setWidget(host);
|
|
|
|
setObjectName("TempNodeInspector");
|
|
setWindowTitle(QObject::tr("Node Inspector"));
|
|
}
|
|
|
|
CustomEntityPropertyEditor* CustomNodeInspectorDockWidget::GetEntityPropertyEditor()
|
|
{
|
|
return m_propertyEditor;
|
|
}
|
|
|
|
#define REGISTER_NODE_PALETTE_ITEM(category, TYPE, editorId) \
|
|
category->CreateChildNode<GraphModelIntegration::StandardNodePaletteItem<TYPE>>(TYPE::TITLE.toUtf8().constData(), editorId);
|
|
|
|
GraphCanvas::GraphCanvasTreeItem* LandscapeCanvasConfig::CreateNodePaletteRoot()
|
|
{
|
|
using namespace LandscapeCanvas;
|
|
|
|
const GraphCanvas::EditorId& editorId = LANDSCAPE_CANVAS_EDITOR_ID;
|
|
GraphCanvas::NodePaletteTreeItem* rootItem = aznew GraphCanvas::NodePaletteTreeItem("Root", editorId);
|
|
|
|
// Vegetation Areas
|
|
GraphCanvas::IconDecoratedNodePaletteTreeItem* areaCategory = rootItem->CreateChildNode<GraphCanvas::IconDecoratedNodePaletteTreeItem>("Vegetation Areas", editorId);
|
|
areaCategory->SetTitlePalette("VegetationAreaNodeTitlePalette");
|
|
REGISTER_NODE_PALETTE_ITEM(areaCategory, AreaBlenderNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(areaCategory, BlockerAreaNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(areaCategory, MeshBlockerAreaNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(areaCategory, SpawnerAreaNode, editorId);
|
|
|
|
// Gradients
|
|
GraphCanvas::IconDecoratedNodePaletteTreeItem* gradientCategory = rootItem->CreateChildNode<GraphCanvas::IconDecoratedNodePaletteTreeItem>("Gradients", editorId);
|
|
gradientCategory->SetTitlePalette("GradientNodeTitlePalette");
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, AltitudeGradientNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, ConstantGradientNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, ImageGradientNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, PerlinNoiseGradientNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, RandomNoiseGradientNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, ShapeAreaFalloffGradientNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, SlopeGradientNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, SurfaceMaskGradientNode, editorId);
|
|
|
|
// Don't give the option for the Fast Noise Gradient if the gem isn't present.
|
|
bool fastNoiseGemIsPresent = AzToolsFramework::IsComponentWithServiceRegistered(AZ_CRC("FastNoiseService", 0x93845780));
|
|
if (fastNoiseGemIsPresent)
|
|
{
|
|
REGISTER_NODE_PALETTE_ITEM(gradientCategory, FastNoiseGradientNode, editorId);
|
|
}
|
|
|
|
// Gradient Modifiers
|
|
GraphCanvas::IconDecoratedNodePaletteTreeItem* gradientModifierCategory = rootItem->CreateChildNode<GraphCanvas::IconDecoratedNodePaletteTreeItem>("Gradient Modifiers", editorId);
|
|
gradientModifierCategory->SetTitlePalette("GradientModifierNodeTitlePalette");
|
|
REGISTER_NODE_PALETTE_ITEM(gradientModifierCategory, DitherGradientModifierNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientModifierCategory, GradientMixerNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientModifierCategory, InvertGradientModifierNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientModifierCategory, LevelsGradientModifierNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientModifierCategory, PosterizeGradientModifierNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientModifierCategory, SmoothStepGradientModifierNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(gradientModifierCategory, ThresholdGradientModifierNode, editorId);
|
|
|
|
// Shapes
|
|
GraphCanvas::IconDecoratedNodePaletteTreeItem* shapeCategory = rootItem->CreateChildNode<GraphCanvas::IconDecoratedNodePaletteTreeItem>("Shapes", editorId);
|
|
shapeCategory->SetTitlePalette("ShapeNodeTitlePalette");
|
|
REGISTER_NODE_PALETTE_ITEM(shapeCategory, BoxShapeNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(shapeCategory, CapsuleShapeNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(shapeCategory, CompoundShapeNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(shapeCategory, CylinderShapeNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(shapeCategory, DiskShapeNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(shapeCategory, PolygonPrismShapeNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(shapeCategory, SphereShapeNode, editorId);
|
|
REGISTER_NODE_PALETTE_ITEM(shapeCategory, TubeShapeNode, editorId);
|
|
|
|
GraphModelIntegration::AddCommonNodePaletteUtilities(rootItem, editorId);
|
|
|
|
return rootItem;
|
|
}
|
|
|
|
#undef REGISTER_NODE_PALETTE_ITEM
|
|
|
|
// Don't register nodes whose corresponding component already exists on the given Entity so that we
|
|
// can prevent the user from adding extender nodes that would leave components in an incompatible state
|
|
#define REGISTER_NODE_PALETTE_ITEM_UNIQUE(category, TYPE, editorId, entityId) \
|
|
{ \
|
|
AZ::TypeId componentTypeId; \
|
|
LandscapeCanvas::LandscapeCanvasNodeFactoryRequestBus::BroadcastResult(componentTypeId, &LandscapeCanvas::LandscapeCanvasNodeFactoryRequests::GetComponentTypeId, azrtti_typeid<TYPE>()); \
|
|
if (!AzToolsFramework::EntityHasComponentOfType(entityId, componentTypeId)) \
|
|
{ \
|
|
category->CreateChildNode<GraphModelIntegration::StandardNodePaletteItem<TYPE>>(TYPE::TITLE.toUtf8().constData(), editorId); \
|
|
} \
|
|
} \
|
|
|
|
GraphCanvas::GraphCanvasTreeItem* GetAreaExtendersNodePaletteRoot(const GraphCanvas::EditorId& editorId, AZ::EntityId entityId)
|
|
{
|
|
using namespace LandscapeCanvas;
|
|
GraphCanvas::NodePaletteTreeItem* rootItem = aznew GraphCanvas::NodePaletteTreeItem("Root", editorId);
|
|
|
|
// Filters
|
|
GraphCanvas::IconDecoratedNodePaletteTreeItem* filtersCategory = rootItem->CreateChildNode<GraphCanvas::IconDecoratedNodePaletteTreeItem>("Filters", editorId);
|
|
filtersCategory->SetTitlePalette("VegetationAreaNodeTitlePalette");
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(filtersCategory, AltitudeFilterNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(filtersCategory, DistanceBetweenFilterNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(filtersCategory, DistributionFilterNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(filtersCategory, ShapeIntersectionFilterNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(filtersCategory, SlopeFilterNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(filtersCategory, SurfaceMaskDepthFilterNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(filtersCategory, SurfaceMaskFilterNode, editorId, entityId);
|
|
|
|
// Modifiers
|
|
GraphCanvas::IconDecoratedNodePaletteTreeItem* modifiersCategory = rootItem->CreateChildNode<GraphCanvas::IconDecoratedNodePaletteTreeItem>("Modifiers", editorId);
|
|
modifiersCategory->SetTitlePalette("VegetationAreaNodeTitlePalette");
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(modifiersCategory, PositionModifierNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(modifiersCategory, RotationModifierNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(modifiersCategory, ScaleModifierNode, editorId, entityId);
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(modifiersCategory, SlopeAlignmentModifierNode, editorId, entityId);
|
|
|
|
// Selectors
|
|
GraphCanvas::IconDecoratedNodePaletteTreeItem* selectorsCategory = rootItem->CreateChildNode<GraphCanvas::IconDecoratedNodePaletteTreeItem>("Selectors", editorId);
|
|
selectorsCategory->SetTitlePalette("VegetationAreaNodeTitlePalette");
|
|
REGISTER_NODE_PALETTE_ITEM_UNIQUE(selectorsCategory, AssetWeightSelectorNode, editorId, entityId);
|
|
|
|
// Remove any category entries that wind up with no sub-items
|
|
for (GraphCanvas::NodePaletteTreeItem* category : { filtersCategory, modifiersCategory, selectorsCategory })
|
|
{
|
|
if (category && category->GetChildCount() <= 0)
|
|
{
|
|
category->DetachItem();
|
|
}
|
|
}
|
|
|
|
return rootItem;
|
|
}
|
|
|
|
#undef REGISTER_NODE_PALETTE_ITEM_UNIQUE
|
|
|
|
LandscapeCanvasConfig* GetDefaultConfig()
|
|
{
|
|
LandscapeCanvasConfig* config = new LandscapeCanvasConfig();
|
|
config->m_editorId = LandscapeCanvas::LANDSCAPE_CANVAS_EDITOR_ID;
|
|
config->m_baseStyleSheet = "LandscapeCanvas/StyleSheet/graphcanvas_style.json";
|
|
config->m_mimeType = LandscapeCanvas::MIME_EVENT_TYPE;
|
|
config->m_saveIdentifier = LandscapeCanvas::SAVE_IDENTIFIER;
|
|
|
|
return config;
|
|
}
|
|
|
|
MainWindow::MainWindow(QWidget* parent)
|
|
: GraphModelIntegration::EditorMainWindow(GetDefaultConfig(), parent)
|
|
{
|
|
// Map the desired layout order for our wrapped nodes so they always
|
|
// show up in the same order, regardless of when the corresponding component was
|
|
// added to the Entity
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorPositionModifierComponentTypeId] = 0;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorRotationModifierComponentTypeId] = 1;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorScaleModifierComponentTypeId] = 2;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorSlopeAlignmentModifierComponentTypeId] = 3;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorSurfaceAltitudeFilterComponentTypeId] = 4;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorDistanceBetweenFilterComponentTypeId] = 5;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorDistributionFilterComponentTypeId] = 6;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorShapeIntersectionFilterComponentTypeId] = 7;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorSurfaceSlopeFilterComponentTypeId] = 8;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorSurfaceMaskDepthFilterComponentTypeId] = 9;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorSurfaceMaskFilterComponentTypeId] = 10;
|
|
m_wrappedNodeLayoutOrderMap[Vegetation::EditorDescriptorWeightSelectorComponentTypeId] = 11;
|
|
|
|
AZ::ComponentApplicationBus::BroadcastResult(m_serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
|
|
AZ_Assert(m_serializeContext, "Failed to acquire application serialize context.");
|
|
|
|
m_prefabFocusPublicInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabFocusPublicInterface>::Get();
|
|
AZ_Assert(m_prefabFocusPublicInterface, "LandscapeCanvas - could not get PrefabFocusPublicInterface on construction.");
|
|
|
|
const GraphCanvas::EditorId& editorId = GetEditorId();
|
|
|
|
// Register unique color palettes for our connections (data types)
|
|
GraphCanvas::StyleManagerRequestBus::Event(editorId, &GraphCanvas::StyleManagerRequests::RegisterDataPaletteStyle, LandscapeCanvas::BoundsTypeId, "BoundsDataColorPalette");
|
|
GraphCanvas::StyleManagerRequestBus::Event(editorId, &GraphCanvas::StyleManagerRequests::RegisterDataPaletteStyle, LandscapeCanvas::GradientTypeId, "GradientDataColorPalette");
|
|
GraphCanvas::StyleManagerRequestBus::Event(editorId, &GraphCanvas::StyleManagerRequests::RegisterDataPaletteStyle, LandscapeCanvas::AreaTypeId, "VegetationAreaDataColorPalette");
|
|
|
|
LandscapeCanvas::LandscapeCanvasRequestBus::Handler::BusConnect();
|
|
AzToolsFramework::EditorPickModeNotificationBus::Handler::BusConnect(AzToolsFramework::GetEntityContextId());
|
|
AzToolsFramework::EntityCompositionNotificationBus::Handler::BusConnect();
|
|
AzToolsFramework::ToolsApplicationNotificationBus::Handler::BusConnect();
|
|
AzToolsFramework::Prefab::PrefabFocusNotificationBus::Handler::BusConnect(AzToolsFramework::GetEntityContextId());
|
|
AzToolsFramework::Prefab::PrefabPublicNotificationBus::Handler::BusConnect();
|
|
CrySystemEventBus::Handler::BusConnect();
|
|
AZ::EntitySystemBus::Handler::BusConnect();
|
|
|
|
// Listen for Entity notifications if a level is already loaded
|
|
// Otherwise, we will connect/disconnect from this bus when levels are loaded/closed
|
|
if (GetLegacyEditor()->IsLevelLoaded())
|
|
{
|
|
AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusConnect();
|
|
}
|
|
|
|
// Create our temporary Node Inspector using a Pinned Inspector
|
|
m_customNodeInspector = aznew CustomNodeInspectorDockWidget(this);
|
|
|
|
// Add our custom action to the scene context menu
|
|
m_sceneContextMenu->AddMenuAction(aznew FindSelectedNodesAction(this));
|
|
|
|
UpdateGraphEnabled();
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
AZ::EntitySystemBus::Handler::BusDisconnect();
|
|
CrySystemEventBus::Handler::BusDisconnect();
|
|
AzToolsFramework::Prefab::PrefabPublicNotificationBus::Handler::BusDisconnect();
|
|
AzToolsFramework::Prefab::PrefabFocusNotificationBus::Handler::BusDisconnect();
|
|
AzToolsFramework::ToolsApplicationNotificationBus::Handler::BusDisconnect();
|
|
AzToolsFramework::EditorPickModeNotificationBus::Handler::BusDisconnect();
|
|
AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusDisconnect();
|
|
AzToolsFramework::EntityCompositionNotificationBus::Handler::BusConnect();
|
|
AzToolsFramework::PropertyEditorEntityChangeNotificationBus::MultiHandler::BusDisconnect();
|
|
LandscapeCanvas::LandscapeCanvasRequestBus::Handler::BusDisconnect();
|
|
}
|
|
|
|
GraphModel::IGraphContextPtr MainWindow::GetGraphContext() const
|
|
{
|
|
return LandscapeCanvas::GraphContext::GetInstance();
|
|
}
|
|
|
|
void MainWindow::OnGraphModelNodeAdded(GraphModel::NodePtr node)
|
|
{
|
|
// If we weren't graphing a scene, then this new node was dragged in from the Node Palette,
|
|
// so we need to create the appropriate underlying Entity/Component(s)
|
|
if (!m_ignoreGraphUpdates)
|
|
{
|
|
HandleNodeCreated(node);
|
|
}
|
|
|
|
// Handle any custom logic when a node is added to the graph (e.g. adding thumbnails)
|
|
HandleNodeAdded(node);
|
|
}
|
|
|
|
void MainWindow::OnGraphModelNodeRemoved(GraphModel::NodePtr node)
|
|
{
|
|
// Remove the cached EntityId mapping for this node
|
|
GraphCanvas::GraphId graphId = (*GraphModelIntegration::GraphControllerNotificationBus::GetCurrentBusId());
|
|
auto nodeMap = GetEntityIdNodeMap(graphId, node);
|
|
if (nodeMap)
|
|
{
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
const AZ::EntityId& entityId = baseNodePtr->GetVegetationEntityId();
|
|
auto it = nodeMap->find(entityId);
|
|
if (it != nodeMap->end())
|
|
{
|
|
nodeMap->erase(it);
|
|
}
|
|
}
|
|
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If an area filter or modifier is removed, then only delete the underlying component.
|
|
// Otherwise, delete the whole underlying Entity when the node is removed.
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (baseNodePtr->IsAreaExtender())
|
|
{
|
|
AZ::Component* component = baseNodePtr->GetComponent();
|
|
if (component)
|
|
{
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
AzToolsFramework::RemoveComponents(component);
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Don't use the m_ignoreGraphUpdates guard here because we want descendant Entities that get deleted
|
|
// to remove their corresponding nodes from the graph as well to stay in sync
|
|
const AZ::EntityId& entityId = baseNodePtr->GetVegetationEntityId();
|
|
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(&AzToolsFramework::ToolsApplicationRequests::DeleteEntityAndAllDescendants, entityId);
|
|
}
|
|
}
|
|
|
|
void MainWindow::PreOnGraphModelNodeRemoved(GraphModel::NodePtr node)
|
|
{
|
|
GraphCanvas::GraphId graphId = (*GraphModelIntegration::GraphControllerNotificationBus::GetCurrentBusId());
|
|
|
|
// Before a node gets removed from the graph, save off its position
|
|
// so that we can restore it to its previous spot if it ends up
|
|
// being added back via Undo
|
|
auto it = m_deletedNodePositions.find(graphId);
|
|
if (it != m_deletedNodePositions.end())
|
|
{
|
|
DeletedNodePositionsMap& deletedNodePositionMap = it->second;
|
|
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
AZ::EntityComponentIdPair pair(baseNodePtr->GetVegetationEntityId(), baseNodePtr->GetComponentId());
|
|
|
|
AZ::Vector2 position;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(position, graphId, &GraphModelIntegration::GraphControllerRequests::GetPosition, node);
|
|
deletedNodePositionMap[pair] = position;
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnGraphModelConnectionAdded(GraphModel::ConnectionPtr connection)
|
|
{
|
|
// Don't need to act on connections that aren't added by the user
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdateConnectionData(connection, true /* added */);
|
|
}
|
|
|
|
void MainWindow::OnGraphModelConnectionRemoved(GraphModel::ConnectionPtr connection)
|
|
{
|
|
// Don't need to act on connections that aren't removed by the user
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdateConnectionData(connection, false /* added */);
|
|
}
|
|
|
|
void MainWindow::OnGraphModelNodeWrapped(GraphModel::NodePtr wrapperNode, GraphModel::NodePtr node)
|
|
{
|
|
// We only need to add components when nodes are created by the user,
|
|
// not when we are parsing/graphing an existing setup
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We don't need to create a new component for nodes that already
|
|
// have a component tied to them, which happens when nodes get deserialized
|
|
// and OnNodeWrapped gets invoked
|
|
auto wrappedNode = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (wrappedNode->GetComponentId() != AZ::InvalidComponentId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// When a node is wrapped (e.g. filter/modifier added to a layer area), then we will
|
|
// add the Component to the Entity of the wrapper node
|
|
auto* sourceNode = static_cast<LandscapeCanvas::BaseNode*>(wrapperNode.get());
|
|
AZ::EntityId vegetationEntityId = sourceNode->GetVegetationEntityId();
|
|
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
AddComponentForNode(node, vegetationEntityId);
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
}
|
|
|
|
void MainWindow::OnSelectionChanged()
|
|
{
|
|
GraphCanvas::AssetEditorMainWindow::OnSelectionChanged();
|
|
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GraphModel::NodePtrList nodeList;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(nodeList, GetActiveGraphCanvasGraphId(), &GraphModelIntegration::GraphControllerRequests::GetSelectedNodes);
|
|
|
|
// Iterate through the selected nodes to find their corresponding vegetation entities
|
|
AzToolsFramework::EntityIdSet vegetationEntityIdsToSelect;
|
|
for (const auto& node : nodeList)
|
|
{
|
|
if (!node)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
vegetationEntityIdsToSelect.insert(baseNodePtr->GetVegetationEntityId());
|
|
}
|
|
|
|
// If we don't have any nodes selected, or the entities selected in the graph aren't nodes (e.g. comments, node groups)
|
|
// then show an empty Node Inspector
|
|
if (vegetationEntityIdsToSelect.empty())
|
|
{
|
|
m_customNodeInspector->GetEntityPropertyEditor()->SetOverrideEntityIds({ AZ::EntityId() });
|
|
return;
|
|
}
|
|
|
|
QTimer::singleShot(0, [this, vegetationEntityIdsToSelect]() {
|
|
// If we are in object pick mode and have selected a single node, then use the Entity for that node
|
|
// as the pick mode selection
|
|
if (m_inObjectPickMode && vegetationEntityIdsToSelect.size() == 1)
|
|
{
|
|
AZ::EntityId selectedEntityId = *vegetationEntityIdsToSelect.begin();
|
|
|
|
AzToolsFramework::EditorPickModeRequestBus::Broadcast(&AzToolsFramework::EditorPickModeRequests::PickModeSelectEntity, selectedEntityId);
|
|
AzToolsFramework::EditorPickModeRequestBus::Broadcast(&AzToolsFramework::EditorPickModeRequests::StopEntityPickMode);
|
|
}
|
|
// Otherwise, update the selection in our node inspector
|
|
else
|
|
{
|
|
m_customNodeInspector->GetEntityPropertyEditor()->SetOverrideEntityIds(vegetationEntityIdsToSelect);
|
|
}
|
|
});
|
|
}
|
|
|
|
void MainWindow::OnEntitiesSerialized(GraphCanvas::GraphSerialization& serializationTarget)
|
|
{
|
|
LandscapeCanvas::LandscapeCanvasSerialization serialization;
|
|
|
|
GraphCanvas::GraphId graphId = GetActiveGraphCanvasGraphId();
|
|
|
|
// Look for any nodes being serialized for which we also need to serialize the Editor Entity
|
|
// corresponding to our Landscape Canvas node
|
|
for (AZ::Entity* nodeEntity : serializationTarget.GetGraphData().m_nodes)
|
|
{
|
|
GraphCanvas::NodeId nodeUiId = nodeEntity->GetId();
|
|
|
|
// Ignore any nodes serialized by GraphCanvas that aren't GraphModel nodes (e.g. comments/node groups), since they
|
|
// don't have an actual Entity/Component tied to them that we'll need to copy
|
|
GraphModel::NodePtr node;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(node, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodeById, nodeUiId);
|
|
if (!node)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
AZ::EntityId entityId = baseNodePtr->GetVegetationEntityId();
|
|
|
|
// There could be multiple nodes being serialized that are tied to the same Entity.
|
|
// We only need to serialize the Entity once since all its components will be serialized as well.
|
|
auto it = serialization.m_serializedNodeEntities.find(entityId);
|
|
if (it != serialization.m_serializedNodeEntities.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
|
|
AZ_Assert(entity, "Entity corresponding to node is not registered with the application");
|
|
|
|
// Clone the Entity and save it with the mapping of its original EntityId since the copied node
|
|
// will stil have a reference to the original EntityId. We have to clone the Entity instead of
|
|
// just saving the EntityId and making a copy later because if this serialization was caused by
|
|
// a Cut operation, then the Entity will actually be deleted when the node is deleted
|
|
AZ::Entity* clonedEntity = CloneSingleEntity(entity);
|
|
serialization.m_serializedNodeEntities[entityId] = clonedEntity;
|
|
}
|
|
|
|
LandscapeCanvas::LandscapeCanvasSerializationRequestBus::Broadcast(&LandscapeCanvas::LandscapeCanvasSerializationRequests::SetSerializedNodeEntities, serialization.m_serializedNodeEntities);
|
|
}
|
|
|
|
void MainWindow::OnEntitiesDeserialized(const GraphCanvas::GraphSerialization&)
|
|
{
|
|
using namespace AzToolsFramework;
|
|
using namespace LandscapeCanvas;
|
|
|
|
// We need to ignore the graph updates here because adding the cloned Entity to the EditorContext
|
|
// will trigger OnEditorEntityCreated
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
ScopedUndoBatch undoBatch("Entities Deserialized");
|
|
|
|
LandscapeCanvasSerialization serialization;
|
|
LandscapeCanvasSerializationRequestBus::BroadcastResult(serialization, &LandscapeCanvasSerializationRequests::GetSerializedMappings);
|
|
|
|
for (auto it : serialization.m_serializedNodeEntities)
|
|
{
|
|
AZ::EntityId originalSerializedEntityId = it.first;
|
|
AZ::Entity* serializedEntity = it.second;
|
|
|
|
// Even though we cloned the original serialized Entity when we saved it in m_serializedNodeEntities,
|
|
// we need to clone it again when we actually deserialize, because the user could paste/duplicate
|
|
// multiple times, and each one will need a unique new Entity
|
|
AZ::Entity* clonedEntity = CloneSingleEntity(serializedEntity);
|
|
AZ::EntityId clonedEntityId = clonedEntity->GetId();
|
|
serialization.m_deserializedEntities[originalSerializedEntityId] = clonedEntityId;
|
|
|
|
// Register this new Entity with the Editor context
|
|
EditorEntityContextRequestBus::Broadcast(&EditorEntityContextRequests::AddEditorEntities, EntityList{ clonedEntity });
|
|
|
|
// If this node was copied from a different graph, then we will need
|
|
// to re-parent the cloned Entity so that it lives within the root Entity
|
|
// of our active graph, otherwise it won't know the node(Entity) belongs to
|
|
// this graph the next time it is loaded.
|
|
AZ::EntityId clonedEntityParentId;
|
|
AZ::TransformBus::EventResult(clonedEntityParentId, clonedEntityId, &AZ::TransformBus::Events::GetParentId);
|
|
GraphCanvas::GraphId clonedEntityGraphId = FindGraphContainingEntity(clonedEntityParentId);
|
|
GraphCanvas::GraphId activeGraphId = GetActiveGraphCanvasGraphId();
|
|
if (clonedEntityGraphId != activeGraphId)
|
|
{
|
|
AZ::EntityId rootEntityId = GetRootEntityIdForGraphId(activeGraphId);
|
|
if (rootEntityId.IsValid())
|
|
{
|
|
AZ::TransformBus::Event(clonedEntityId, &AZ::TransformBus::Events::SetParent, rootEntityId);
|
|
}
|
|
}
|
|
|
|
// Make sure all cloned entities are contained within the currently active undo batch command
|
|
// We have to use a EntityCreateCommand instead of just marking the Entity as dirty within
|
|
// the undo batch because of how it is cloned as opposed to being created from scratch
|
|
EntityCreateCommand* command = aznew EntityCreateCommand(
|
|
static_cast<UndoSystem::URCommandID>(clonedEntityId));
|
|
command->Capture(clonedEntity);
|
|
command->SetParent(undoBatch.GetUndoBatch());
|
|
}
|
|
|
|
LandscapeCanvas::LandscapeCanvasSerializationRequestBus::Broadcast(&LandscapeCanvas::LandscapeCanvasSerializationRequests::SetDeserializedEntities, serialization.m_deserializedEntities);
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
}
|
|
|
|
void MainWindow::OnGraphModelGraphModified(GraphModel::NodePtr node)
|
|
{
|
|
AZ_UNUSED(node);
|
|
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Flag the level as dirty if anything in the graph changes, since some graph actions
|
|
// (e.g. moving nodes around, creating bookmarks, etc...) don't trigger actual Entity/Component
|
|
// changes that would flag the level as dirty.
|
|
if (const auto editor = GetLegacyEditor();
|
|
!editor->IsModified())
|
|
{
|
|
editor->SetModifiedFlag();
|
|
editor->SetModifiedModule(eModifiedEntities);
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnEditorOpened(GraphCanvas::EditorDockWidget* dockWidget)
|
|
{
|
|
using namespace AzFramework::Terrain;
|
|
|
|
// Invoke the GraphCanvas base instead of the GraphModelIntegration::EditorMainWindow so that we
|
|
// can do our own custom handling when opening an existing graph
|
|
GraphCanvas::AssetEditorMainWindow::OnEditorOpened(dockWidget);
|
|
|
|
// If this graph was opened by File -> New or by dragging a node from
|
|
// the Node Palette onto the empty canvas, then we first need to create
|
|
// a root Entity for it with a Landscape Canvas component.
|
|
if (!m_ignoreGraphUpdates)
|
|
{
|
|
AZ::EntityId rootEntityId;
|
|
AzToolsFramework::EditorRequestBus::BroadcastResult(rootEntityId, &AzToolsFramework::EditorRequests::CreateNewEntity, AZ::EntityId());
|
|
|
|
AZ::Vector3 translation = AZ::Vector3::CreateZero();
|
|
AZ::TransformBus::EventResult(translation, rootEntityId, &AZ::TransformBus::Events::GetWorldTranslation);
|
|
|
|
// Get the terrain height at the XY world coordinate where our new Entity was created
|
|
float height = translation.GetZ();
|
|
TerrainDataRequestBus::BroadcastResult(height, &TerrainDataRequests::GetHeightFromFloats, translation.GetX(), translation.GetY(), TerrainDataRequests::Sampler::BILINEAR, nullptr);
|
|
|
|
// Update the new Entity translation so that it is placed on the terrain so that any vegetation resulting
|
|
// from it will be planted on the terrain
|
|
translation.SetZ(height);
|
|
AZ::TransformBus::Event(rootEntityId, &AZ::TransformBus::Events::SetWorldTranslation, translation);
|
|
|
|
AzToolsFramework::EntityCompositionRequestBus::Broadcast(&AzToolsFramework::EntityCompositionRequests::AddComponentsToEntities, AzToolsFramework::EntityIdList{ rootEntityId }, AZ::ComponentTypeList{ LandscapeCanvas::EditorLandscapeCanvasComponentTypeId });
|
|
|
|
HandleGraphOpened(rootEntityId, dockWidget->GetDockWidgetId());
|
|
|
|
// Update the tab name for the new graph after creating the root Entity to hold its Landscape Canvas component
|
|
AZ::Entity* rootEntity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(rootEntity, &AZ::ComponentApplicationRequests::FindEntity, rootEntityId);
|
|
AZ_Assert(rootEntity, "No Entity found for EntityId = %s", rootEntityId.ToString().c_str());
|
|
OnEntityNameChanged(rootEntityId, rootEntity->GetName());
|
|
}
|
|
|
|
// Initialize the EntityIdNodeMaps that will be used for parsing/creating connections later
|
|
GraphCanvas::GraphId graphId = dockWidget->GetGraphId();
|
|
EntityIdNodeMaps newNodeMaps;
|
|
for (int i = 0; i < EntityIdNodeMapEnum::Count; ++i)
|
|
{
|
|
newNodeMaps.push_back(EntityIdNodeMap());
|
|
}
|
|
m_entityIdNodeMapsByGraph[graphId] = newNodeMaps;
|
|
m_deletedNodePositions[graphId] = DeletedNodePositionsMap();
|
|
}
|
|
|
|
void MainWindow::OnEditorClosing(GraphCanvas::EditorDockWidget* dockWidget)
|
|
{
|
|
// Stop listening for changes to this Vegetation Entity when we close the graph for it.
|
|
GraphCanvas::DockWidgetId dockWidgetId = dockWidget->GetDockWidgetId();
|
|
auto it = AZStd::find_if(m_dockWidgetsByEntity.begin(), m_dockWidgetsByEntity.end(),
|
|
[dockWidgetId](decltype(m_dockWidgetsByEntity)::const_reference pair)
|
|
{
|
|
return dockWidgetId == pair.second;
|
|
}
|
|
);
|
|
|
|
if (it != m_dockWidgetsByEntity.end())
|
|
{
|
|
const AZ::EntityId& rootEntityId = it->first;
|
|
m_dockWidgetsByEntity.erase(it);
|
|
|
|
// Save our graph whenever it is closed
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, rootEntityId);
|
|
if (entity)
|
|
{
|
|
GraphCanvas::GraphId graphId = dockWidget->GetGraphId();
|
|
GraphCanvas::GraphModelRequestBus::Event(graphId, &GraphCanvas::GraphModelRequests::OnSaveDataDirtied, graphId);
|
|
|
|
// Serialize the graph into the Landscape Canvas component on the root Entity that
|
|
// corresponds to this graph
|
|
GraphModel::GraphPtr graph = GetGraphById(graphId);
|
|
auto landscapeCanvasComponent = azrtti_cast<LandscapeCanvas::EditorLandscapeCanvasComponent*>(entity->FindComponent(LandscapeCanvas::EditorLandscapeCanvasComponentTypeId));
|
|
if (landscapeCanvasComponent)
|
|
{
|
|
landscapeCanvasComponent->m_graph = *m_serializeContext->CloneObject(graph.get());
|
|
|
|
// Mark the Landscape Canvas entity as dirty so the changes to the graph will be picked up on the next save
|
|
AzToolsFramework::ScopedUndoBatch undo("Update Landscape Canvas Graph");
|
|
AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(&AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, rootEntityId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear out the cached EntityIdNode mapping for the graph when it is closed
|
|
GraphCanvas::GraphId graphId = dockWidget->GetGraphId();
|
|
auto nodeMapIt = m_entityIdNodeMapsByGraph.find(graphId);
|
|
if (nodeMapIt != m_entityIdNodeMapsByGraph.end())
|
|
{
|
|
m_entityIdNodeMapsByGraph.erase(nodeMapIt);
|
|
}
|
|
auto nodePositionsMapIt = m_deletedNodePositions.find(graphId);
|
|
if (nodePositionsMapIt != m_deletedNodePositions.end())
|
|
{
|
|
m_deletedNodePositions.erase(nodePositionsMapIt);
|
|
}
|
|
|
|
// Do this last so that the graph isn't closed before we get a chance to save it
|
|
GraphModelIntegration::EditorMainWindow::OnEditorClosing(dockWidget);
|
|
}
|
|
|
|
QAction* MainWindow::AddFileNewAction(QMenu* menu)
|
|
{
|
|
m_fileNewAction = GraphModelIntegration::EditorMainWindow::AddFileNewAction(menu);
|
|
|
|
// Disable our file menu action for creating a new graph if a level isn't loaded
|
|
m_fileNewAction->setEnabled(GetLegacyEditor()->IsLevelLoaded());
|
|
|
|
return m_fileNewAction;
|
|
}
|
|
|
|
QAction* MainWindow::AddFileOpenAction(QMenu* menu)
|
|
{
|
|
AZ_UNUSED(menu);
|
|
return nullptr;
|
|
}
|
|
|
|
QAction* MainWindow::AddFileSaveAction(QMenu* menu)
|
|
{
|
|
AZ_UNUSED(menu);
|
|
return nullptr;
|
|
}
|
|
|
|
QAction* MainWindow::AddFileSaveAsAction(QMenu* menu)
|
|
{
|
|
AZ_UNUSED(menu);
|
|
return nullptr;
|
|
}
|
|
|
|
QMenu* MainWindow::AddEditMenu()
|
|
{
|
|
QMenu* menu = GraphModelIntegration::EditorMainWindow::AddEditMenu();
|
|
|
|
// Temporarily add our own Undo/Redo menu actions that will just trigger the main Editor's
|
|
// Undo/Redo actions, since our graphs are listening/responding to Editor Entity/Component
|
|
// changes (e.g. entities/components being added/removed).
|
|
// Once our generic GraphModel windowing framework supports Undo/Redo then we will extend
|
|
// the GraphModel::EditorMainWindow to provide the Undo/Redo menu actions by default.
|
|
if (menu && !menu->actions().empty())
|
|
{
|
|
auto separatorAction = menu->insertSeparator(menu->actions().first());
|
|
|
|
auto redoAction = new QAction(QObject::tr("&Redo"), this);
|
|
redoAction->setShortcut(AzQtComponents::RedoKeySequence);
|
|
QObject::connect(redoAction, &QAction::triggered, [] {
|
|
GetLegacyEditor()->Redo();
|
|
});
|
|
menu->insertAction(separatorAction, redoAction);
|
|
|
|
auto undoAction = new QAction(QObject::tr("&Undo"), this);
|
|
undoAction->setShortcut(QKeySequence::Undo);
|
|
QObject::connect(undoAction, &QAction::triggered, [] {
|
|
GetLegacyEditor()->Undo();
|
|
});
|
|
menu->insertAction(redoAction, undoAction);
|
|
}
|
|
|
|
return menu;
|
|
}
|
|
|
|
void MainWindow::HandleWrapperNodeActionWidgetClicked(GraphModel::NodePtr wrapperNode, [[maybe_unused]] const QRect& actionWidgetBoundingRect, const QPointF& scenePoint, const QPoint& screenPoint)
|
|
{
|
|
auto baseNode = static_cast<LandscapeCanvas::BaseNode*>(wrapperNode.get());
|
|
AZ::EntityId entityId = baseNode->GetVegetationEntityId();
|
|
|
|
GraphCanvas::NodePaletteConfig config;
|
|
config.m_rootTreeItem = GetAreaExtendersNodePaletteRoot(GetEditorId(), entityId);
|
|
config.m_editorId = GetEditorId();
|
|
config.m_mimeType = LandscapeCanvas::MIME_EVENT_TYPE;
|
|
config.m_isInContextMenu = true;
|
|
config.m_saveIdentifier = LandscapeCanvas::CONTEXT_MENU_SAVE_IDENTIFIER;
|
|
|
|
// Create the Context Menu with embedded Node Palette for adding Filters/Modifiers to Layers
|
|
// The ownership of this Node Palette is passed to the context menu
|
|
LayerExtenderContextMenu menu(config, this);
|
|
menu.exec(screenPoint);
|
|
|
|
// Check if a node was selected in the Node Palette of our context menu.
|
|
// If the menu was dismissed, then the mime event will be null.
|
|
GraphCanvas::GraphCanvasMimeEvent* mimeEvent = menu.GetNodePalette()->GetContextMenuEvent();
|
|
if (mimeEvent)
|
|
{
|
|
GraphCanvas::GraphId graphId = GetActiveGraphCanvasGraphId();
|
|
AZ::Vector2 dropPos(aznumeric_cast<float>(scenePoint.x()), aznumeric_cast<float>(scenePoint.y()));
|
|
|
|
// Create the node that was selected from the node palette.
|
|
if (mimeEvent->ExecuteEvent(dropPos, dropPos, graphId))
|
|
{
|
|
GraphCanvas::NodeId nodeId = mimeEvent->GetCreatedNodeId();
|
|
|
|
GraphModel::NodePtr node;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(node, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodeById, nodeId);
|
|
|
|
// Wrap the created Filter or Modifier node on its parent layer node.
|
|
AZ::u32 layoutOrder = GetWrappedNodeLayoutOrder(node);
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::WrapNodeOrdered, wrapperNode, node, layoutOrder);
|
|
}
|
|
}
|
|
}
|
|
|
|
GraphCanvas::Endpoint MainWindow::CreateNodeForProposal(const AZ::EntityId& connectionId, const GraphCanvas::Endpoint& endpoint, const QPointF& scenePoint, const QPoint& screenPoint)
|
|
{
|
|
GraphCanvas::Endpoint createdEndpoint = GraphCanvas::AssetEditorMainWindow::CreateNodeForProposal(connectionId, endpoint, scenePoint, screenPoint);
|
|
|
|
if (createdEndpoint.IsValid())
|
|
{
|
|
GraphModel::NodePtr sourceNode;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(sourceNode, GetActiveGraphCanvasGraphId(), &GraphModelIntegration::GraphControllerRequests::GetNodeById, endpoint.GetNodeId());
|
|
|
|
GraphModel::NodePtr createdNode;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(createdNode, GetActiveGraphCanvasGraphId(), &GraphModelIntegration::GraphControllerRequests::GetNodeById, createdEndpoint.GetNodeId());
|
|
|
|
AZ_Assert(sourceNode && createdNode, "Unable to find GraphModel::Node for associated Endpoint.");
|
|
|
|
// If the source node and the created node both have preview bounds slots,
|
|
// then automatically connect the preview bounds on the created node to the
|
|
// same slot as the one on the source node (if it is connected to something)
|
|
GraphModel::SlotPtr sourcePreviewBoundsSlot = sourceNode->GetSlot(LandscapeCanvas::PREVIEW_BOUNDS_SLOT_ID);
|
|
GraphModel::SlotPtr createdPreviewBoundsSlot = createdNode->GetSlot(LandscapeCanvas::PREVIEW_BOUNDS_SLOT_ID);
|
|
if (sourcePreviewBoundsSlot && createdPreviewBoundsSlot)
|
|
{
|
|
// The preview bounds is an input slot, so it will only have 1 connection (if any)
|
|
GraphModel::Slot::ConnectionList connections = sourcePreviewBoundsSlot->GetConnections();
|
|
if (connections.size() == 1)
|
|
{
|
|
GraphModel::ConnectionPtr connection = *connections.begin();
|
|
GraphModel::SlotPtr previewBoundsSourceSlot = connection->GetSourceSlot();
|
|
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(GetActiveGraphCanvasGraphId(), &GraphModelIntegration::GraphControllerRequests::AddConnection, previewBoundsSourceSlot, createdPreviewBoundsSlot);
|
|
}
|
|
}
|
|
}
|
|
|
|
return createdEndpoint;
|
|
}
|
|
|
|
void MainWindow::OnEntityActivated(const AZ::EntityId& entityId)
|
|
{
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We already handle when components are explicitly enabled/disabled, but if they are enabled/disabled
|
|
// as a result of their dependencies being enabled/disabled, then we don't get an explicit
|
|
// notification for that action. The Entity Inspector also uses this EntitySystemBus::OnEntityActivated
|
|
// to determine when to re-check component state, since the Entity gets deactivated/re-activated when
|
|
// making component changes, so this is when we should update the enabled/disabled state of any nodes
|
|
// associated with this Entity.
|
|
GraphModel::NodePtrList matchingNodes = GetAllNodesMatchingEntity(entityId);
|
|
for (auto node : matchingNodes)
|
|
{
|
|
GraphCanvas::GraphId graphId = GetGraphId(node->GetGraph());
|
|
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (baseNodePtr->GetComponent())
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::EnableNode, node);
|
|
}
|
|
else
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::DisableNode, node);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnEntityNameChanged(const AZ::EntityId& entityId, const AZStd::string& name)
|
|
{
|
|
auto it = m_dockWidgetsByEntity.find(entityId);
|
|
if (it == m_dockWidgetsByEntity.end())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Update the tab name for the graph corresponding to this Vegetation Entity
|
|
GraphCanvas::DockWidgetId dockWidgetId = it->second;
|
|
GraphCanvas::EditorDockWidgetRequestBus::Event(dockWidgetId, &GraphCanvas::EditorDockWidgetRequests::SetTitle, name);
|
|
}
|
|
|
|
bool MainWindow::HandleGraphOpened(const AZ::EntityId& rootEntityId, const GraphCanvas::DockWidgetId& dockWidgetId)
|
|
{
|
|
// Keep track of the dock widget created for this root Vegetation Entity, and
|
|
// listen for any changes to the entity
|
|
m_dockWidgetsByEntity[rootEntityId] = dockWidgetId;
|
|
|
|
GraphCanvas::GraphId graphId;
|
|
GraphCanvas::EditorDockWidgetRequestBus::EventResult(graphId, dockWidgetId, &GraphCanvas::EditorDockWidgetRequests::GetGraphId);
|
|
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, rootEntityId);
|
|
AZ_Assert(entity, "No Entity found for EntityId = %s", rootEntityId.ToString().c_str());
|
|
|
|
auto landscapeCanvasComponent = azrtti_cast<LandscapeCanvas::EditorLandscapeCanvasComponent*>(entity->FindComponent(LandscapeCanvas::EditorLandscapeCanvasComponentTypeId));
|
|
AZ_Assert(landscapeCanvasComponent, "Missing Landscape Canvas component on EntityId = %s", rootEntityId.ToString().c_str());
|
|
|
|
bool isNewGraph = false;
|
|
GraphModel::GraphPtr graph = AZStd::make_shared<GraphModel::Graph>(GetGraphContext());
|
|
GraphModel::Graph& savedGraph = landscapeCanvasComponent->m_graph;
|
|
if (savedGraph.GetNodes().empty())
|
|
{
|
|
// If this graph has never been saved before, then there won't be any nodes in
|
|
// the serialized graph from our component, so we don't need to load anything
|
|
isNewGraph = true;
|
|
}
|
|
else
|
|
{
|
|
// Load the serialized graph and invoke the PostLoadSetup so that all the metadata
|
|
// for the graph/nodes/slots gets setup properly before we call CreateGraphController
|
|
// that will actually recreate the full graph in the scene
|
|
graph.reset(m_serializeContext->CloneObject(&savedGraph));
|
|
graph->PostLoadSetup(GetGraphContext());
|
|
}
|
|
|
|
// Keep track of our new graph.
|
|
m_graphs[graphId] = graph;
|
|
|
|
// Listen for GraphController notifications on the new graph.
|
|
GraphModelIntegration::GraphControllerNotificationBus::MultiHandler::BusConnect(graphId);
|
|
|
|
// Create the controller for the new graph.
|
|
GraphModelIntegration::GraphManagerRequestBus::Broadcast(&GraphModelIntegration::GraphManagerRequests::CreateGraphController, graphId, graph);
|
|
|
|
// If we loaded a saved graph, we need to make sure all the loaded nodes Entity/Components still exist,
|
|
// and also look for any new components that have been added that need new nodes created for them
|
|
if (!isNewGraph)
|
|
{
|
|
RefreshEntityComponentNodes(rootEntityId, graphId);
|
|
}
|
|
|
|
return isNewGraph;
|
|
}
|
|
|
|
GraphCanvas::ContextMenuAction::SceneReaction MainWindow::ShowNodeContextMenu(const AZ::EntityId& nodeId, const QPoint& screenPoint, const QPointF& scenePoint)
|
|
{
|
|
NodeContextMenu contextMenu(GetActiveGraphCanvasGraphId());
|
|
|
|
return AssetEditorMainWindow::HandleContextMenu(contextMenu, nodeId, screenPoint, scenePoint);
|
|
}
|
|
|
|
void MainWindow::GetChildrenTree(const AZ::EntityId& rootEntityId, AzToolsFramework::EntityIdList& childrenList)
|
|
{
|
|
AzToolsFramework::EntityIdList children;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(children, rootEntityId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::GetChildren);
|
|
for (auto childId : children)
|
|
{
|
|
childrenList.push_back(childId);
|
|
|
|
GetChildrenTree(childId, childrenList);
|
|
}
|
|
}
|
|
|
|
QString MainWindow::GetPropertyPathForSlot(GraphModel::SlotPtr slot, GraphModel::DataType::Enum dataType, int elementIndex)
|
|
{
|
|
static const char* ConfigurationPropertyPrefix = "Configuration|";
|
|
static const char* PreviewEntityIdPropertyPath = "Preview Settings|Pin Preview to Shape";
|
|
static const char* GradientEntityIdPropertyPath = "Gradient|Gradient Entity Id";
|
|
static const char* ShapeEntityIdPropertyPath = "Shape Entity Id";
|
|
static const char* PinToShapeEntityIdPropertyPath = "Pin To Shape Entity Id";
|
|
static const char* VegetationAreasPropertyPath = "Vegetation Areas";
|
|
|
|
const GraphModel::SlotName& slotName = slot->GetName();
|
|
QString propertyPath;
|
|
bool useConfigurationPrefix = true;
|
|
switch (dataType)
|
|
{
|
|
case LandscapeCanvas::LandscapeCanvasDataTypeEnum::Bounds:
|
|
{
|
|
if (slotName == LandscapeCanvas::PREVIEW_BOUNDS_SLOT_ID)
|
|
{
|
|
propertyPath = PreviewEntityIdPropertyPath;
|
|
useConfigurationPrefix = false;
|
|
}
|
|
else if (slotName == LandscapeCanvas::INBOUND_SHAPE_SLOT_ID
|
|
|| slotName == LandscapeCanvas::PLACEMENT_BOUNDS_SLOT_ID)
|
|
{
|
|
propertyPath = ShapeEntityIdPropertyPath;
|
|
}
|
|
else if (slotName == LandscapeCanvas::PIN_TO_SHAPE_SLOT_ID)
|
|
{
|
|
propertyPath = PinToShapeEntityIdPropertyPath;
|
|
}
|
|
} break;
|
|
case LandscapeCanvas::LandscapeCanvasDataTypeEnum::Gradient:
|
|
{
|
|
propertyPath = GradientEntityIdPropertyPath;
|
|
|
|
// Special case handling of some gradient properties for extendable gradient mixers
|
|
// and the position modifier which are nested under group elements
|
|
if (slot->SupportsExtendability())
|
|
{
|
|
propertyPath.prepend(QString("Layers|[%1]|").arg(elementIndex));
|
|
}
|
|
else if (slotName == LandscapeCanvas::BaseAreaModifierNode::INBOUND_GRADIENT_X_SLOT_ID
|
|
|| slotName == LandscapeCanvas::BaseAreaModifierNode::INBOUND_GRADIENT_Y_SLOT_ID
|
|
|| slotName == LandscapeCanvas::BaseAreaModifierNode::INBOUND_GRADIENT_Z_SLOT_ID)
|
|
{
|
|
// The X/Y/Z supported nodes are Position/Rotation modifiers, so we need
|
|
// to figure out which one this is to get the right property path
|
|
GraphModel::NodePtr node = slot->GetParentNode();
|
|
if (node)
|
|
{
|
|
// The node titles are "Position Modifier" or "Rotation Modifier", and
|
|
// the property path is expecting Position/Rotation|Gradient|Gradient Entity Id
|
|
// so we need to parse the "Position"/"Rotation" out of the title to use
|
|
// in the property path
|
|
QStringList parts = QString(node->GetTitle()).split(' ');
|
|
AZ_Assert(!parts.empty(), "Unrecognized node title");
|
|
propertyPath.prepend(QString("%1 %2|").arg(parts[0]).arg(slotName.back()));
|
|
}
|
|
}
|
|
} break;
|
|
case LandscapeCanvas::LandscapeCanvasDataTypeEnum::Area:
|
|
{
|
|
propertyPath = QString("%1|[%2]").arg(VegetationAreasPropertyPath).arg(elementIndex);
|
|
} break;
|
|
}
|
|
|
|
// Most of our supported properties are nested under a top-level configuration path
|
|
if (!propertyPath.isEmpty() && useConfigurationPrefix)
|
|
{
|
|
propertyPath.prepend(ConfigurationPropertyPrefix);
|
|
}
|
|
|
|
return propertyPath;
|
|
}
|
|
|
|
void MainWindow::UpdateConnectionData(GraphModel::ConnectionPtr connection, bool added)
|
|
{
|
|
if (!connection)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GraphCanvas::GraphId graphId = (*GraphModelIntegration::GraphControllerNotificationBus::GetCurrentBusId());
|
|
|
|
// Similarly as below, this protects against the edge case where this logic gets hit if the node and/or
|
|
// slot belonging to this connection got deleted before this was executed.
|
|
if (!connection->GetSourceNode() || !connection->GetTargetNode() || !connection->GetSourceSlot() || !connection->GetTargetSlot())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Figure out the element index we need to update based on the index of the
|
|
// target slot on the target node that have the same data type
|
|
GraphModel::NodePtr targetNode = connection->GetTargetNode();
|
|
GraphModel::SlotPtr targetSlot = connection->GetTargetSlot();
|
|
GraphModel::DataTypePtr dataType = connection->GetSourceSlot()->GetDataType();
|
|
int elementIndexToModify = GetInboundDataSlotIndex(targetNode, dataType, targetSlot);
|
|
if (elementIndexToModify == InvalidSlotIndex)
|
|
{
|
|
// Typically this shouldn't be reached, but there are cases where the slot index might
|
|
// be invalid, such as the target node being deleted before the connection is triggered
|
|
// to be removed, which could happen if the node was deleted while it was in a collapsed group.
|
|
return;
|
|
}
|
|
|
|
// If the connection was removed, the target will be set to an invalid EntityId
|
|
// If the connection was added, the target will be updated with the appropriate EntityId from the source
|
|
AZ::EntityId newEntityId;
|
|
if (added)
|
|
{
|
|
auto sourceNode = static_cast<LandscapeCanvas::BaseNode*>(connection->GetSourceNode().get());
|
|
newEntityId = sourceNode->GetVegetationEntityId();
|
|
}
|
|
|
|
// Figure out the property path we are looking for based on the data type of the slot
|
|
GraphModel::DataType::Enum dataTypeEnum = dataType->GetTypeEnum();
|
|
QString propertyPath = GetPropertyPathForSlot(targetSlot, dataTypeEnum, elementIndexToModify);
|
|
if (propertyPath.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If our target is an extendable slot (e.g. gradient mixer, area blender, etc...) then the element that needs
|
|
// to be set is actually in a container, and might need to be added
|
|
bool elementInContainer = targetSlot->SupportsExtendability();
|
|
|
|
// Queue this event since it occurs when attaching/detaching connections
|
|
// in the UI, otherwise the attach/detach will appear to stall momentarily
|
|
QTimer::singleShot(0, [this, graphId, targetNode, targetSlot, newEntityId, propertyPath, elementIndexToModify, elementInContainer]() {
|
|
if (!targetNode)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Special case for the Vegetation Area Placement Bounds, the slot actually represents a separate
|
|
// Vegetation Reference Shape or actual Shape component on the same Entity
|
|
AZ::Component* component = nullptr;
|
|
auto targetBaseNode = static_cast<LandscapeCanvas::BaseNode*>(targetNode.get());
|
|
if (targetBaseNode->GetBaseNodeType() == LandscapeCanvas::BaseNode::BaseNodeType::VegetationArea && targetSlot->GetName() == LandscapeCanvas::PLACEMENT_BOUNDS_SLOT_ID)
|
|
{
|
|
// Make sure the target entity still exists before we do all this special-case logic, because it
|
|
// might have been deleted and UpdateConnectionData was only executed because GraphModel was removing
|
|
// the connections associated with a node being deleted
|
|
const AZ::EntityId& targetEntityId = targetBaseNode->GetVegetationEntityId();
|
|
AZ::Entity* targetEntity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(targetEntity, &AZ::ComponentApplicationRequests::FindEntity, targetEntityId);
|
|
if (!targetEntity)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Special case handling when connecting the Placement Bounds to a Shape that exists
|
|
// on the same Entity by re-enabling that disabled Shape component. This is mainly for
|
|
// handling existing Vegetation data that wasn't authored in a graph originally.
|
|
if (newEntityId == targetEntityId)
|
|
{
|
|
auto nodeMapsIt = m_entityIdNodeMapsByGraph.find(graphId);
|
|
if (nodeMapsIt == m_entityIdNodeMapsByGraph.end())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const EntityIdNodeMaps& nodeMaps = nodeMapsIt->second;
|
|
const auto& shapeNodeMap = nodeMaps[EntityIdNodeMapEnum::Shapes];
|
|
auto shapeIt = shapeNodeMap.find(targetEntityId);
|
|
if (shapeIt != shapeNodeMap.end())
|
|
{
|
|
AzToolsFramework::ScopedUndoBatch undoBatch("Enable Embedded Shape");
|
|
|
|
auto shapeNode = static_cast<LandscapeCanvas::BaseNode*>(shapeIt->second.get());
|
|
AZ::Entity::ComponentArrayType disabledComponents;
|
|
AzToolsFramework::EditorDisabledCompositionRequestBus::Event(targetEntityId, &AzToolsFramework::EditorDisabledCompositionRequests::GetDisabledComponents, disabledComponents);
|
|
for (auto disabledComponent : disabledComponents)
|
|
{
|
|
// Look through the disabled components on our Entity for our disabled Shape component
|
|
if (disabledComponent->GetId() == shapeNode->GetComponentId())
|
|
{
|
|
// Re-enable our Shape component
|
|
AzToolsFramework::EntityCompositionRequestBus::Broadcast(&AzToolsFramework::EntityCompositionRequests::EnableComponents, AZ::Entity::ComponentArrayType{ disabledComponent });
|
|
|
|
// Disable any incompatible components (e.g. an existing Reference Shape component on the Entity)
|
|
AzToolsFramework::EntityCompositionRequests::PendingComponentInfo pendingComponentInfo;
|
|
AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(pendingComponentInfo, &AzToolsFramework::EntityCompositionRequests::GetPendingComponentInfo, disabledComponent);
|
|
if (!pendingComponentInfo.m_validComponentsThatAreIncompatible.empty())
|
|
{
|
|
AzToolsFramework::EntityCompositionRequestBus::Broadcast(&AzToolsFramework::EntityCompositionRequests::DisableComponents, pendingComponentInfo.m_validComponentsThatAreIncompatible);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
undoBatch.MarkEntityDirty(targetEntityId);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// For the common case, we just need to use the Reference Shape component on this Entity if it is enabled
|
|
auto baseAreaNodePtr = static_cast<LandscapeCanvas::BaseAreaNode*>(targetNode.get());
|
|
component = baseAreaNodePtr->GetReferenceShapeComponent();
|
|
|
|
// If GetReferenceShapeComponent() fails, then that means either there is no Reference Shape component
|
|
// on our Entity, or there is but it is disabled
|
|
if (!component)
|
|
{
|
|
// Look for a disabled Reference Shape component on this Entity and re-enable it if we find it
|
|
AZ::Entity::ComponentArrayType disabledComponents;
|
|
AzToolsFramework::EditorDisabledCompositionRequestBus::Event(targetEntityId, &AzToolsFramework::EditorDisabledCompositionRequests::GetDisabledComponents, disabledComponents);
|
|
for (auto disabledComponent : disabledComponents)
|
|
{
|
|
if (disabledComponent->RTTI_GetType() == Vegetation::EditorReferenceShapeComponentTypeId)
|
|
{
|
|
component = disabledComponent;
|
|
|
|
// Re-enable our Reference Shape component
|
|
AzToolsFramework::EntityCompositionRequestBus::Broadcast(&AzToolsFramework::EntityCompositionRequests::EnableComponents, AZ::Entity::ComponentArrayType{ component });
|
|
|
|
// Disable any incompatible components (e.g. a previous Shape Component)
|
|
AzToolsFramework::EntityCompositionRequests::PendingComponentInfo pendingComponentInfo;
|
|
AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(pendingComponentInfo, &AzToolsFramework::EntityCompositionRequests::GetPendingComponentInfo, component);
|
|
if (!pendingComponentInfo.m_validComponentsThatAreIncompatible.empty())
|
|
{
|
|
AzToolsFramework::EntityCompositionRequestBus::Broadcast(&AzToolsFramework::EntityCompositionRequests::DisableComponents, pendingComponentInfo.m_validComponentsThatAreIncompatible);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If 'component' is still null then that means there is no Reference Shape component on our Entity, so we need to add one
|
|
if (!component)
|
|
{
|
|
AZ::ComponentId componentId = AddComponentTypeIdToEntity(targetEntityId, Vegetation::EditorReferenceShapeComponentTypeId);
|
|
|
|
component = targetEntity->FindComponent(componentId);
|
|
}
|
|
}
|
|
}
|
|
// Otherwise, just retrieve the main component that this node represents
|
|
else
|
|
{
|
|
component = targetBaseNode->GetComponent();
|
|
}
|
|
|
|
// Check this here because the target node might have been deleted before
|
|
// this gets invoked (e.g. a connection being removed because a node was deleted)
|
|
if (!component)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Iterate through the component class element edit context to expand the elements container
|
|
// size (if necessary)
|
|
m_serializeContext->EnumerateObject(component,
|
|
// beginElemCB (this is called at the beginning of processing a new element)
|
|
[this, propertyPath, elementIndexToModify, elementInContainer](void *instance, const AZ::SerializeContext::ClassData *classData, [[maybe_unused]] const AZ::SerializeContext::ClassElement *classElement) -> bool
|
|
{
|
|
// If the element we are trying to set is in a container, we might need to add some more elements
|
|
// to the container to hold it
|
|
if (elementInContainer && classData && classData->m_container)
|
|
{
|
|
AZ::SerializeContext::IDataContainer* container = classData->m_container;
|
|
const AZ::SerializeContext::ClassElement* containerClassElement = container->GetElement(container->GetDefaultElementNameCrc());
|
|
|
|
// If the container already has enough elements, then we don't need to do anything with the container
|
|
size_t containerSize = container->Size(instance);
|
|
size_t requiredSize = elementIndexToModify + 1;
|
|
if (containerSize >= requiredSize)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (container->IsFixedCapacity() && !container->IsSmartPointer() && requiredSize >= container->Capacity(instance))
|
|
{
|
|
GraphModel::GraphPtr graph = GetGraphById(GetActiveGraphCanvasGraphId());
|
|
AZ_Warning(graph->GetSystemName(), false, "Cannot add additional entries to the container as it is at its capacity of %zu", container->Capacity(instance));
|
|
|
|
return true;
|
|
}
|
|
|
|
// Add more elements to the container to reach the necessary size
|
|
while (containerSize < requiredSize)
|
|
{
|
|
// Reserve entry in the container
|
|
void* dataAddress = container->ReserveElement(instance, containerClassElement);
|
|
|
|
// Store the new element in the container
|
|
container->StoreElement(instance, dataAddress);
|
|
|
|
++containerSize;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}, {},
|
|
AZ::SerializeContext::ENUM_ACCESS_FOR_WRITE, nullptr/* errorHandler */);
|
|
|
|
// Update the property with the new EntityId
|
|
AzToolsFramework::ScopedUndoBatch undoBatch("Update Component Property");
|
|
|
|
AzToolsFramework::PropertyTreeEditor pte = AzToolsFramework::PropertyTreeEditor(reinterpret_cast<void*>(component), component->RTTI_GetType());
|
|
pte.SetProperty(propertyPath.toUtf8().constData(), AZStd::any(newEntityId));
|
|
|
|
undoBatch.MarkEntityDirty(targetBaseNode->GetVegetationEntityId());
|
|
|
|
// Trigger property editors to update attributes/values or else they might be showing stale data
|
|
// since we are updating the property value directly.
|
|
AzToolsFramework::ToolsApplicationEvents::Bus::Broadcast(
|
|
&AzToolsFramework::ToolsApplicationEvents::InvalidatePropertyDisplay,
|
|
AzToolsFramework::Refresh_AttributesAndValues);
|
|
});
|
|
}
|
|
|
|
GraphCanvas::GraphId MainWindow::OnGraphEntity(const AZ::EntityId& entityId)
|
|
{
|
|
GraphCanvas::GraphId graphId;
|
|
|
|
// If we already have a graph open for this Entity, then just focus it
|
|
// instead of creating a new graph
|
|
auto it = m_dockWidgetsByEntity.find(entityId);
|
|
if (it != m_dockWidgetsByEntity.end())
|
|
{
|
|
GraphCanvas::DockWidgetId dockWidgetId = it->second;
|
|
if (FocusDockWidget(dockWidgetId))
|
|
{
|
|
GraphCanvas::EditorDockWidgetRequestBus::EventResult(graphId, dockWidgetId, &GraphCanvas::EditorDockWidgetRequests::GetGraphId);
|
|
return graphId;
|
|
}
|
|
}
|
|
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
// Retrieve the entity being graphed so we can use the name for the graph title
|
|
AZ::Entity* rootEntity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(rootEntity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
|
|
AZ_Assert(rootEntity, "No Entity found for EntityId = %s", entityId.ToString().c_str());
|
|
|
|
// Create a new scene
|
|
GraphCanvas::DockWidgetId dockWidgetId = CreateEditorDockWidget(rootEntity->GetName().c_str());
|
|
GraphCanvas::EditorDockWidgetRequestBus::EventResult(graphId, dockWidgetId, &GraphCanvas::EditorDockWidgetRequests::GetGraphId);
|
|
|
|
// If HandleGraphOpened returns true, then it means there was no previously saved graph loaded,
|
|
// so we need to do the first time parsing/creating of nodes/connections + default node layout
|
|
if (HandleGraphOpened(entityId, dockWidgetId))
|
|
{
|
|
InitialEntityGraph(entityId, graphId);
|
|
}
|
|
// Otherwise, we were able to load a previously saved graph so we just need to update the
|
|
// connections
|
|
else
|
|
{
|
|
GraphModel::NodePtrList nodes;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(nodes, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodes);
|
|
for (auto node : nodes)
|
|
{
|
|
UpdateConnections(node);
|
|
}
|
|
}
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
|
|
// Clear the selection once we have added all the nodes, because by default nodes get
|
|
// selected when they are added to the graph
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::ClearSelection);
|
|
|
|
return graphId;
|
|
}
|
|
|
|
bool MainWindow::ConfigureDefaultLayout()
|
|
{
|
|
if (!GraphCanvas::AssetEditorMainWindow::ConfigureDefaultLayout())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// First try to close our node inspector
|
|
if (!m_customNodeInspector->close())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Add our custom Node Inspector to the default layout
|
|
addDockWidget(Qt::RightDockWidgetArea, m_customNodeInspector);
|
|
m_customNodeInspector->setFloating(false);
|
|
m_customNodeInspector->show();
|
|
|
|
return true;
|
|
}
|
|
|
|
void MainWindow::OnEditorEntityCreated(const AZ::EntityId& entityId)
|
|
{
|
|
// If the user has deleted an Entity and then invokes Undo, its parent
|
|
// Entity may be deleted and then re-created as part of the restore
|
|
// operation, so we need to queue our deletes and detect this case
|
|
// in order to safely ignore the Entity deletion
|
|
auto queuedIt = AZStd::find(m_queuedEntityDeletes.begin(), m_queuedEntityDeletes.end(), entityId);
|
|
if (queuedIt != m_queuedEntityDeletes.end())
|
|
{
|
|
// Deleting this from the queue signifies the delete being ignored
|
|
// when it gets invoked after the singleShot
|
|
m_queuedEntityDeletes.erase(queuedIt);
|
|
|
|
// If this is any other Entity besides one of our root Entities, then
|
|
// we should still do the refresh (RefreshEntityComponentNodes) to make
|
|
// sure any components that may have been added/removed are parsed
|
|
auto it = m_dockWidgetsByEntity.find(entityId);
|
|
if (it != m_dockWidgetsByEntity.end())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
HandleEditorEntityCreated(entityId);
|
|
}
|
|
|
|
void MainWindow::HandleEditorEntityCreated(const AZ::EntityId& entityId, GraphCanvas::GraphId graphId)
|
|
{
|
|
if (m_ignoreGraphUpdates || m_prefabPropagationInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Try to find an open graph whose root Entity contains the Entity which this component was added to
|
|
if (!graphId.IsValid())
|
|
{
|
|
graphId = FindGraphContainingEntity(entityId);
|
|
}
|
|
|
|
// If we still couldn't find a graph for this Entity, then bail out
|
|
if (!graphId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
// Refresh the Entity/Component tree for this entity to create any nodes that may
|
|
// have been added by this change. We only need to update all connections if node(s)
|
|
// were actually created.
|
|
GraphModel::NodePtrList createdNodes = RefreshEntityComponentNodes(entityId, graphId);
|
|
GraphModel::NodePtrList nodes;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(nodes, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodes);
|
|
if (!createdNodes.empty())
|
|
{
|
|
for (auto node : nodes)
|
|
{
|
|
UpdateConnections(node);
|
|
}
|
|
}
|
|
// Otherwise, we only need to update connections for nodes corresponding to this Entity
|
|
else
|
|
{
|
|
for (auto node : nodes)
|
|
{
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (baseNodePtr->GetVegetationEntityId() == entityId)
|
|
{
|
|
UpdateConnections(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
}
|
|
|
|
void MainWindow::OnEditorEntityDeleted(const AZ::EntityId& entityId)
|
|
{
|
|
if (m_prefabPropagationInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_queuedEntityDeletes.push_back(entityId);
|
|
|
|
QTimer::singleShot(0, [this, entityId]() {
|
|
QueuedEditorEntityDeleted(entityId);
|
|
});
|
|
}
|
|
|
|
void MainWindow::QueuedEditorEntityDeleted(const AZ::EntityId& entityId)
|
|
{
|
|
// Check if this was a legitimate Entity deletion, or if it was just a result
|
|
// of an undo/redo restoration
|
|
auto queuedIt = AZStd::find(m_queuedEntityDeletes.begin(), m_queuedEntityDeletes.end(), entityId);
|
|
if (queuedIt != m_queuedEntityDeletes.end())
|
|
{
|
|
m_queuedEntityDeletes.erase(queuedIt);
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
AzToolsFramework::PropertyEditorEntityChangeNotificationBus::MultiHandler::BusDisconnect(entityId);
|
|
|
|
HandleEditorEntityDeleted(entityId);
|
|
}
|
|
|
|
void MainWindow::HandleEditorEntityDeleted(const AZ::EntityId& entityId)
|
|
{
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
// If the Entity deleted corresponds to one of our graphs, then close it
|
|
auto it = m_dockWidgetsByEntity.find(entityId);
|
|
if (it != m_dockWidgetsByEntity.end())
|
|
{
|
|
CloseEditor(it->second);
|
|
}
|
|
// Otherwise check if there are any nodes matching that Entity that need
|
|
// to be removed
|
|
else
|
|
{
|
|
for (GraphCanvas::GraphId graphId : GetOpenGraphIds())
|
|
{
|
|
GraphModel::NodePtrList nodes;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(nodes, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodes);
|
|
|
|
for (auto node : nodes)
|
|
{
|
|
// Ignore area extenders since those nodes will end up being removed when their wrapper node (parent) is deleted
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (baseNodePtr->GetVegetationEntityId() == entityId && !baseNodePtr->IsAreaExtender())
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::RemoveNode, node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
}
|
|
|
|
void MainWindow::OnEntityPickModeStarted()
|
|
{
|
|
m_inObjectPickMode = true;
|
|
}
|
|
|
|
void MainWindow::OnEntityPickModeStopped()
|
|
{
|
|
m_inObjectPickMode = false;
|
|
}
|
|
|
|
GraphModel::NodePtr MainWindow::GetNodeMatchingEntityInGraph(const GraphCanvas::GraphId& graphId, const AZ::EntityId& entityId)
|
|
{
|
|
GraphModel::NodePtrList nodes;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(nodes, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodes);
|
|
|
|
for (auto node : nodes)
|
|
{
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (entityId == baseNodePtr->GetVegetationEntityId())
|
|
{
|
|
return node;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
GraphModel::NodePtr MainWindow::GetNodeMatchingEntityComponentInGraph(const GraphCanvas::GraphId& graphId, const AZ::EntityComponentIdPair& entityComponentId)
|
|
{
|
|
GraphModel::NodePtrList nodes;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(nodes, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodes);
|
|
|
|
const AZ::EntityId& entityId = entityComponentId.GetEntityId();
|
|
const AZ::ComponentId& componentId = entityComponentId.GetComponentId();
|
|
for (auto node : nodes)
|
|
{
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (entityId == baseNodePtr->GetVegetationEntityId() && componentId == baseNodePtr->GetComponentId())
|
|
{
|
|
return node;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
GraphModel::NodePtrList MainWindow::GetAllNodesMatchingEntity(const AZ::EntityId& entityId)
|
|
{
|
|
GraphModel::NodePtrList matchingNodes;
|
|
|
|
for (GraphCanvas::GraphId graphId : GetOpenGraphIds())
|
|
{
|
|
GraphModel::NodePtr node = GetNodeMatchingEntityInGraph(graphId, entityId);
|
|
if (node)
|
|
{
|
|
matchingNodes.push_back(node);
|
|
}
|
|
}
|
|
|
|
return matchingNodes;
|
|
}
|
|
|
|
GraphModel::NodePtrList MainWindow::GetAllNodesMatchingEntityComponent(const AZ::EntityComponentIdPair& entityComponentId)
|
|
{
|
|
GraphModel::NodePtrList matchingNodes;
|
|
|
|
for (GraphCanvas::GraphId graphId : GetOpenGraphIds())
|
|
{
|
|
GraphModel::NodePtr node = GetNodeMatchingEntityComponentInGraph(graphId, entityComponentId);
|
|
if (node)
|
|
{
|
|
matchingNodes.push_back(node);
|
|
}
|
|
}
|
|
|
|
return matchingNodes;
|
|
}
|
|
|
|
void MainWindow::UpdateConnections(GraphModel::NodePtr node)
|
|
{
|
|
// Retrieve all the input data connections for this node that would be expected
|
|
// based on the component property fields. If this differs from what is actually
|
|
// connected for the slots on this node, then we will need to update (add/remove)
|
|
// the connections so that they match.
|
|
ConnectionsList expectedConnections;
|
|
GraphCanvas::GraphId graphId = GetGraphId(node->GetGraph());
|
|
ParseNodeConnections(graphId, node, expectedConnections);
|
|
|
|
// Iterate through the input data slots on this node to check for
|
|
// existing connections that satisfy our expected connections, and to
|
|
// remove any current connections that aren't in our expected list.
|
|
for (auto slotPair : node->GetSlots())
|
|
{
|
|
GraphModel::SlotPtr slot = slotPair.second;
|
|
|
|
// We only care about input data slots because those are the only slots
|
|
// that could be modified when a Component on an Entity is changed,
|
|
// which is what triggers OnEntityComponentPropertyChanged
|
|
if (!slot->Is(GraphModel::SlotDirection::Input, GraphModel::SlotType::Data))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If there aren't any connections to this slot, we can skip it
|
|
auto slotConnections = slot->GetConnections();
|
|
if (slotConnections.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Input data slots will only have one connection
|
|
GraphModel::ConnectionPtr connection = *slotConnections.begin();
|
|
|
|
// Check if this connection matches one in our list of expected connections
|
|
bool matchesExisting = false;
|
|
for (auto it = expectedConnections.begin(); it != expectedConnections.end(); ++it)
|
|
{
|
|
auto sourceNode = it->first.first;
|
|
auto sourceSlot = it->first.second;
|
|
auto targetNode = it->second.first;
|
|
auto targetSlot = it->second.second;
|
|
|
|
// If we found a matching connection, then remove it from our list of expected
|
|
// so we don't have to process it after we are done checking all the slots
|
|
// on the node
|
|
if (sourceNode == connection->GetSourceNode() && sourceSlot == connection->GetSourceSlot() &&
|
|
targetNode == connection->GetTargetNode() && targetSlot == connection->GetTargetSlot())
|
|
{
|
|
matchesExisting = true;
|
|
expectedConnections.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If this connection doesn't match an expected connection, then it needs to be removed
|
|
if (!matchesExisting)
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::RemoveConnection, connection);
|
|
}
|
|
}
|
|
|
|
// For the remaining expected connections, this means they didn't exist already,
|
|
// so we need to create them
|
|
for (auto it : expectedConnections)
|
|
{
|
|
GraphModel::SlotPtr sourceSlot = it.first.second;
|
|
GraphModel::SlotPtr targetSlot = it.second.second;
|
|
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::AddConnection, sourceSlot, targetSlot);
|
|
}
|
|
}
|
|
|
|
GraphCanvas::GraphId MainWindow::FindGraphContainingEntity(const AZ::EntityId& entityId)
|
|
{
|
|
GraphCanvas::GraphId graphId;
|
|
AZ::EntityId parentEntityId = entityId;
|
|
|
|
AZ::EntityId levelEntityId;
|
|
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(levelEntityId, &AzToolsFramework::ToolsApplicationRequests::GetCurrentLevelEntityId);
|
|
|
|
// Crawl up the Entity hierarchy looking for a matching open graph.
|
|
// Stop the loop if we encounter the Level Entity, which can be hit here when
|
|
// components are added/removed via the Level Inspector.
|
|
while (parentEntityId.IsValid() && parentEntityId != levelEntityId)
|
|
{
|
|
auto it = m_dockWidgetsByEntity.find(parentEntityId);
|
|
if (it != m_dockWidgetsByEntity.end())
|
|
{
|
|
const GraphCanvas::DockWidgetId& dockWidgetId = it->second;
|
|
GraphCanvas::EditorDockWidgetRequestBus::EventResult(graphId, dockWidgetId, &GraphCanvas::EditorDockWidgetRequests::GetGraphId);
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
AZ::EntityId previousParentEntityId = parentEntityId;
|
|
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(parentEntityId, parentEntityId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::GetParent);
|
|
|
|
// Prevent infinite loop if the GetParent ends up returning itself, which could happen in a case where a slice is in
|
|
// the process of being restored and this logic gets invoked.
|
|
if (previousParentEntityId == parentEntityId)
|
|
{
|
|
AZ_Assert(false, "Corrupt parent hierarchy - entity parent ID is set to itself, breaking here to prevent infinite loop.");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return graphId;
|
|
}
|
|
|
|
void MainWindow::EnumerateEntityComponentTree(const AZ::EntityId& rootEntityId, EntityComponentCallback callback)
|
|
{
|
|
// Retrieve the entity hierarchy for our root entity
|
|
AzToolsFramework::EntityIdList children;
|
|
children.push_back(rootEntityId);
|
|
GetChildrenTree(rootEntityId, children);
|
|
|
|
// Iterate through our entity hierarchy and invoke our callback on all
|
|
// components that are found (both enabled and disabled)
|
|
for (auto entityId : children)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
|
|
if (!entity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Retrieve the enabled components on our Entity
|
|
AZ::Entity::ComponentArrayType components = entity->GetComponents();
|
|
for (AZ::Component* component : components)
|
|
{
|
|
callback(entityId, component, false);
|
|
}
|
|
|
|
// If there are any disabled components on our Entity, we need to
|
|
// retrieve them separately because they won't show up with Entity::GetComponents()
|
|
AZ::Entity::ComponentArrayType disabledComponents;
|
|
AzToolsFramework::EditorDisabledCompositionRequestBus::Event(entityId, &AzToolsFramework::EditorDisabledCompositionRequests::GetDisabledComponents, disabledComponents);
|
|
for (AZ::Component* disabledComponent : disabledComponents)
|
|
{
|
|
callback(entityId, disabledComponent, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::InitialEntityGraph(const AZ::EntityId& entityId, GraphCanvas::GraphId graphId)
|
|
{
|
|
// Keep track of our node points for creating a better default node layout
|
|
NodePoint* rootPoint = new NodePoint();
|
|
AZStd::unordered_map<AZ::EntityId, AZStd::vector<NodePoint*>> nodePointMap;
|
|
|
|
// Keep track of any node wrappings we will need to setup after the nodes
|
|
// have been added to the graph
|
|
AZStd::unordered_map<AZ::EntityId, GraphModel::NodePtrList> nodeWrappings;
|
|
|
|
// We don't need to cache a mapping of the area extenders since they don't have
|
|
// output slots that connect to other nodes
|
|
AZStd::vector<LandscapeCanvas::BaseNode::BaseNodePtr> vegetationAreaExtenders;
|
|
|
|
// Iterate through our entity hierarchy to look for components that
|
|
// correspond with nodes we know how to graph
|
|
GraphModel::NodePtrList disabledNodes;
|
|
GraphModel::GraphPtr graph = GetGraphById(graphId);
|
|
EnumerateEntityComponentTree(entityId, [this, graph, graphId, rootPoint, &nodePointMap, &nodeWrappings, &vegetationAreaExtenders, &disabledNodes](const AZ::EntityId& entityId, AZ::Component* component, bool isDisabled) {
|
|
const AZ::TypeId& componentTypeId = component->RTTI_GetType();
|
|
|
|
// Create the node for the given component type.
|
|
// If we don't support a node for this component type, it will just return nullptr.
|
|
LandscapeCanvas::BaseNode::BaseNodePtr node;
|
|
LandscapeCanvas::LandscapeCanvasNodeFactoryRequestBus::BroadcastResult(node, &LandscapeCanvas::LandscapeCanvasNodeFactoryRequests::CreateNodeForType, graph, componentTypeId);
|
|
|
|
// Set the EntityId for the vegetation entity corresponding to this node (if we found one)
|
|
if (node)
|
|
{
|
|
node->SetVegetationEntityId(entityId);
|
|
node->SetComponentId(component->GetId());
|
|
|
|
// Update the node mappings we need to cache for this node
|
|
UpdateEntityIdNodeMap(graphId, node);
|
|
|
|
// Keep track of which nodes came from disabled components so that we can disable
|
|
// those nodes once they are added to the graph
|
|
if (isDisabled)
|
|
{
|
|
disabledNodes.push_back(node);
|
|
}
|
|
|
|
// Keep track locally of our area extenders so we can parse them later
|
|
LandscapeCanvas::BaseNode::BaseNodeType baseNodeType = node->GetBaseNodeType();
|
|
switch (baseNodeType)
|
|
{
|
|
case LandscapeCanvas::BaseNode::VegetationAreaFilter:
|
|
case LandscapeCanvas::BaseNode::VegetationAreaModifier:
|
|
case LandscapeCanvas::BaseNode::VegetationAreaSelector:
|
|
vegetationAreaExtenders.push_back(node);
|
|
break;
|
|
}
|
|
|
|
// If this node is meant to be wrapped on a WrapperNode, then
|
|
// add it to the node wrappings so we can wrap it later after
|
|
// the nodes have been added to the graph
|
|
if (node->IsAreaExtender())
|
|
{
|
|
auto nodeWrapIt = nodeWrappings.find(entityId);
|
|
if (nodeWrapIt == nodeWrappings.end())
|
|
{
|
|
nodeWrappings[entityId] = { node };
|
|
}
|
|
else
|
|
{
|
|
nodeWrappings[entityId].push_back(node);
|
|
}
|
|
}
|
|
// Otherwise, create a new node point for this general node and just place it as a child on our root
|
|
else
|
|
{
|
|
NodePoint* point = new NodePoint();
|
|
point->node = node;
|
|
point->vegetationEntityId = entityId;
|
|
point->parent = rootPoint;
|
|
rootPoint->children.push_back(point);
|
|
nodePointMap[entityId].push_back(point);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Find connections between nodes. Save the corresponding node for the slot in a pair, because
|
|
// we can't retrieve the parent node from the Slot until the node has been added to the graph, but we need
|
|
// to match based on that data to place nodes near eachother that have slots connected.
|
|
ConnectionsList connections;
|
|
const EntityIdNodeMaps& nodeMaps = m_entityIdNodeMapsByGraph[graphId];
|
|
for (auto nodeType : { EntityIdNodeMapEnum::Gradients, EntityIdNodeMapEnum::VegetationAreas })
|
|
{
|
|
const EntityIdNodeMap& nodeMap = nodeMaps[nodeType];
|
|
for (const auto& it : nodeMap)
|
|
{
|
|
GraphModel::NodePtr node = it.second;
|
|
ParseNodeConnections(graphId, node, connections);
|
|
}
|
|
}
|
|
for (const auto& it : vegetationAreaExtenders)
|
|
{
|
|
ParseNodeConnections(graphId, it, connections);
|
|
}
|
|
|
|
// Use the connections between nodes to setup the node point tree so
|
|
// that nodes that are connected together are:
|
|
// 1. Placed near eachother
|
|
// 2. Target nodes are placed to the right of the source node
|
|
// When the node points are created, they are all placed as children on
|
|
// a dummy root node point, so any nodes that don't have connections will
|
|
// be placed at the bottom in a vertical column. The tree is connection type
|
|
// agnostic, so it doesn't matter whether a Shape is connected to a Gradient,
|
|
// or a Gradient is connecte to a Gradient Modifier, any nodes that are connected
|
|
// will be placed in a left to right flow, and also handles if one node has multiple
|
|
// output slots connected to multiple nodes. As we continue to add support for more
|
|
// connections, they will automatically be handled by this logic.
|
|
for (auto it : connections)
|
|
{
|
|
GraphModel::NodePtr sourceNode = it.first.first;
|
|
GraphModel::NodePtr targetNode = it.second.first;
|
|
|
|
auto sourceBaseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(sourceNode.get());
|
|
auto targetBaseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(targetNode.get());
|
|
AZ::EntityId sourceEntityId = sourceBaseNodePtr->GetVegetationEntityId();
|
|
AZ::EntityId targetEntityId = targetBaseNodePtr->GetVegetationEntityId();
|
|
|
|
// Find the source and target NodePoints from the map. There may be multiple
|
|
// NodePoints for a single Vegetation EntityId in the case where multiple
|
|
// components are on the same Entity, so if there's more than one entry
|
|
// we need to search and match based on the NodePtr.
|
|
auto sourcePoints = nodePointMap[sourceEntityId];
|
|
auto targetPoints = nodePointMap[targetEntityId];
|
|
NodePoint* sourcePoint = sourcePoints.size() == 1 ? sourcePoints[0] : FindNodePoint(sourcePoints, nodeWrappings, sourceNode);
|
|
NodePoint* targetPoint = targetPoints.size() == 1 ? targetPoints[0] : FindNodePoint(targetPoints, nodeWrappings, targetNode);
|
|
if (!sourcePoint || !targetPoint)
|
|
{
|
|
AZ_Error(graph->GetSystemName(), false, "Invalid source or target point connection");
|
|
continue;
|
|
}
|
|
|
|
// Add this target node as one of the children from the source node
|
|
sourcePoint->children.push_back(targetPoint);
|
|
|
|
// If the target already had a parent, remove it as a child
|
|
if (targetPoint->parent)
|
|
{
|
|
NodePoint* parentPoint = targetPoint->parent;
|
|
auto iter = AZStd::find(parentPoint->children.begin(), parentPoint->children.end(), targetPoint);
|
|
if (iter != parentPoint->children.end())
|
|
{
|
|
parentPoint->children.erase(iter);
|
|
}
|
|
}
|
|
|
|
// Then set the new parent for our target
|
|
targetPoint->parent = sourcePoint;
|
|
}
|
|
|
|
// Place the nodes in a tree layout grouped by their connections
|
|
AZ::Vector2 gridMajorPitch;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(gridMajorPitch, graphId, &GraphModelIntegration::GraphControllerRequests::GetMajorPitch);
|
|
PlaceNodes(graphId, rootPoint, gridMajorPitch);
|
|
|
|
// Setup the node wrappings now that the nodes have been placed in the graph
|
|
for (auto it : nodeWrappings)
|
|
{
|
|
const AZ::EntityId& wrapperNodeEntityId = it.first;
|
|
auto nodePointIt = nodePointMap.find(wrapperNodeEntityId);
|
|
if (nodePointIt == nodePointMap.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Find the wrapper node for this EntityId. There could be multiple nodes with the same
|
|
// EntityId (e.g. box shapes), but there can't be multiple wrapper nodes on the same Entity.
|
|
GraphModel::NodePtr wrapperNode = nullptr;
|
|
for (auto nodePoint : nodePointIt->second)
|
|
{
|
|
if (nodePoint->node->GetNodeType() == GraphModel::NodeType::WrapperNode)
|
|
{
|
|
wrapperNode = nodePoint->node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
GraphModel::NodePtrList wrappedNodes = it.second;
|
|
for (GraphModel::NodePtr node : wrappedNodes)
|
|
{
|
|
// Wrap the node using its preferred layout order (if it has one)
|
|
AZ::u32 layoutOrder = GetWrappedNodeLayoutOrder(node);
|
|
if (layoutOrder != GraphModel::DefaultWrappedNodeLayoutOrder)
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::WrapNodeOrdered, wrapperNode, node, layoutOrder);
|
|
}
|
|
else
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::WrapNode, wrapperNode, node);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete the node points now that we've completed placing the nodes
|
|
delete rootPoint;
|
|
for (auto it : nodePointMap)
|
|
{
|
|
for (auto pointIt : it.second)
|
|
{
|
|
delete pointIt;
|
|
}
|
|
}
|
|
|
|
// Disable any nodes that came from disabled components now that they've all been added to the graph
|
|
for (auto node : disabledNodes)
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::DisableNode, node);
|
|
}
|
|
|
|
// Create the connections now, after placing the nodes, since the
|
|
// connection data is used for appropriate node placement
|
|
for (auto it : connections)
|
|
{
|
|
GraphModel::SlotPtr sourceSlot = it.first.second;
|
|
GraphModel::SlotPtr targetSlot = it.second.second;
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::AddConnection, sourceSlot, targetSlot);
|
|
}
|
|
}
|
|
|
|
GraphModel::NodePtrList MainWindow::RefreshEntityComponentNodes(const AZ::EntityId& targetEntityId, GraphCanvas::GraphId graphId)
|
|
{
|
|
GraphModel::GraphPtr graph = GetGraphById(graphId);
|
|
GraphModel::NodePtrList loadedNodes, disabledNodes, createdNodes;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(loadedNodes, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodes);
|
|
|
|
EnumerateEntityComponentTree(targetEntityId, [this, graph, graphId, &loadedNodes, &disabledNodes, &createdNodes](const AZ::EntityId& entityId, AZ::Component* component, bool isDisabled) {
|
|
bool foundMatch = false;
|
|
GraphModel::NodePtr validNode = nullptr;
|
|
|
|
// Check if this component matches a node that was already loaded in the graph
|
|
for (auto it = loadedNodes.begin(); it != loadedNodes.end(); ++it)
|
|
{
|
|
GraphModel::NodePtr node = *it;
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
AZ::ComponentId componentId = component->GetId();
|
|
if (entityId == baseNodePtr->GetVegetationEntityId() && componentId == baseNodePtr->GetComponentId())
|
|
{
|
|
foundMatch = true;
|
|
validNode = node;
|
|
|
|
// Erase this from our list of loaded nodes so that we know we found its match
|
|
// After we iterate through the Entity/Component tree, anything left in loadedNodes
|
|
// will represent saved nodes that no longer have a corresponding Entity/Component in the level
|
|
loadedNodes.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we didn't find a match for this component, check if this is a newly added component we need to
|
|
// create a node for
|
|
if (!foundMatch)
|
|
{
|
|
const AZ::TypeId& componentTypeId = component->RTTI_GetType();
|
|
|
|
// Try to create the node for the given component type.
|
|
// If we don't support a node for this component type, it will just return nullptr.
|
|
LandscapeCanvas::BaseNode::BaseNodePtr node;
|
|
LandscapeCanvas::LandscapeCanvasNodeFactoryRequestBus::BroadcastResult(node, &LandscapeCanvas::LandscapeCanvasNodeFactoryRequests::CreateNodeForType, graph, componentTypeId);
|
|
|
|
if (node)
|
|
{
|
|
validNode = node;
|
|
createdNodes.push_back(node);
|
|
node->SetVegetationEntityId(entityId);
|
|
node->SetComponentId(component->GetId());
|
|
|
|
PlaceNewNode(graphId, node);
|
|
}
|
|
}
|
|
|
|
if (validNode)
|
|
{
|
|
if (isDisabled)
|
|
{
|
|
disabledNodes.push_back(validNode);
|
|
}
|
|
|
|
// Update the node mappings we need to cache for this node
|
|
UpdateEntityIdNodeMap(graphId, validNode);
|
|
}
|
|
});
|
|
|
|
// Disable any nodes that came from disabled components now that they've all been added to the graph
|
|
for (auto node : disabledNodes)
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::DisableNode, node);
|
|
}
|
|
|
|
// Anything left in 'loadedNodes' at this point after the enumerate is done can be
|
|
// deleted if we were refreshing the the root Entity for this graph, since that means
|
|
// there's no longer an existing component matching it
|
|
if (targetEntityId == GetRootEntityIdForGraphId(graphId))
|
|
{
|
|
for (auto node : loadedNodes)
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::RemoveNode, node);
|
|
}
|
|
}
|
|
|
|
return createdNodes;
|
|
}
|
|
|
|
void MainWindow::OnEntityComponentAdded(const AZ::EntityId& entityId, const AZ::ComponentId& componentId)
|
|
{
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Try to find an open graph whose root Entity contains the Entity which this component was added to
|
|
GraphCanvas::GraphId graphId = FindGraphContainingEntity(entityId);
|
|
if (!graphId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// When OnEntityComponentAdded is called, the component won't be accessible by Entity::FindComponent yet, it will still
|
|
// be pending even whether it is disabled or not
|
|
AZ::Component* component = nullptr;
|
|
AZ::Entity::ComponentArrayType pendingComponents;
|
|
AzToolsFramework::EditorPendingCompositionRequestBus::Event(entityId, &AzToolsFramework::EditorPendingCompositionRequests::GetPendingComponents, pendingComponents);
|
|
for (AZ::Component* pendingComponent : pendingComponents)
|
|
{
|
|
if (pendingComponent->GetId() == componentId)
|
|
{
|
|
component = pendingComponent;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!component)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Create the node for the given component type.
|
|
// If we don't support a node for this component type, it will just return nullptr.
|
|
LandscapeCanvas::BaseNode::BaseNodePtr node;
|
|
GraphModel::GraphPtr graph = GetGraphById(graphId);
|
|
const AZ::TypeId& componentTypeId = component->RTTI_GetType();
|
|
LandscapeCanvas::LandscapeCanvasNodeFactoryRequestBus::BroadcastResult(node, &LandscapeCanvas::LandscapeCanvasNodeFactoryRequests::CreateNodeForType, graph, componentTypeId);
|
|
|
|
if (!node)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Set the EntityId for the vegetation entity corresponding to this node (if we found one)
|
|
node->SetVegetationEntityId(entityId);
|
|
node->SetComponentId(componentId);
|
|
|
|
// Update the node mappings we need to cache for this node and parse any connections that it may have setup already
|
|
UpdateEntityIdNodeMap(graphId, node);
|
|
ConnectionsList connections;
|
|
ParseNodeConnections(graphId, node, connections);
|
|
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
// Add the node to the graph, either wrapped on its parent or just in the scene if it's standalone
|
|
PlaceNewNode(graphId, node);
|
|
|
|
// Disable this node for now since it's pending when OnEntityComponentAdded is called, it will be enabled
|
|
// after if it becomes enabled
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::DisableNode, node);
|
|
|
|
// Create connections if any exist (e.g. if a component was copied/pasted with existing configuration)
|
|
for (auto it : connections)
|
|
{
|
|
GraphModel::SlotPtr sourceSlot = it.first.second;
|
|
GraphModel::SlotPtr targetSlot = it.second.second;
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::AddConnection, sourceSlot, targetSlot);
|
|
}
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
|
|
// As mentioned earlier, the component added when OnEntityComponentAdded is called is still pending currently,
|
|
// so we need to delay checking until after this event is invoked to see if the component was enabled
|
|
QTimer::singleShot(0, [this, entityId, componentId, graphId, node]() {
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
|
|
if (!entity)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZ::Component* component = entity->FindComponent(componentId);
|
|
if (component)
|
|
{
|
|
// If FindComponent succeeds, then the component has been enabled
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::EnableNode, node);
|
|
|
|
// Also check if any other previously deactivated (pending) components on this same Entity were activated
|
|
// when this new component was added (e.g. a random noise gradient component being activated once the
|
|
// gradient transform modifier and shape are added)
|
|
auto nodeMapsIt = m_entityIdNodeMapsByGraph.find(graphId);
|
|
if (nodeMapsIt != m_entityIdNodeMapsByGraph.end())
|
|
{
|
|
const EntityIdNodeMaps& nodeMaps = nodeMapsIt->second;
|
|
for (int i = 0; i < EntityIdNodeMapEnum::Count; ++i)
|
|
{
|
|
const auto& nodeMap = nodeMaps[i];
|
|
auto it = nodeMap.find(entityId);
|
|
if (it != nodeMap.end())
|
|
{
|
|
GraphModel::NodePtr cachedNode = it->second;
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(cachedNode.get());
|
|
|
|
// Ignore node matching the same componentId as the component that was directly added
|
|
// If the GetComponent() method returns a valid pointer, it means the component is enabled now
|
|
if ((baseNodePtr->GetComponentId() != componentId) && baseNodePtr->GetComponent())
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::EnableNode, cachedNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void MainWindow::PlaceNewNode(GraphCanvas::GraphId graphId, LandscapeCanvas::BaseNode::BaseNodePtr node)
|
|
{
|
|
// If this is an extender node, then we need to wrap it to its parent node
|
|
if (node->IsAreaExtender())
|
|
{
|
|
auto nodeMapsIt = m_entityIdNodeMapsByGraph.find(graphId);
|
|
if (nodeMapsIt == m_entityIdNodeMapsByGraph.end())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const EntityIdNodeMaps& nodeMaps = nodeMapsIt->second;
|
|
const auto& vegetationAreaNodeMap = nodeMaps[EntityIdNodeMapEnum::VegetationAreas];
|
|
auto it = vegetationAreaNodeMap.find(node->GetVegetationEntityId());
|
|
if (it != vegetationAreaNodeMap.end())
|
|
{
|
|
GraphModel::NodePtr wrapperNode = it->second;
|
|
AZ::u32 layoutOrder = GetWrappedNodeLayoutOrder(node);
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::WrapNodeOrdered, wrapperNode, node, layoutOrder);
|
|
}
|
|
}
|
|
// Otherwise, add this node to the graph
|
|
else
|
|
{
|
|
AZ::Vector2 nodePosition = AZ::Vector2::CreateZero();
|
|
auto it = m_deletedNodePositions.find(graphId);
|
|
if (it != m_deletedNodePositions.end())
|
|
{
|
|
// Check if there was a saved position from a previous node with matching Entity/Component pair
|
|
// that had been previously deleted, so that we can handle Undo/Redo placing the re-created
|
|
// node back in the same position
|
|
const DeletedNodePositionsMap& deletedNodePositionMap = it->second;
|
|
AZ::EntityComponentIdPair pair(node->GetVegetationEntityId(), node->GetComponentId());
|
|
auto deletedPositionIt = deletedNodePositionMap.find(pair);
|
|
if (deletedPositionIt != deletedNodePositionMap.end())
|
|
{
|
|
nodePosition = deletedPositionIt->second;
|
|
}
|
|
// Otherwise, this really is a new node, so place it outside the top-left edge of the bounds of all nodes in the scene
|
|
else
|
|
{
|
|
QRectF sceneArea;
|
|
GraphCanvas::SceneRequestBus::EventResult(sceneArea, graphId, &GraphCanvas::SceneRequests::GetSceneBoundingArea);
|
|
nodePosition = AZ::Vector2(aznumeric_cast<float>(sceneArea.right()) + NODE_OFFSET_X_PIXELS, aznumeric_cast<float>(sceneArea.top()));
|
|
}
|
|
}
|
|
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::AddNode, node, nodePosition);
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnEntityComponentRemoved(const AZ::EntityId& entityId, const AZ::ComponentId& componentId)
|
|
{
|
|
if (m_ignoreGraphUpdates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
for (GraphCanvas::GraphId graphId : GetOpenGraphIds())
|
|
{
|
|
GraphModel::NodePtrList nodes;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(nodes, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodes);
|
|
|
|
for (auto node : nodes)
|
|
{
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (baseNodePtr->GetVegetationEntityId() == entityId && baseNodePtr->GetComponentId() == componentId)
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::RemoveNode, node);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
}
|
|
|
|
void MainWindow::OnEntityComponentEnabled(const AZ::EntityId& entityId, const AZ::ComponentId& componentId)
|
|
{
|
|
AZ::EntityComponentIdPair entityComponentId(entityId, componentId);
|
|
GraphModel::NodePtrList matchingNodes = GetAllNodesMatchingEntityComponent(entityComponentId);
|
|
for (auto node : matchingNodes)
|
|
{
|
|
GraphCanvas::GraphId graphId = GetGraphId(node->GetGraph());
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::EnableNode, node);
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnEntityComponentDisabled(const AZ::EntityId& entityId, const AZ::ComponentId& componentId)
|
|
{
|
|
AZ::EntityComponentIdPair entityComponentId(entityId, componentId);
|
|
GraphModel::NodePtrList matchingNodes = GetAllNodesMatchingEntityComponent(entityComponentId);
|
|
for (auto node : matchingNodes)
|
|
{
|
|
GraphCanvas::GraphId graphId = GetGraphId(node->GetGraph());
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::DisableNode, node);
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnEntityComponentPropertyChanged(AZ::ComponentId changedComponentId)
|
|
{
|
|
AZ_UNUSED(changedComponentId);
|
|
|
|
const AZ::EntityId changedEntityId = *AzToolsFramework::PropertyEditorEntityChangeNotificationBus::GetCurrentBusId();
|
|
|
|
GraphModel::NodePtrList matchingNodes = GetAllNodesMatchingEntity(changedEntityId);
|
|
for (auto node : matchingNodes)
|
|
{
|
|
// Re-parse any input connections for this node to add/remove any connections
|
|
// that might've been modified when the component/property was changed
|
|
UpdateConnections(node);
|
|
}
|
|
}
|
|
|
|
void MainWindow::EntityParentChanged(AZ::EntityId entityId, AZ::EntityId newParentId, AZ::EntityId oldParentId)
|
|
{
|
|
if (m_prefabPropagationInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GraphCanvas::GraphId oldGraphId = FindGraphContainingEntity(oldParentId);
|
|
GraphCanvas::GraphId newGraphId = FindGraphContainingEntity(newParentId);
|
|
|
|
// If the Entity is being re-parented but still inside the same graph, then we don't need to do anything
|
|
// This will also trigger if the Entity isn't in a currently open graph, in which case we can also ignore
|
|
if (newGraphId == oldGraphId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If there is an open graph for the previous parent, then treat this like the Entity being deleted
|
|
if (oldGraphId.IsValid())
|
|
{
|
|
HandleEditorEntityDeleted(entityId);
|
|
}
|
|
|
|
// If there is an open graph for the new parent, then treat this like an Entity being created
|
|
if (newGraphId.IsValid())
|
|
{
|
|
// We need to pass in the new graphId for the new parentEntity because when EntityParentChanged
|
|
// is invoked, the EditorEntityInfoRequestBus::Events::GetParent (that is used by FindGraphContainingEntity)
|
|
// will still return the old parentId
|
|
HandleEditorEntityCreated(entityId, newGraphId);
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnPrefabFocusChanged()
|
|
{
|
|
// Make sure to close any open graphs that aren't currently in prefab focus
|
|
// to prevent the user from making modifications outside of the allowed focus scope
|
|
AZStd::vector<GraphCanvas::DockWidgetId> dockWidgetsToClose;
|
|
for (auto [entityId, dockWidgetId] : m_dockWidgetsByEntity)
|
|
{
|
|
if (!m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
|
|
{
|
|
dockWidgetsToClose.push_back(dockWidgetId);
|
|
}
|
|
}
|
|
for (auto dockWidgetId : dockWidgetsToClose)
|
|
{
|
|
CloseEditor(dockWidgetId);
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnPrefabInstancePropagationBegin()
|
|
{
|
|
// Ignore graph updates during prefab propagation because the entities will be
|
|
// deleted and re-created, which would inadvertantly trigger our logic to close
|
|
// the graph when the corresponding entity is deleted.
|
|
m_prefabPropagationInProgress = true;
|
|
}
|
|
|
|
void MainWindow::OnPrefabInstancePropagationEnd()
|
|
{
|
|
// See comment above in OnPrefabInstancePropagationBegin
|
|
m_prefabPropagationInProgress = false;
|
|
|
|
// After prefab propagation is complete, the entity tied to one of our open
|
|
// graphs might have been deleted (e.g. if a prefab was created from that entity).
|
|
// Any open graphs tied to an entity that no longer exists will need to be closed.
|
|
// We need to close them in a separate iterator because the CloseEditor API will
|
|
// end up modifying m_dockWidgetsByEntity.
|
|
AZStd::vector<GraphCanvas::DockWidgetId> dockWidgetsToDelete;
|
|
for (auto [entityId, dockWidgetId] : m_dockWidgetsByEntity)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
|
|
if (!entity)
|
|
{
|
|
dockWidgetsToDelete.push_back(dockWidgetId);
|
|
}
|
|
}
|
|
for (auto dockWidgetId : dockWidgetsToDelete)
|
|
{
|
|
CloseEditor(dockWidgetId);
|
|
}
|
|
}
|
|
|
|
void MainWindow::OnCryEditorEndCreate()
|
|
{
|
|
UpdateGraphEnabled();
|
|
}
|
|
|
|
void MainWindow::OnCryEditorEndLoad()
|
|
{
|
|
UpdateGraphEnabled();
|
|
|
|
AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusConnect();
|
|
}
|
|
|
|
void MainWindow::OnCryEditorCloseScene()
|
|
{
|
|
UpdateGraphEnabled();
|
|
|
|
AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusDisconnect();
|
|
}
|
|
|
|
void MainWindow::OnCryEditorSceneClosed()
|
|
{
|
|
UpdateGraphEnabled();
|
|
|
|
// Close all the open editor graphs when the level is closed, and stop listening
|
|
// for Editor Entity property changes since our graphs are tied to the level data
|
|
CloseAllEditors();
|
|
AzToolsFramework::PropertyEditorEntityChangeNotificationBus::MultiHandler::BusDisconnect();
|
|
}
|
|
|
|
void MainWindow::UpdateGraphEnabled()
|
|
{
|
|
bool isLevelLoaded = GetLegacyEditor()->IsLevelLoaded();
|
|
|
|
// Disable being able to drag from the node palette to the empty dock window
|
|
// to create a new graph when a level isn't loaded
|
|
GetCentralDockWindow()->GetEmptyDockWidget()->setAcceptDrops(isLevelLoaded);
|
|
|
|
// Disable the new graph menu action when no level is loaded
|
|
if (m_fileNewAction)
|
|
{
|
|
m_fileNewAction->setEnabled(isLevelLoaded);
|
|
}
|
|
|
|
// Extra safety check to prevent our tool from creating Entities if a node is added to a graph
|
|
// This in theory shouldn't be hit since we are preventing new graphs from being created
|
|
// in the first place, but is just an extra precaution
|
|
m_ignoreGraphUpdates = !isLevelLoaded;
|
|
}
|
|
|
|
void MainWindow::PostOnActiveGraphChanged()
|
|
{
|
|
// Update our selection in our custom Node Inspector when the active graph changes
|
|
OnSelectionChanged();
|
|
}
|
|
|
|
AZ::u32 MainWindow::GetWrappedNodeLayoutOrder(GraphModel::NodePtr node)
|
|
{
|
|
AZ::u32 layoutOrder = GraphModel::DefaultWrappedNodeLayoutOrder;
|
|
if (!node)
|
|
{
|
|
return layoutOrder;
|
|
}
|
|
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (!baseNodePtr)
|
|
{
|
|
return layoutOrder;
|
|
}
|
|
|
|
// Find the layout order for the component type in our mapping
|
|
AZ::TypeId componentTypeId;
|
|
LandscapeCanvas::LandscapeCanvasNodeFactoryRequestBus::BroadcastResult(componentTypeId, &LandscapeCanvas::LandscapeCanvasNodeFactoryRequests::GetComponentTypeId, baseNodePtr->RTTI_GetType());
|
|
|
|
auto it = m_wrappedNodeLayoutOrderMap.find(componentTypeId);
|
|
if (it != m_wrappedNodeLayoutOrderMap.end())
|
|
{
|
|
layoutOrder = it->second;
|
|
}
|
|
|
|
return layoutOrder;
|
|
}
|
|
|
|
AZ::EntityId MainWindow::GetRootEntityIdForGraphId(const GraphCanvas::GraphId& graphId)
|
|
{
|
|
for (const auto& pair : m_dockWidgetsByEntity)
|
|
{
|
|
const GraphCanvas::DockWidgetId& dockWidgetId = pair.second;
|
|
|
|
GraphCanvas::GraphId dockGraphId;
|
|
GraphCanvas::EditorDockWidgetRequestBus::EventResult(dockGraphId, dockWidgetId, &GraphCanvas::EditorDockWidgetRequests::GetGraphId);
|
|
if (dockGraphId == graphId)
|
|
{
|
|
return pair.first;
|
|
}
|
|
}
|
|
|
|
return AZ::EntityId();
|
|
}
|
|
|
|
AZ::ComponentId MainWindow::AddComponentTypeIdToEntity(const AZ::EntityId& entityId, AZ::TypeId componentToAddTypeId)
|
|
{
|
|
using namespace AzToolsFramework;
|
|
|
|
// Add the corresponding Component for this node to its representative Entity,
|
|
// and any required Components it may need by keeping track of any missing required
|
|
// services that are reported when the Component(s) are added
|
|
EntityCompositionRequests::ComponentServicesList missingRequiredServices;
|
|
AZ::ComponentId requestedComponentId = AZ::InvalidComponentId;
|
|
do
|
|
{
|
|
AZ::ComponentDescriptor* componentDescriptor = nullptr;
|
|
AZ::ComponentDescriptorBus::EventResult(componentDescriptor, componentToAddTypeId, &AZ::ComponentDescriptor::GetDescriptor);
|
|
AZ_Assert(componentDescriptor, "Unable to find ComponentDescriptor for %s.", componentToAddTypeId.ToString<AZStd::string>().c_str());
|
|
|
|
// Find what (if any) services are provided by the Component we are about to add,
|
|
// and remove them from the list of missing required services are are tracking
|
|
AZ::ComponentDescriptor::DependencyArrayType providedServices;
|
|
componentDescriptor->GetProvidedServices(providedServices, nullptr);
|
|
for (const auto& service : providedServices)
|
|
{
|
|
auto it = AZStd::find(missingRequiredServices.begin(), missingRequiredServices.end(), service);
|
|
if (it != missingRequiredServices.end())
|
|
{
|
|
missingRequiredServices.erase(it);
|
|
}
|
|
}
|
|
|
|
// Add the Component to the Vegetation Entity
|
|
EntityCompositionRequests::AddComponentsOutcome outcome = AZ::Failure(AZStd::string());
|
|
EntityCompositionRequestBus::BroadcastResult(outcome, &EntityCompositionRequests::AddComponentsToEntities, EntityIdList{ entityId }, AZ::ComponentTypeList{ componentToAddTypeId });
|
|
AZ_Assert(outcome.IsSuccess(), "Failed to add component %s", componentToAddTypeId.ToString<AZStd::string>().c_str());
|
|
|
|
// Capture the ComponentId for the original component type that was requested to be added
|
|
if (requestedComponentId == AZ::InvalidComponentId)
|
|
{
|
|
const AZ::Entity::ComponentArrayType& componentsAdded = outcome.GetValue()[entityId].m_componentsAdded;
|
|
AZ_Assert(!componentsAdded.empty(), "Failed to add component %s", componentToAddTypeId.ToString<AZStd::string>().c_str());
|
|
|
|
requestedComponentId = componentsAdded.front()->GetId();
|
|
}
|
|
|
|
// After the Component has been added, check if it is missing any required services
|
|
// by checking the m_addedPendingComponents property in the outcome, which means
|
|
// the Component was added to the Entity, but is missing one or more required services.
|
|
// If m_addedPendingComponents is empty, then that means the Component was added
|
|
// with no issues, so we can continue.
|
|
const AZ::Entity::ComponentArrayType& pendingComponents = outcome.GetValue()[entityId].m_addedPendingComponents;
|
|
if (!pendingComponents.empty())
|
|
{
|
|
AZ::Component* component = pendingComponents.front();
|
|
|
|
// Find the missing required services for the pending Component,
|
|
// and them to our list (if it wasn't in the list already).
|
|
EntityCompositionRequests::PendingComponentInfo pendingComponentInfo;
|
|
EntityCompositionRequestBus::BroadcastResult(pendingComponentInfo, &EntityCompositionRequests::GetPendingComponentInfo, component);
|
|
for (const auto& service : pendingComponentInfo.m_missingRequiredServices)
|
|
{
|
|
if (AZStd::find(missingRequiredServices.begin(), missingRequiredServices.end(), service) == missingRequiredServices.end())
|
|
{
|
|
missingRequiredServices.push_back(service);
|
|
}
|
|
}
|
|
|
|
// Disable any components that are incompatible with the component we have added
|
|
if (!pendingComponentInfo.m_validComponentsThatAreIncompatible.empty())
|
|
{
|
|
AzToolsFramework::EntityCompositionRequestBus::Broadcast(&AzToolsFramework::EntityCompositionRequests::DisableComponents, pendingComponentInfo.m_validComponentsThatAreIncompatible);
|
|
}
|
|
}
|
|
|
|
// If we are missing any required services, use the ComponentPaletteUtil::ComponentDataTable to find
|
|
// what components will satisfy them, then choose one to be added and repeat the loop so we can find
|
|
// any additional required services that Component may need
|
|
if (!missingRequiredServices.empty())
|
|
{
|
|
ComponentPaletteUtil::ComponentDataTable componentDataTable;
|
|
ComponentPaletteUtil::ComponentIconTable componentIconTable;
|
|
ComponentPaletteUtil::BuildComponentTables(m_serializeContext, AppearsInGameComponentMenu, missingRequiredServices, componentDataTable, componentIconTable);
|
|
AZ_Assert(componentDataTable.size(), "No components found that satisfy the missing required service(s).");
|
|
|
|
componentToAddTypeId = PickComponentTypeIdToAdd(componentDataTable);
|
|
}
|
|
|
|
} while (!missingRequiredServices.empty());
|
|
|
|
return requestedComponentId;
|
|
}
|
|
|
|
void MainWindow::HandleNodeCreated(GraphModel::NodePtr node)
|
|
{
|
|
using namespace LandscapeCanvas;
|
|
|
|
auto* baseNodePtr = static_cast<BaseNode*>(node.get());
|
|
if (!baseNodePtr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GraphCanvas::GraphId graphId = (*GraphModelIntegration::GraphControllerNotificationBus::GetCurrentBusId());
|
|
AZ::EntityId rootEntityId = GetRootEntityIdForGraphId(graphId);
|
|
if (!rootEntityId.IsValid())
|
|
{
|
|
AZ_Assert(false, "No root Entity associated with this graph.");
|
|
return;
|
|
}
|
|
|
|
m_ignoreGraphUpdates = true;
|
|
|
|
// If the new node already has a valid EntityId, then it means the node was copy/pasted, so we need
|
|
// to find the corresponding deserialized Entity and fix-up the references
|
|
AZ::EntityId existingEntityId = baseNodePtr->GetVegetationEntityId();
|
|
if (existingEntityId.IsValid())
|
|
{
|
|
LandscapeCanvasSerialization serialization;
|
|
LandscapeCanvasSerializationRequestBus::BroadcastResult(serialization, &LandscapeCanvasSerializationRequests::GetSerializedMappings);
|
|
|
|
auto it = serialization.m_deserializedEntities.find(existingEntityId);
|
|
if (it != serialization.m_deserializedEntities.end())
|
|
{
|
|
AZ::EntityId newEntityId = it->second;
|
|
|
|
AZ::TypeId componentTypeId;
|
|
LandscapeCanvasNodeFactoryRequestBus::BroadcastResult(componentTypeId, &LandscapeCanvasNodeFactoryRequests::GetComponentTypeId, baseNodePtr->RTTI_GetType());
|
|
if (!componentTypeId.IsNull())
|
|
{
|
|
AZ::Entity* newEntity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(newEntity, &AZ::ComponentApplicationRequests::FindEntity, newEntityId);
|
|
AZ_Assert(newEntity, "Unable to find deserialized Entity");
|
|
|
|
// Find the component on the Entity that corresponds to this node
|
|
AZ::Component* newComponent = newEntity->FindComponent(componentTypeId);
|
|
if (!newComponent)
|
|
{
|
|
// The FindComponent won't find a component if its disabled, so if it failed
|
|
// then look through the disabled components on this Entity
|
|
AZ::Entity::ComponentArrayType disabledComponents;
|
|
AzToolsFramework::EditorDisabledCompositionRequestBus::Event(newEntityId, &AzToolsFramework::EditorDisabledCompositionRequests::GetDisabledComponents, disabledComponents);
|
|
for (auto disabledComponent : disabledComponents)
|
|
{
|
|
if (disabledComponent->RTTI_GetType() == componentTypeId)
|
|
{
|
|
newComponent = disabledComponent;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Look through the pending components next if we didn't find it in the disabled components,
|
|
// since it may be put in the pending bucket if a dependent component is actually deleted
|
|
// instead of just being disabled
|
|
if (!newComponent)
|
|
{
|
|
AZ::Entity::ComponentArrayType pendingComponents;
|
|
AzToolsFramework::EditorPendingCompositionRequestBus::Event(newEntityId, &AzToolsFramework::EditorPendingCompositionRequests::GetPendingComponents, pendingComponents);
|
|
for (AZ::Component* pendingComponent : pendingComponents)
|
|
{
|
|
if (pendingComponent->RTTI_GetType() == componentTypeId)
|
|
{
|
|
newComponent = pendingComponent;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the component for this node is disabled, then the node needs to be disabled as well
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::DisableNode, node);
|
|
}
|
|
|
|
AZ_Assert(newComponent, "Deserialized Entity missing component matching node");
|
|
|
|
// Fix-up the references on the new node to the deserialized Entity/Component
|
|
baseNodePtr->SetVegetationEntityId(newEntityId);
|
|
baseNodePtr->SetComponentId(newComponent->GetId());
|
|
}
|
|
}
|
|
}
|
|
// Otherwise, this new node was created by the user from the node palette or right-click menu,
|
|
// so create a fresh Entity/Component for the node, except we need to ignore area extender
|
|
// nodes since they aren't wrapped until after they're created, so we won't know which
|
|
// Entity to add the Component to at this point. The OnGraphModelNodeWrapped event will
|
|
// add the area extender component.
|
|
else if (!baseNodePtr->IsAreaExtender())
|
|
{
|
|
// Creating a node is actually two operations: creating an Entity + adding a component(s) to that Entity
|
|
// so we need to batch the operations so that undo/redo will treat it all as one operation
|
|
AzToolsFramework::ScopedUndoBatch undoBatch("Create Node");
|
|
|
|
// Create a new Entity to hold the Component for this new node
|
|
AZ::EntityId vegetationEntityId;
|
|
AzToolsFramework::EditorRequestBus::BroadcastResult(vegetationEntityId, &AzToolsFramework::EditorRequests::CreateNewEntity, rootEntityId);
|
|
|
|
// Add the Component for this node, as well as any required components
|
|
AddComponentForNode(node, vegetationEntityId);
|
|
}
|
|
|
|
m_ignoreGraphUpdates = false;
|
|
}
|
|
|
|
void MainWindow::AddComponentForNode(GraphModel::NodePtr node, const AZ::EntityId& entityId)
|
|
{
|
|
auto* baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (!baseNodePtr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZ::TypeId componentToAddTypeId;
|
|
LandscapeCanvas::LandscapeCanvasNodeFactoryRequestBus::BroadcastResult(componentToAddTypeId, &LandscapeCanvas::LandscapeCanvasNodeFactoryRequests::GetComponentTypeId, baseNodePtr->RTTI_GetType());
|
|
if (componentToAddTypeId.IsNull())
|
|
{
|
|
AZ_Assert(false, "Node missing a registered component TypeId.");
|
|
return;
|
|
}
|
|
|
|
AZ::ComponentId newComponentId = AddComponentTypeIdToEntity(entityId, componentToAddTypeId);
|
|
|
|
// Tie this new node to its representative Entity and Component
|
|
baseNodePtr->SetVegetationEntityId(entityId);
|
|
baseNodePtr->SetComponentId(newComponentId);
|
|
}
|
|
|
|
void MainWindow::HandleNodeAdded(GraphModel::NodePtr node)
|
|
{
|
|
auto* baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
if (!baseNodePtr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Update our EntityId/Node mappings when a new node is added
|
|
GraphCanvas::GraphId graphId = (*GraphModelIntegration::GraphControllerNotificationBus::GetCurrentBusId());
|
|
if (!m_ignoreGraphUpdates)
|
|
{
|
|
UpdateEntityIdNodeMap(graphId, node);
|
|
}
|
|
|
|
// For any node with an Entity Name slot, we need to replace the string property display with a read-only version
|
|
// instead until we have support for listening for GraphModel slot value changes. We need to delay this because
|
|
// when the node is added, the slots haven't been added to the element map yet.
|
|
QTimer::singleShot(0, [node, graphId]() {
|
|
GraphModel::SlotPtr slot = node->GetSlot(LandscapeCanvas::ENTITY_NAME_SLOT_ID);
|
|
if (slot)
|
|
{
|
|
GraphCanvas::NodeId nodeId;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(nodeId, graphId, &GraphModelIntegration::GraphControllerRequests::GetNodeIdByNode, node);
|
|
|
|
GraphCanvas::SlotId slotId;
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(slotId, graphId, &GraphModelIntegration::GraphControllerRequests::GetSlotIdBySlot, slot);
|
|
|
|
// The ownership of the new data interface and property display get passed to the node property display widget
|
|
// when we call SetNodePropertyDisplay
|
|
auto dataInterface = aznew GraphModelIntegration::ReadOnlyDataInterface(slot);
|
|
GraphCanvas::NodePropertyDisplay* readOnlyPropertyDisplay = nullptr;
|
|
GraphCanvas::GraphCanvasRequestBus::BroadcastResult(readOnlyPropertyDisplay, &GraphCanvas::GraphCanvasRequests::CreateReadOnlyNodePropertyDisplay, static_cast<GraphCanvas::ReadOnlyDataInterface*>(dataInterface));
|
|
|
|
readOnlyPropertyDisplay->SetNodeId(nodeId);
|
|
readOnlyPropertyDisplay->SetSlotId(slotId);
|
|
GraphCanvas::NodePropertyRequestBus::Event(slotId, &GraphCanvas::NodePropertyRequests::SetNodePropertyDisplay, readOnlyPropertyDisplay);
|
|
}
|
|
});
|
|
|
|
// Listen for component property changes on the Entity corresponding to this node
|
|
AzToolsFramework::PropertyEditorEntityChangeNotificationBus::MultiHandler::BusConnect(baseNodePtr->GetVegetationEntityId());
|
|
|
|
LandscapeCanvas::BaseNode::BaseNodeType nodeType = baseNodePtr->GetBaseNodeType();
|
|
if (nodeType == LandscapeCanvas::BaseNode::Shape)
|
|
{
|
|
// Add thumbnail image of the shape type to the node
|
|
AZ::TypeId componentTypeId;
|
|
LandscapeCanvas::LandscapeCanvasNodeFactoryRequestBus::BroadcastResult(componentTypeId, &LandscapeCanvas::LandscapeCanvasNodeFactoryRequests::GetComponentTypeId, baseNodePtr->RTTI_GetType());
|
|
AZStd::string entityIconPath;
|
|
AzToolsFramework::EditorRequestBus::BroadcastResult(entityIconPath, &AzToolsFramework::EditorRequestBus::Events::GetComponentIconPath, componentTypeId, AZ::Edit::Attributes::ViewportIcon, nullptr);
|
|
if (!entityIconPath.empty())
|
|
{
|
|
QPixmap iconPixmap(entityIconPath.c_str());
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::SetThumbnailImageOnNode, node, iconPixmap);
|
|
}
|
|
}
|
|
else if (nodeType == LandscapeCanvas::BaseNode::Gradient || nodeType == LandscapeCanvas::BaseNode::GradientGenerator || nodeType == LandscapeCanvas::BaseNode::GradientModifier)
|
|
{
|
|
// Add custom gradient preview thumbnail to all gradient type nodes
|
|
// The node layout takes ownership of the thumbnail, so it will be deleted whenever the node is deleted
|
|
const AZ::EntityId& gradientEntityId = baseNodePtr->GetVegetationEntityId();
|
|
GradientPreviewThumbnailItem* previewThumbnail = new GradientPreviewThumbnailItem(gradientEntityId);
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::SetThumbnailOnNode, node, previewThumbnail);
|
|
}
|
|
else if (nodeType == LandscapeCanvas::BaseNode::VegetationArea)
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::Event(graphId, &GraphModelIntegration::GraphControllerRequests::SetWrapperNodeActionString, node, QObject::tr("Add Extenders").toUtf8().constData());
|
|
}
|
|
}
|
|
|
|
void MainWindow::UpdateEntityIdNodeMap(GraphCanvas::GraphId graphId, GraphModel::NodePtr node)
|
|
{
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
const AZ::EntityId& entityId = baseNodePtr->GetVegetationEntityId();
|
|
auto nodeMap = GetEntityIdNodeMap(graphId, node);
|
|
if (nodeMap)
|
|
{
|
|
nodeMap->insert({ entityId, node });
|
|
}
|
|
}
|
|
|
|
MainWindow::EntityIdNodeMap* MainWindow::GetEntityIdNodeMap(GraphCanvas::GraphId graphId, GraphModel::NodePtr node)
|
|
{
|
|
auto nodeMapsIt = m_entityIdNodeMapsByGraph.find(graphId);
|
|
if (nodeMapsIt == m_entityIdNodeMapsByGraph.end())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Return the corresponding EntityIdNodeMap for this node type
|
|
EntityIdNodeMaps& nodeMaps = nodeMapsIt->second;
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
LandscapeCanvas::BaseNode::BaseNodeType baseNodeType = baseNodePtr->GetBaseNodeType();
|
|
auto nodeMapType = EntityIdNodeMapEnum::Invalid;
|
|
switch (baseNodeType)
|
|
{
|
|
case LandscapeCanvas::BaseNode::Shape:
|
|
nodeMapType = EntityIdNodeMapEnum::Shapes;
|
|
break;
|
|
case LandscapeCanvas::BaseNode::VegetationArea:
|
|
nodeMapType = EntityIdNodeMapEnum::VegetationAreas;
|
|
break;
|
|
case LandscapeCanvas::BaseNode::Gradient:
|
|
case LandscapeCanvas::BaseNode::GradientGenerator:
|
|
case LandscapeCanvas::BaseNode::GradientModifier:
|
|
nodeMapType = EntityIdNodeMapEnum::Gradients;
|
|
break;
|
|
}
|
|
|
|
if (nodeMapType != EntityIdNodeMapEnum::Invalid)
|
|
{
|
|
return &nodeMaps[nodeMapType];
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void MainWindow::ParseNodeConnections(GraphCanvas::GraphId graphId, GraphModel::NodePtr node, ConnectionsList& connections)
|
|
{
|
|
auto baseNodePtr = static_cast<LandscapeCanvas::BaseNode*>(node.get());
|
|
AZ::Component* component = baseNodePtr->GetComponent();
|
|
if (!component)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Find the node mappings for this graph
|
|
auto nodeMapsIt = m_entityIdNodeMapsByGraph.find(graphId);
|
|
if (nodeMapsIt == m_entityIdNodeMapsByGraph.end())
|
|
{
|
|
return;
|
|
}
|
|
const EntityIdNodeMaps& nodeMaps = nodeMapsIt->second;
|
|
|
|
// Iterate through the component class elements to find any matching fields corresponding
|
|
// to input slots
|
|
AZ::EntityId previewEntityId, inboundShapeEntityId;
|
|
AzToolsFramework::EntityIdList gradientSamplerIds;
|
|
AzToolsFramework::EntityIdList vegetationAreaIds;
|
|
m_serializeContext->EnumerateObject(component,
|
|
// beginElemCB
|
|
[&previewEntityId, &inboundShapeEntityId, &gradientSamplerIds, &vegetationAreaIds](void *instance, [[maybe_unused]] const AZ::SerializeContext::ClassData *classData, const AZ::SerializeContext::ClassElement *classElement) -> bool
|
|
{
|
|
if (classElement && (classElement->m_typeId == azrtti_typeid<AZ::EntityId>()))
|
|
{
|
|
if (strcmp(classElement->m_name, PreviewEntityElementName) == 0)
|
|
{
|
|
previewEntityId = *reinterpret_cast<AZ::EntityId*>(instance);
|
|
return false;
|
|
}
|
|
else if (strcmp(classElement->m_name, GradientIdElementName) == 0)
|
|
{
|
|
gradientSamplerIds.push_back(*reinterpret_cast<AZ::EntityId*>(instance));
|
|
return false;
|
|
}
|
|
else if (strcmp(classElement->m_name, VegetationAreaEntityIdElementName) == 0)
|
|
{
|
|
vegetationAreaIds.push_back(*reinterpret_cast<AZ::EntityId*>(instance));
|
|
return false;
|
|
}
|
|
else if (strcmp(classElement->m_name, ShapeEntityIdElementName) == 0)
|
|
{
|
|
inboundShapeEntityId = *reinterpret_cast<AZ::EntityId*>(instance);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
// endElemCB
|
|
[]() -> bool { return true; },
|
|
AZ::SerializeContext::ENUM_ACCESS_FOR_READ, nullptr/* errorHandler */);
|
|
|
|
// Connect any preview entities to the corresponding shape bounds
|
|
AZStd::vector<AZStd::pair<GraphModel::SlotId, AZ::EntityId>> shapeSlotEntityPairs;
|
|
if (previewEntityId.IsValid())
|
|
{
|
|
shapeSlotEntityPairs.push_back(AZStd::make_pair(LandscapeCanvas::PREVIEW_BOUNDS_SLOT_ID, previewEntityId));
|
|
}
|
|
|
|
// Connect any inbound shape slots to the corresponding shape bounds
|
|
if (inboundShapeEntityId.IsValid())
|
|
{
|
|
// We have two different inbound shape slots that share the same underlying property,
|
|
// so we need to figure out which kind of inbound shape slot this node has
|
|
GraphModel::SlotId shapeSlotId(LandscapeCanvas::INBOUND_SHAPE_SLOT_ID);
|
|
if (!node->GetSlot(shapeSlotId))
|
|
{
|
|
shapeSlotId = GraphModel::SlotId(LandscapeCanvas::PIN_TO_SHAPE_SLOT_ID);
|
|
}
|
|
|
|
shapeSlotEntityPairs.push_back(AZStd::make_pair(shapeSlotId, inboundShapeEntityId));
|
|
}
|
|
|
|
// Look for a placement bounds on Vegetation Areas, which is a special case since it could be
|
|
// driven by a Reference Shape or actual Shape component that also exists on the same Entity
|
|
// as the Vegetation Area Component that we represent with the node, but in this case the
|
|
// component will actually be shown as a Placement Bounds slot
|
|
AZ::EntityId placementBoundsEntityId;
|
|
if (baseNodePtr->GetBaseNodeType() == LandscapeCanvas::BaseNode::BaseNodeType::VegetationArea)
|
|
{
|
|
GraphModel::SlotPtr placementBoundsSlot = node->GetSlot(LandscapeCanvas::PLACEMENT_BOUNDS_SLOT_ID);
|
|
if (placementBoundsSlot)
|
|
{
|
|
// Retrieve the Placement Bounds slot value from the Reference Shape component if it exists
|
|
auto baseAreaNodePtr = static_cast<LandscapeCanvas::BaseAreaNode*>(node.get());
|
|
AZ::Component* referenceShapeComponent = baseAreaNodePtr->GetReferenceShapeComponent();
|
|
if (referenceShapeComponent)
|
|
{
|
|
QString propertyPath = GetPropertyPathForSlot(placementBoundsSlot, LandscapeCanvas::LandscapeCanvasDataTypeEnum::Bounds);
|
|
AzToolsFramework::PropertyTreeEditor pte = AzToolsFramework::PropertyTreeEditor(reinterpret_cast<void*>(referenceShapeComponent), referenceShapeComponent->RTTI_GetType());
|
|
auto placementBounds = pte.GetProperty(propertyPath.toUtf8().constData());
|
|
if (placementBounds.IsSuccess())
|
|
{
|
|
placementBoundsEntityId = AZStd::any_cast<AZ::EntityId>(placementBounds.GetValue());
|
|
if (placementBoundsEntityId.IsValid())
|
|
{
|
|
shapeSlotEntityPairs.push_back(AZStd::make_pair(LandscapeCanvas::PLACEMENT_BOUNDS_SLOT_ID, placementBoundsEntityId));
|
|
}
|
|
}
|
|
}
|
|
// Otherwise, also check if this Entity has its own Shape component as well that will serve
|
|
// as the placement bounds
|
|
else
|
|
{
|
|
const auto& shapeNodeMap = nodeMaps[EntityIdNodeMapEnum::Shapes];
|
|
const AZ::EntityId& entityId = baseAreaNodePtr->GetVegetationEntityId();
|
|
auto shapeIt = shapeNodeMap.find(entityId);
|
|
if (shapeIt != shapeNodeMap.end())
|
|
{
|
|
GraphModel::NodePtr shapeNode = (*shapeIt).second;
|
|
auto baseShapeNodePtr = static_cast<LandscapeCanvas::BaseNode*>(shapeNode.get());
|
|
if (baseShapeNodePtr->GetComponent())
|
|
{
|
|
shapeSlotEntityPairs.push_back(AZStd::make_pair(LandscapeCanvas::PLACEMENT_BOUNDS_SLOT_ID, entityId));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Connect any input bounds slots to their corresponding shape bounds
|
|
const auto& shapeNodeMap = nodeMaps[EntityIdNodeMapEnum::Shapes];
|
|
for (auto slotEntityPair : shapeSlotEntityPairs)
|
|
{
|
|
const AZ::EntityId& entityId = slotEntityPair.second;
|
|
auto shapeIt = shapeNodeMap.find(entityId);
|
|
if (shapeIt == shapeNodeMap.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
GraphModel::NodePtr shapeNode = (*shapeIt).second;
|
|
GraphModel::SlotPtr shapeBoundsSlot = shapeNode->GetSlot(LandscapeCanvas::BaseShapeNode::BOUNDS_SLOT_ID);
|
|
GraphModel::SlotPtr shapeTargetSlot = node->GetSlot(slotEntityPair.first);
|
|
|
|
auto source = AZStd::make_pair(shapeNode, shapeBoundsSlot);
|
|
auto target = AZStd::make_pair(node, shapeTargetSlot);
|
|
connections.push_back(AZStd::make_pair(source, target));
|
|
}
|
|
|
|
auto handleIndexedSlots = [this, graphId, node, &connections](AzToolsFramework::EntityIdList entityIds, const EntityIdNodeMap& sourceNodeMap, GraphModel::SlotName outboundSlotId, LandscapeCanvas::LandscapeCanvasDataTypeEnum slotDataType)
|
|
{
|
|
if (entityIds.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
size_t numEntityIds = entityIds.size();
|
|
for (int i = 0; i < numEntityIds; ++i)
|
|
{
|
|
const AZ::EntityId& entityId = entityIds[i];
|
|
if (!entityId.IsValid())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Find the source node
|
|
GraphModel::NodePtr sourceNode;
|
|
auto nodeIt = sourceNodeMap.find(entityId);
|
|
if (nodeIt != sourceNodeMap.end())
|
|
{
|
|
sourceNode = (*nodeIt).second;
|
|
}
|
|
|
|
if (sourceNode)
|
|
{
|
|
// Don't allow a node's output to be connected to itself
|
|
if (sourceNode == node)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
GraphModel::SlotPtr outboundSlot = sourceNode->GetSlot(outboundSlotId);
|
|
|
|
// Find the corresponding input slot based on the index
|
|
GraphModel::DataTypePtr dataType = GetGraphContext()->GetDataType(slotDataType);
|
|
GraphModel::SlotPtr inboundSlot = EnsureInboundDataSlotWithIndex(graphId, node, dataType, i);
|
|
if (!inboundSlot)
|
|
{
|
|
AZ_Assert(false, "Unhandled inbound slot mapping.");
|
|
continue;
|
|
}
|
|
|
|
auto source = AZStd::make_pair(sourceNode, outboundSlot);
|
|
auto target = AZStd::make_pair(node, inboundSlot);
|
|
connections.push_back(AZStd::make_pair(source, target));
|
|
}
|
|
}
|
|
};
|
|
|
|
// Connect any inbound gradient slots to the corresponding Gradient, Gradient Generator, or Gradient Modifier
|
|
handleIndexedSlots(gradientSamplerIds, nodeMaps[EntityIdNodeMapEnum::Gradients], LandscapeCanvas::OUTBOUND_GRADIENT_SLOT_ID, LandscapeCanvas::LandscapeCanvasDataTypeEnum::Gradient);
|
|
|
|
// Connect any inbound vegetation area slots to the corresponding vegetation area
|
|
handleIndexedSlots(vegetationAreaIds, nodeMaps[EntityIdNodeMapEnum::VegetationAreas], LandscapeCanvas::OUTBOUND_AREA_SLOT_ID, LandscapeCanvas::LandscapeCanvasDataTypeEnum::Area);
|
|
}
|
|
|
|
int MainWindow::GetInboundDataSlotIndex(GraphModel::NodePtr node, GraphModel::DataTypePtr dataType, GraphModel::SlotPtr targetSlot)
|
|
{
|
|
if (!node)
|
|
{
|
|
return InvalidSlotIndex;
|
|
}
|
|
|
|
// Return the index of the specified targetSlot based on the input data slots that match the specified data type on the given node
|
|
int index = 0;
|
|
for (auto& it : node->GetSlots())
|
|
{
|
|
GraphModel::SlotPtr slot = it.second;
|
|
|
|
if (slot->Is(GraphModel::SlotDirection::Input, GraphModel::SlotType::Data))
|
|
{
|
|
// Our Bounds and Gradient data types are both AZ::EntityId under the hood, so there is
|
|
// some magic that takes place where they each support an Invalid data type as well as
|
|
// their specific data type, so instead of comparing the current slot->GetDataType() directly
|
|
// we need to check the possible data types instead for a match.
|
|
const auto& dataTypes = slot->GetPossibleDataTypes();
|
|
auto iter = AZStd::find(dataTypes.begin(), dataTypes.end(), dataType);
|
|
if (iter != dataTypes.end())
|
|
{
|
|
if (slot == targetSlot)
|
|
{
|
|
return index;
|
|
}
|
|
else
|
|
{
|
|
++index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return InvalidSlotIndex;
|
|
}
|
|
|
|
GraphModel::SlotPtr MainWindow::EnsureInboundDataSlotWithIndex(GraphCanvas::GraphId graphId, GraphModel::NodePtr node, GraphModel::DataTypePtr dataType, int index)
|
|
{
|
|
// Iterate through all the slots on the node to find an input data slot that matches the specified data type for the specified index
|
|
int currentIndex = 0;
|
|
for (GraphModel::SlotDefinitionPtr slotDefinition : node->GetSlotDefinitions())
|
|
{
|
|
if (slotDefinition->Is(GraphModel::SlotDirection::Input, GraphModel::SlotType::Data))
|
|
{
|
|
const GraphModel::SlotName& slotName = slotDefinition->GetName();
|
|
const auto& dataTypes = slotDefinition->GetSupportedDataTypes();
|
|
auto iter = AZStd::find(dataTypes.begin(), dataTypes.end(), dataType);
|
|
if (iter != dataTypes.end())
|
|
{
|
|
if (slotDefinition->SupportsExtendability())
|
|
{
|
|
// The subId for the extendable slots aren't necessarily an index starting at 0, depending
|
|
// on if the user removes/re-adds slots, so we first need to check if we need to offset
|
|
// the index we are expecting based on the starting subId
|
|
int subIdOffset = 0;
|
|
auto extendableSlots = node->GetExtendableSlots(slotName);
|
|
if (!extendableSlots.empty())
|
|
{
|
|
GraphModel::SlotPtr firstSlot = *extendableSlots.begin();
|
|
subIdOffset = firstSlot->GetSlotSubId();
|
|
index += subIdOffset;
|
|
}
|
|
|
|
GraphModel::SlotId slotId(slotName, index);
|
|
|
|
// If it's an extendable slot, we need to add enough to be able to accommodate the specified index.
|
|
for (int i = node->GetExtendableSlotCount(slotName) + subIdOffset; i < index + 1; ++i)
|
|
{
|
|
// If we fail to add an extended slot at any point (e.g. reached maximum, node has custom logic overriding, etc..)
|
|
// then we need to bail out. We need to add the extended slot using a different API when we are doing an initial
|
|
// graph vs. if the graph is already loaded because in the former case the node hasn't been fully created yet so
|
|
// we are just updating the data model, whereas in the latter case the node already exists in the graph and so
|
|
// we need to use the GraphController API so that the UI gets updated properly.
|
|
bool success = false;
|
|
if (node->GetId() == GraphModel::Node::INVALID_NODE_ID)
|
|
{
|
|
success = node->AddExtendedSlot(slotName);
|
|
}
|
|
else
|
|
{
|
|
GraphModelIntegration::GraphControllerRequestBus::EventResult(slotId, graphId, &GraphModelIntegration::GraphControllerRequests::ExtendSlot, node, slotName);
|
|
success = slotId.IsValid();
|
|
}
|
|
|
|
if (!success)
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return node->GetSlot(slotId);
|
|
}
|
|
else if (currentIndex == index)
|
|
{
|
|
return node->GetSlot(slotName);
|
|
}
|
|
|
|
++currentIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
#include <Source/Editor/moc_MainWindow.cpp>
|