diff --git a/Changes.md b/Changes.md index 4b53fe3280a..f1d2b4969cb 100644 --- a/Changes.md +++ b/Changes.md @@ -1,12 +1,23 @@ 1.3.x.x (relative to 1.3.5.0) ======= +Features +-------- + +- LightTool : + - Added manipulator for disk and point light radii. + - Added manipulators for cylinder length and radius. + +Improvements +------------ + +- LightTool : Changed spot light and quad light edge tool tip locations so that they follow the cone and edge during drag. + Fixes ----- - Windows : Fixed a bug preventing anything except strings from being copied and pasted. - 1.3.5.0 (relative to 1.3.4.0) ======= diff --git a/include/GafferSceneUI/Private/Inspector.h b/include/GafferSceneUI/Private/Inspector.h index 04d641bb808..8350039da93 100644 --- a/include/GafferSceneUI/Private/Inspector.h +++ b/include/GafferSceneUI/Private/Inspector.h @@ -284,6 +284,12 @@ class GAFFERSCENEUI_API Inspector::Result : public IECore::RefCounted /// The inspected value that should be displayed by the UI. const IECore::Object *value() const; + /// The inspected value cast to its native type. If the inspected + /// value is not of the requested type, the given default value + /// will be returned. + template + const T typedValue( const T &defaultValue ) const; + /// The plug that was used to author the current value, or null if /// it cannot be determined. Gaffer::ValuePlug *source() const; @@ -346,3 +352,5 @@ class GAFFERSCENEUI_API Inspector::Result : public IECore::RefCounted } // namespace Private } // namespace GafferSceneUI + +#include "GafferSceneUI/Private/Inspector.inl" diff --git a/include/GafferSceneUI/Private/Inspector.inl b/include/GafferSceneUI/Private/Inspector.inl new file mode 100644 index 00000000000..80e8c0c08b5 --- /dev/null +++ b/include/GafferSceneUI/Private/Inspector.inl @@ -0,0 +1,61 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2023, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided with +// the distribution. +// +// * Neither the name of John Haddon nor the names of +// any other contributors to this software may be used to endorse or +// promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "IECore/RunTimeTyped.h" +#include "IECore/SimpleTypedData.h" + +namespace GafferSceneUI +{ + +namespace Private +{ + +template +const T Inspector::Result::typedValue( const T &defaultValue ) const +{ + if( auto valueData = IECore::runTimeCast>( value() ) ) + { + return valueData->readable(); + } + + return defaultValue; +} + +} // namespace Private + +} // namespace GafferScene diff --git a/src/GafferSceneUI/LightTool.cpp b/src/GafferSceneUI/LightTool.cpp index f722966391b..59ee6900a64 100644 --- a/src/GafferSceneUI/LightTool.cpp +++ b/src/GafferSceneUI/LightTool.cpp @@ -104,6 +104,8 @@ using namespace GafferSceneUI::Private; namespace { +const std::string g_lightAttributePattern = "light *:light"; + const Color3f g_lightToolHandleColor = Color3f( 0.825, 0.720f, 0.230f ); // Color from `StandardLightVisualiser` @@ -121,16 +123,16 @@ const InternedString g_insetPenumbraType( "inset" ); const InternedString g_outsetPenumbraType( "outset" ); const InternedString g_absolutePenumbraType( "absolute" ); -const float g_circleHandleWidth = 2.5f; -const float g_circleHandleWidthLarge = 3.f; -const float g_circleHandleSelectionWidth = 5.f; +const float g_circleHandleWidth = 4.375f; +const float g_circleHandleWidthLarge = 5.25f; +const float g_circleHandleSelectionWidth = 8.875f; -const float g_lineHandleWidth = 0.5f; -const float g_lineHandleWidthLarge = 1.f; -const float g_lineSelectionWidth = 3.f; +const float g_lineHandleWidth = 0.875f; +const float g_lineHandleWidthLarge = 1.75f; +const float g_lineSelectionWidth = 5.25f; -const float g_minorLineHandleWidth = 0.25f; -const float g_minorLineHandleWidthLarge = 0.5f; +const float g_minorLineHandleWidth = 0.4375f; +const float g_minorLineHandleWidthLarge = 0.875f; const float g_dragArcWidth = 24.f; @@ -138,7 +140,7 @@ const float g_arrowHandleSize = g_circleHandleWidth * 2.f; const float g_arrowHandleSizeLarge = g_circleHandleWidthLarge * 2.f; const float g_arrowHandleSelectionSize = g_circleHandleSelectionWidth * 2.f; -const float g_quadLightHandleSizeMultiplier = 1.75f; +const float g_spotLightHandleSizeMultiplier = 1 / 1.75f; const Color4f g_hoverTextColor( 1, 1, 1, 1 ); @@ -146,7 +148,8 @@ const int g_warningTipCount = 3; const ModifiableEvent::Modifiers g_quadLightConstrainAspectRatioKey = ModifiableEvent::Modifiers::Control; -enum class Axis { X, Y, Z }; +const InternedString g_coneAngleParameter = "coneAngleParameter"; +const InternedString g_penumbraAngleParameter = "penumbraAngleParameter"; // Return the plug that holds the value we need to edit, and make sure it's enabled. @@ -344,14 +347,16 @@ IECoreScene::MeshPrimitivePtr solidArc( float minorRadius, float majorRadius, fl return solidAngle; } -IECoreGL::MeshPrimitivePtr circle() +enum class Axis { X, Y, Z }; + +// Reorients `p` so that `p.z` points along the positive `axis` +V3f axisAlignedVector( const Axis axis, const V3f &p ) { - static IECoreGL::MeshPrimitivePtr result; - if( result ) - { - return result; - } + return axis == Axis::X ? V3f( p.z, p.y, p.x ) : ( axis == Axis::Y ? V3f( p.x, p.z, p.y ) : p ); +} +IECoreGL::MeshPrimitivePtr circle( const Axis axis = Axis::X, const V3f &offset = V3f( 0 ) ) +{ IntVectorDataPtr vertsPerPolyData = new IntVectorData; IntVectorDataPtr vertIdsData = new IntVectorData; V3fVectorDataPtr pData = new V3fVectorData; @@ -360,13 +365,14 @@ IECoreGL::MeshPrimitivePtr circle() std::vector &vertIds = vertIdsData->writable(); std::vector &p = pData->writable(); - p.push_back( V3f( 0 ) ); + p.push_back( offset ); const int numSegments = 20; for( int i = 0; i < numSegments + 1; ++i ) { const float a = ( (float)i / (float)numSegments ) * 2.f * M_PI; - p.push_back( V3f( 0, cos( a ), -sin( a ) ) ); // Face the X-axis + const V3f v = axisAlignedVector( axis, V3f( -sin( a ), cos( a ), 0 ) ); + p.push_back( v + offset ); } for( int i = 0; i < numSegments; ++i ) { @@ -378,7 +384,7 @@ IECoreGL::MeshPrimitivePtr circle() IECoreScene::MeshPrimitivePtr circle = new IECoreScene::MeshPrimitive( vertsPerPolyData, vertIdsData, "linear", pData ); ToGLMeshConverterPtr converter = new ToGLMeshConverter( circle ); - result = runTimeCast( converter->convert() ); + IECoreGL::MeshPrimitivePtr result = runTimeCast( converter->convert() ); return result; } @@ -424,6 +430,7 @@ IECoreGL::MeshPrimitivePtr ring() return result; } +// Returns a (potentially truncated) cone facing the +Z axis. IECoreGL::MeshPrimitivePtr cone( float height, float startRadius, float endRadius ) { IECoreGL::MeshPrimitivePtr result; @@ -442,7 +449,7 @@ IECoreGL::MeshPrimitivePtr cone( float height, float startRadius, float endRadiu const float a = ( (float)i / (float)numSegments ) * 2.f * M_PI; p.push_back( V3f( -sin( a ) * startRadius, cos( a ) * startRadius, 0 ) ); - p.push_back( V3f( -sin( a ) * endRadius, cos( a ) * endRadius, height ) ); // Face the -Z axis + p.push_back( V3f( -sin( a ) * endRadius, cos( a ) * endRadius, height ) ); // Face the +Z axis } for( int i = 0; i < numSegments; ++i ) { @@ -460,18 +467,69 @@ IECoreGL::MeshPrimitivePtr cone( float height, float startRadius, float endRadiu return result; } -const float g_tipScale = 10.f; -const float g_tipIconSize = 1.25f; -const float g_tipIconOffset = -0.25f; -const float g_tipIndent = 1.75f; -const float g_tipLineSpacing = -1.375f; - +// Returns a cone faceing the +Z axis. IECoreGL::MeshPrimitivePtr unitCone() { static IECoreGL::MeshPrimitivePtr result = cone( 1.5f, 0.5f, 0 ); return result; } +IECoreGL::MeshPrimitivePtr torus( const float width, const float height, const float tubeRadius, const Handle *handle, const Axis axis ) +{ + IECoreGL::MeshPrimitivePtr result; + + IECore::IntVectorDataPtr verticesPerFaceData = new IECore::IntVectorData; + std::vector &verticesPerFace = verticesPerFaceData->writable(); + + IECore::IntVectorDataPtr vertexIdsData = new IECore::IntVectorData; + std::vector &vertexIds = vertexIdsData->writable(); + + IECore::V3fVectorDataPtr pData = new IECore::V3fVectorData; + std::vector &p = pData->writable(); + + const V3f radiusScale = V3f( width, height, 0 ); + + const int numDivisionsI = 60; + const int numDivisionsJ = 15; + for( int i = 0; i < numDivisionsI; ++i ) + { + const float iAngle = 2 * M_PI * (float)i / (float)( numDivisionsI - 1 ); + const V3f v = V3f( -sin( iAngle ), cos( iAngle ), 0 ); + const V3f tubeCenter = v * radiusScale; + + const int ii = i == numDivisionsI - 1 ? 0 : i + 1; + + const float jRadius = tubeRadius * rasterScaleFactor( handle, tubeCenter ); + + for( int j = 0; j < numDivisionsJ; ++j ) + { + const float jAngle = 2 * M_PI * (float)j / (float)( numDivisionsJ - 1 ); + + p.push_back( + axisAlignedVector( + axis, + tubeCenter + jRadius * ( cos( jAngle ) * v + V3f( 0, 0, sin( jAngle ) ) ) + ) + ); + + const int jj = j == numDivisionsJ - 1 ? 0 : j + 1; + + verticesPerFace.push_back( 4 ); + + vertexIds.push_back( i * numDivisionsJ + j ); + vertexIds.push_back( i * numDivisionsJ + jj ); + vertexIds.push_back( ii * numDivisionsJ + jj ); + vertexIds.push_back( ii * numDivisionsJ + j ); + } + } + + IECoreScene::MeshPrimitivePtr mesh = new IECoreScene::MeshPrimitive( verticesPerFaceData, vertexIdsData, "linear", pData ); + IECoreGL::ToGLMeshConverterPtr converter = new ToGLMeshConverter( mesh ); + result = runTimeCast( converter->convert() ); + + return result; +} + GraphComponent *commonAncestor( std::vector &graphComponents ) { const size_t gcSize = graphComponents.size(); @@ -498,6 +556,12 @@ GraphComponent *commonAncestor( std::vector &graphComponents ) return commonAncestor; } +const float g_tipScale = 10.f; +const float g_tipIconSize = 1.25f; +const float g_tipIconOffset = -0.25f; +const float g_tipIndent = 1.75f; +const float g_tipLineSpacing = -1.375f; + void drawSelectionTips( const V3f &gadgetSpacePosition, std::vector inspections, @@ -693,6 +757,26 @@ float sphereSpokeClickAngle( const Line3f &eventLine, float radius, float spokeA return true; } +// Returns the intersection point between the line and sphere closest to the line origin. +// If the line and sphere don't intersect, returns the closest point between them. +template +T lineSphereIntersection( const LineSegment &line, const T ¢er, const float radius ) +{ + const LineSegment offsetLine( line.p0 - center, line.p1 - center ); + const T direction = line.direction(); + const float A = direction.dot( direction ); + const float B = 2.f * ( direction ^ ( offsetLine.p0 ) ); + const float C = ( offsetLine.p0 ^ offsetLine.p0 ) - ( radius * radius ); + + const float discriminant = B * B - 4.f * A * C; + if( discriminant < 0 ) + { + return line.closestPointTo( center ); + } + + return line( ( -B - std::sqrt( discriminant ) ) / ( 2.f * A ) ); +} + // ============================================================================ // LightToolHandle // ============================================================================ @@ -702,45 +786,87 @@ class LightToolHandle : public Handle public : - LightToolHandle( - const std::string &attributePattern, - const std::string &name - ) : - Handle( name ), - m_attributePattern( attributePattern ), - m_lookThroughLight( false ) - { + using InspectionMap = std::unordered_map; - } ~LightToolHandle() override { } - // Update inspectors and data needed to display and interact with the tool. Called + // Update inspectors and data needed to display and interact with the handle. Called // in `preRender()` if the inspections are dirty. - // Derived classes should call this parent method first, then implement custom logic. - virtual void update( ScenePathPtr scenePath, const PlugPtr &editScope ) + void updateHandlePath( ScenePlugPtr scene, const Context *context, const ScenePlug::ScenePath &handlePath ) { - m_handleScenePath = scenePath; - m_editScope = editScope; + m_scene = scene; + m_context = context; + m_handlePath = handlePath; + + m_inspectors.clear(); + + if( !m_scene->exists( m_handlePath ) ) + { + return; + } + + m_editScope = m_view->editScopePlug(); + + /// \todo This can be simplified and some of the logic, especially getting the inspectors, can + /// be moved to the constructor when we standardize on a single USDLux light representation. + + ConstCompoundObjectPtr attributes = m_scene->fullAttributes( m_handlePath ); + + for( const auto &[attributeName, value ] : attributes->members() ) + { + if( + StringAlgo::matchMultiple( attributeName, g_lightAttributePattern ) && + value->typeId() == (IECore::TypeId)ShaderNetworkTypeId + ) + { + const auto shader = attributes->member( attributeName )->outputShader(); + std::string shaderAttribute = shader->getType() + ":" + shader->getName(); + + if( !isLightType( shaderAttribute ) ) + { + continue; + } + + for( const auto &m : m_metaParameters ) + { + if( auto parameter = Metadata::value( shaderAttribute, m ) ) + { + m_inspectors[m] = new ParameterInspector( + m_scene, + m_editScope, + attributeName, + ShaderNetwork::Parameter( "", parameter->readable() ) + ); + } + } + + break; + } + } + + handlePathChanged(); } - const std::string attributePattern() const + /// \todo Should these three be protected, or left out entirely until they are needed by client code? + const ScenePlug *scene() const { - return m_attributePattern; + return m_scene.get(); } - ScenePath *handleScenePath() const + const Context *context() const { - return m_handleScenePath.get(); + return m_context; } - Plug *editScope() const + const ScenePlug::ScenePath &handlePath() const { - return m_editScope.get(); + return m_handlePath; } + /// \todo Remove these and handle the lookThrough logic internally? void setLookThroughLight( bool lookThroughLight ) { m_lookThroughLight = lookThroughLight; @@ -751,490 +877,495 @@ class LightToolHandle : public Handle return m_lookThroughLight; } - // Must be implemented by derived classes to create inspections needed - // by the handle. Called during `preRender()` if the inspections are dirty. - virtual void addDragInspection() = 0; - - virtual void clearDragInspections() = 0; - - virtual bool handleDragMove( const GafferUI::DragDropEvent &event ) = 0; - virtual bool handleDragEnd() = 0; - - // Must be implemented by derived classes to set the local transform of the handle - // relative to the light. The parent of the handle will have rotation and translation - // set independently. `scale` and `shear` are passed here to allow the handle to decide - // how to deal with those transforms. - virtual void updateLocalTransform( const V3f &scale, const V3f &shear ) = 0; - - // Must be implemented by derived classes to return the visible and enabled state for - // the `scenePath` in the current context. - virtual bool visible() const = 0; - virtual bool enabled() const = 0; - - // Must be implemented by derived classes to return all of the inspectors the handle uses. - virtual std::vector inspectors() const = 0; - - private : + // Adds an inspection for all metaParameters for the current context. + void addInspection() + { + InspectionMap inspectionMap; + for( const auto &m : m_metaParameters ) + { + if( Inspector::ResultPtr i = inspection( m ) ) + { + inspectionMap[m] = i; + } + } - ScenePathPtr m_handleScenePath; + m_inspections.push_back( inspectionMap ); + } - const std::string m_attributePattern; + void clearInspections() + { + m_inspections.clear(); + } - Gaffer::PlugPtr m_editScope; + // Called by `LightTool` to handle `dragMove` events. + bool handleDragMove( const GafferUI::DragDropEvent &event ) + { + if( m_inspections.empty() || !allInspectionsEnabled() ) + { + return true; + } - bool m_lookThroughLight; -}; + const bool result = handleDragMoveInternal( event ); + updateTooltipPosition( event.line ); -// ============================================================================ -// SpotLightHandle -// ============================================================================ + return result; + } -class SpotLightHandle : public LightToolHandle -{ + // Called by `LightTool` at the end of a drag event. + bool handleDragEnd() + { + m_dragStartInspection.clear(); - private : + return handleDragEndInternal(); + } - // A struct holding the angle inspections and the original angles during a drag. - // Angles are in "handle-space" (generally 1/2 the full cone for the cone angle - // and the full penumbra angle for penumbras. See `handleAngles` and `plugAngles` - // for conversion details.) - struct DragStartData + // Called by `LightTool` when the transform changes for the scene location the handle + // is attached to. + void updateLocalTransform( const V3f &scale, const V3f &shear ) { - Inspector::ResultPtr coneInspection; - float originalConeHandleAngle; - Inspector::ResultPtr penumbraInspection; - std::optional originalPenumbraHandleAngle; - }; - - public : + updateLocalTransformInternal( scale, shear ); + } - enum class HandleType + // Called by `LightTool` to determine if the handle is enabled. + bool enabled() const { - Cone, - Penumbra - }; + // Return true without checking the `enabled()` state of our inspections. + // This allows the tooltip-on-highlight behavior to show a tooltip explaining + // why an edit is not possible. The alternative is to draw the tooltip for all + // handles regardless of mouse position because a handle can only be in a disabled + // or highlighted drawing state. + // The drawing code takes care of graying out uneditable handles and the inspections + // prevent the value from being changed. + return !m_inspectors.empty(); + } - SpotLightHandle( - const std::string &attributePattern, - HandleType handleType, - const SceneView *view, - const float zRotation, - const std::string &name = "SpotLightHandle" - ) : - LightToolHandle( attributePattern, name ), - m_view( view ), - m_zRotation( zRotation ), - m_handleType( handleType ), - m_angleMultiplier( 1.f ), - m_visualiserScale( 1.f ), - m_frustumScale( 1.f ), - m_lensRadius( 0 ), - m_dragStartData {} + // Called by `LightTool` to determine if the handle is visible. + bool visible() const { + if( m_inspectors.empty() ) + { + return false; + } - mouseMoveSignal().connect( boost::bind( &SpotLightHandle::mouseMove, this, ::_2 ) ); + for( const auto &i : m_inspectors ) + { + if( !i.second->inspect() ) + { + return false; + } + } + return visibleInternal(); } - ~SpotLightHandle() override - { + protected : + + // Protected to reinforce that `LightToolHandle` can not be created directly, only + // derived classes. + LightToolHandle( + const std::string &lightTypePattern, + SceneView *view, + const std::vector &metaParameters, + const std::string &name + ) : + Handle( name ), + m_lightTypePattern( lightTypePattern ), + m_view( view ), + m_metaParameters( metaParameters ), + m_inspectors(), + m_inspections(), + m_dragStartInspection(), + m_tooltipPosition(), + m_lookThroughLight( false ) + { + mouseMoveSignal().connect( boost::bind( &LightToolHandle::mouseMove, this, ::_2 ) ); } - void update( ScenePathPtr scenePath, const PlugPtr &editScope ) override + // Returns true if `shaderAttribute` refers to the same light type + // as this handle was constructed to apply to. + bool isLightType( const std::string &shaderAttribute ) const { - LightToolHandle::update( scenePath, editScope ); + auto lightType = Metadata::value( shaderAttribute, "type" ); - if( !handleScenePath()->isValid() ) + if( !lightType || !StringAlgo::matchMultiple( lightType->readable(), m_lightTypePattern ) ) { - m_coneAngleInspector.reset(); - m_penumbraAngleInspector.reset(); - return; + return false; } - ConstCompoundObjectPtr attributes = handleScenePath()->getScene()->fullAttributes( handleScenePath()->names() ); + return true; + } - float defaultVisualiserScale = 1.f; - if( auto p = m_view->descendant( "drawingMode.visualiser.scale" ) ) + // Returns the inspection stored when the drag event started. Returns `nullptr` + // if not inspection was stored. + Inspector::ResultPtr dragStartInspection( const InternedString &metaParameter ) const + { + auto it = m_dragStartInspection.find( metaParameter ); + if( it != m_dragStartInspection.end() ) { - defaultVisualiserScale = p->getValue(); + return it->second; } - auto visualiserScaleData = attributes->member( g_lightVisualiserScaleAttributeName ); - m_visualiserScale = visualiserScaleData ? visualiserScaleData->readable() : defaultVisualiserScale; + return nullptr; + } - float defaultFrustumScale = 1.f; - if( auto p = m_view->descendant( "drawingMode.light.frustumScale" ) ) - { - defaultFrustumScale = p->getValue(); - } - auto frustumScaleData = attributes->member( g_frustumScaleAttributeName ); - m_frustumScale = frustumScaleData ? frustumScaleData->readable() : defaultFrustumScale; + // Returns the inspection for the scene location the handle is attached to. + // Returns `nullptr` if no inspection exists for the handle. + Inspector::ResultPtr handleInspection( const InternedString &metaParameter ) const + { + ScenePlug::PathScope pathScope( m_context ); + pathScope.setPath( &m_handlePath ); - /// \todo This can be simplified and some of the logic, especially getting the inspectors, can - /// be moved to the constructor when we standardize on a single USDLux light representation. + return inspection( metaParameter ); + } - for( const auto &[attributeName, value] : attributes->members() ) + // Applies an multiplier edit to all of the inspections for `metaParameter`. + void applyMultiplier( const InternedString &metaParameter, const float mult ) + { + for( const auto &i : m_inspections ) { - if( - StringAlgo::match( attributeName, attributePattern() ) && - value->typeId() == (IECore::TypeId)ShaderNetworkTypeId - ) + auto it = i.find( metaParameter ); + if( it == i.end() ) { - const auto shader = attributes->member( attributeName )->outputShader(); - std::string shaderAttribute = shader->getType() + ":" + shader->getName(); - - auto coneParameterName = Metadata::value( shaderAttribute, "coneAngleParameter" ); - if( !coneParameterName ) - { - continue; - } - - m_coneAngleInspector = new ParameterInspector( - handleScenePath()->getScene(), - this->editScope(), - attributeName, - ShaderNetwork::Parameter( "", coneParameterName->readable() ) - ); - - auto penumbraTypeData = Metadata::value( shaderAttribute, "penumbraType" ); - m_penumbraType = penumbraTypeData ? std::optional( InternedString( penumbraTypeData->readable() ) ) : std::nullopt; - - m_penumbraAngleInspector.reset(); - if( auto penumbraParameterName = Metadata::value( shaderAttribute, "penumbraAngleParameter" ) ) - { - m_penumbraAngleInspector = new ParameterInspector( - handleScenePath()->getScene(), - this->editScope(), - attributeName, - ShaderNetwork::Parameter( "", penumbraParameterName->readable() ) - ); - } + continue; + } - m_lensRadius = 0; - if( auto lensRadiusParameterName = Metadata::value( shaderAttribute, "lensRadiusParameter" ) ) - { - if( auto lensRadiusData = shader->parametersData()->member( lensRadiusParameterName->readable() ) ) - { - m_lensRadius = lensRadiusData->readable(); - } - } + ValuePlugPtr parameterPlug = it->second->acquireEdit(); + auto floatPlug = runTimeCast( activeValuePlug( parameterPlug.get() ) ); + if( !floatPlug ) + { + throw Exception( fmt::format( "\"{}\" parameter must use `FloatPlug`", metaParameter.string() ) ); + } - auto angleType = Metadata::value( shaderAttribute, "coneAngleType" ); - if( angleType && angleType->readable() == "half" ) - { - m_angleMultiplier = 2.f; - } - else - { - m_angleMultiplier = 1.f; - } + const float originalValue = it->second->typedValue( 0.f ); + const float nonZeroValue = originalValue == 0 ? 1.f : originalValue; + setValueOrAddKey( floatPlug, m_view->getContext()->getTime(), nonZeroValue * mult ); - break; - } } } - void addDragInspection() override + // Increments the values for all inspections for `metaParameter`, limiting the resulting + // values to minimum and maximum values. + void applyIncrement( const InternedString &metaParameter, const float incr, const float minValue, const float maxValue ) { - Inspector::ResultPtr coneAngleInspection = m_coneAngleInspector->inspect(); - if( !coneAngleInspection ) + for( const auto &i : m_inspections ) { - return; - } - Inspector::ResultPtr penumbraAngleInspection = m_penumbraAngleInspector ? m_penumbraAngleInspector->inspect() : nullptr; + auto it = i.find( metaParameter ); + if( it == i.end() ) + { + continue; + } - ConstFloatDataPtr originalConeAngleData = runTimeCast( coneAngleInspection->value() ); - if( !originalConeAngleData ) - { - return; - } + ValuePlugPtr parameterPlug = it->second->acquireEdit(); + auto floatPlug = runTimeCast( activeValuePlug( parameterPlug.get() ) ); + if( !floatPlug ) + { + throw Exception( fmt::format( "\"{}\" parameter must use `FloatPlug`", metaParameter.string() ) ); + } - ConstFloatDataPtr originalPenumbraAngleData; - if( penumbraAngleInspection ) - { - originalPenumbraAngleData = runTimeCast( penumbraAngleInspection->value() ); - assert( originalPenumbraAngleData ); + const float originalValue = it->second->typedValue( 0.f ); + setValueOrAddKey( + floatPlug, + m_view->getContext()->getTime(), + std::clamp( originalValue + incr, minValue, maxValue ) + ); } + } - const auto &[coneHandleAngle, penumbraHandleAngle] = handleAngles( - originalConeAngleData.get(), - originalPenumbraAngleData ? originalPenumbraAngleData.get() : nullptr - ); + bool hasInspectors() const + { + return !m_inspectors.empty(); + } - m_inspections.push_back( - { - coneAngleInspection, - coneHandleAngle, - penumbraAngleInspection, - penumbraHandleAngle - } - ); + const std::vector &inspections() const + { + return m_inspections; } - void clearDragInspections() override + // Sets the position of the tooltip in gadget space. + void setTooltipPosition( const V3f &p ) { - m_inspections.clear(); + m_tooltipPosition = p; } - bool handleDragMove( const GafferUI::DragDropEvent &event ) override + const V3f getTooltipPosition() const { - if( m_inspections.empty() || !allInspectionsEnabled() ) - { - return true; - } - - float newHandleAngle = 0; - if( getLookThroughLight() ) - { - // When looking through a light, the viewport field of view changes - // with the cone angle. When dragging, taking just the `event` coordinates - // causes a feedback loop where the `event` coordinates as a fraction of - // the viewport cause the viewport to get smaller / larger, which causes the fraction - // to get smaller / larger, quickly going to zero / 180. - // We can avoid the feedback loop by using raster coordinates, which unproject - // the local coordinates to a fixed frame of reference (the screen). - const Line3f dragLine( event.line.p0, event.line.p1 ); - - newHandleAngle = radiansToDegrees( - atan2( rasterDragDistance( dragLine ) + m_rasterXOffset, m_rasterZPosition ) - ); - } - else if( m_drag.value().isLinearDrag() ) - { - // Intersect the gadget-local `event` line with the sphere centered at the gadget - // origin with radius equal to the distance along the handle where the user clicked. - // `Imath::Sphere3::intersect()` returns the closest (if any) intersection, but we - // want the intersection closest to the handle line, so we do the calculation here. - - const Line3f eventLine( event.line.p0, event.line.p1 ); + return m_tooltipPosition; + } - const auto &[coneInspection, coneHandleAngle, penumbraInspection, penumbraHandleAngle] = spotLightHandleAngles(); - const float angle = m_handleType == HandleType::Cone ? coneHandleAngle : penumbraHandleAngle.value(); + const SceneView *view() const + { + return m_view; + } - if( !sphereSpokeClickAngle( eventLine, m_arcRadius, angle, newHandleAngle ) ) - { - return true; - } - } - else + const Inspector *inspector( const InternedString &metaParameter ) const + { + const auto it = m_inspectors.find( metaParameter ); + if( it == m_inspectors.end() ) { - // All other drags can use the `AngularDrag` directly. - newHandleAngle = radiansToDegrees( m_drag.value().updatedRotation( event ) ); + return nullptr; } - // Clamp the handle being dragged, then calculate the angle delta. + return it->second.get(); + } - const float clampedHandleAngle = clampHandleAngle( - newHandleAngle, - m_dragStartData.originalConeHandleAngle, - m_dragStartData.originalPenumbraHandleAngle - ); - const float angleDelta = clampedHandleAngle - ( - m_handleType == HandleType::Cone ? m_dragStartData.originalConeHandleAngle : m_dragStartData.originalPenumbraHandleAngle.value() - ); + // The following protected methods are used by derived classes to implement + // handle-specific behavior. - for( auto &[coneInspection, originalConeHandleAngle, penumbraInspection, originalPenumbraHandleAngle] : m_inspections ) - { - if( m_handleType == HandleType::Cone ) - { - ValuePlugPtr conePlug = coneInspection->acquireEdit(); - auto coneFloatPlug = runTimeCast( activeValuePlug( conePlug.get() ) ); - if( !coneFloatPlug ) - { - throw Exception( "Invalid type for \"coneAngleParameter\"" ); - } + // May be overriden to update internal state when the scene location the handle + // is attached to changes. + virtual void handlePathChanged() + { - // Clamp each individual cone angle as well - setValueOrAddKey( - coneFloatPlug, - m_view->getContext()->getTime(), - conePlugAngle( - clampHandleAngle( - originalConeHandleAngle + angleDelta, - originalConeHandleAngle, - originalPenumbraHandleAngle - ) - ) - ); - } + } - if( m_handleType == HandleType::Penumbra ) - { - ValuePlugPtr penumbraPlug = penumbraInspection->acquireEdit(); - auto penumbraFloatPlug = runTimeCast( activeValuePlug( penumbraPlug.get() ) ); - if( !penumbraFloatPlug ) - { - throw Exception( "Inavlid type for \"penumbraAngleParameter\"" ); - } + // May be overriden to clean up internal state after a drag. + virtual bool handleDragEndInternal() + { + return false; + } - // Clamp each individual cone angle as well - setValueOrAddKey( - penumbraFloatPlug, - m_view->getContext()->getTime(), - penumbraPlugAngle( - clampHandleAngle( - originalPenumbraHandleAngle.value() + angleDelta, - originalConeHandleAngle, - originalPenumbraHandleAngle - ) - ) - ); - } - } + // May be overridden to set the local transform of the handle + // relative to the light. The parent of the handle will have rotation and translation + // set independently. `scale` and `shear` are passed here to allow the handle to decide + // how to deal with those transforms. + /// \todo Should this be something like `setScaleAndShear()` and rework `updateLocalTransform()`? + virtual void updateLocalTransformInternal( const V3f &, const V3f & ) + { - return true; } - bool handleDragEnd() override + // Called by `visible()`, may be overridden to extend the logic determining visibility. + // Called with `scenePath` set in the current context. + virtual bool visibleInternal() const { - m_drag = std::nullopt; + return true; + } - return false; + // Called by `renderHandle()`, may be overridden to return a string suffix to be + // displayed in a tool tip after the plug count in the case of modifying multiple + // unrelated plugs. + virtual std::string tipPlugSuffix() const + { + return ""; } - void updateLocalTransform( const V3f &, const V3f & ) override + // Called by `renderHandle()`, may be overridden to include a string suffix to be + // displayed at the end of the entire tool tip. + virtual std::string tipInfoSuffix() const { - M44f transform; - if( m_handleType == HandleType::Penumbra && ( !m_penumbraType || m_penumbraType == g_insetPenumbraType ) ) - { - // Rotate 180 on the Z-axis to make positive rotations inset - transform *= M44f().rotate( V3f( 0, 0, M_PI ) ); - } + return ""; + } - if( m_handleType == HandleType::Penumbra ) + // May be overridden to return a vector of inspection results that will receive + // edits from this handle. By default, returns all inspections. + // Handles can hold inspections for additional parameters + // than those being edited, but only the inspections returned from `handleValueInspections()` + // will be considered for editing and related UI indications. + virtual std::vector handleValueInspections() const + { + std::vector result; + for( const auto &i : m_inspections ) { - // For inset and outset penumbras, transform the handle so the -Z axis - // points along the cone line, making all angles relative to the cone angle. - const auto &[coneInspection, coneHandleAngle, penumbraInspection, penumbraHandleAngle] = spotLightHandleAngles(); - if( !m_penumbraType || m_penumbraType == g_insetPenumbraType || m_penumbraType == g_outsetPenumbraType ) + for( const auto &p : i ) { - transform *= M44f().rotate( V3f( 0, degreesToRadians( coneHandleAngle ), 0 ) ); + result.push_back( p.second.get() ); } } - transform *= M44f().translate( V3f( -m_lensRadius, 0, 0 ) ); - transform *= M44f().rotate( V3f( 0, 0, degreesToRadians( m_zRotation ) ) ); + return result; + } - setTransform( transform ); + // Must be overriden to set the tooltip position in gadget space based + // on `eventLine` from a `DragDropEvent` or `ButtonEvent`. + virtual void updateTooltipPosition( const LineSegment3f &eventLine ) = 0; + + // Must be overriden to make edits to the inspections in `handleDragMove()`. + virtual bool handleDragMoveInternal( const GafferUI::DragDropEvent &event ) = 0; + + // Must be overridden to add `IECoreGL` components to `rootGroup` in `renderHandle()`. + virtual void addHandleVisualisation( IECoreGL::Group *rootGroup, const bool selectionPass, const bool highlighted ) const = 0; + + // Must be overridden to prepare for the drag in `dragBegin()`. + virtual void setupDrag( const DragDropEvent &event ) = 0; + + private : + + bool mouseMove( const ButtonEvent &event ) + { + updateTooltipPosition( event.line ); + dirty( DirtyType::Render ); + + return false; } - bool visible() const override + void renderHandle( const Style *style, Style::State state ) const override { - if( !m_coneAngleInspector || ( m_handleType == HandleType::Penumbra && !m_penumbraAngleInspector ) ) - { - return false; - } + State::bindBaseState(); + auto glState = const_cast( State::defaultState() ); - // We can be called to check visibility for any scene location set in the current context, spot light - // or otherwise. If there isn't an inspection, this handle should be hidden (likely because the scene - // location is not a spot light). + IECoreGL::GroupPtr group = new IECoreGL::Group; - Inspector::ResultPtr contextConeInspection = m_coneAngleInspector->inspect(); - Inspector::ResultPtr contextPenumbraInspection = m_penumbraAngleInspector ? m_penumbraAngleInspector->inspect() : nullptr; + const bool highlighted = state == Style::State::HighlightedState; + const bool selectionPass = (bool)IECoreGL::Selector::currentSelector(); - if( !contextConeInspection || ( m_handleType == HandleType::Penumbra && !contextPenumbraInspection ) ) - { - return false; - } + group->getState()->add( + new IECoreGL::ShaderStateComponent( + ShaderLoader::defaultShaderLoader(), + TextureLoader::defaultTextureLoader(), + "", + "", + constantFragSource(), + new CompoundObject + ) + ); - // We are a spot light, but the penumbra will be hidden if it's too close to the cone angle, for - // the location we're attaching the handles to. + auto standardStyle = runTimeCast( style ); + assert( standardStyle ); + const Color3f highlightColor3 = standardStyle->getColor( StandardStyle::Color::HighlightColor ); + const Color4f highlightColor4 = Color4f( highlightColor3.x, highlightColor3.y, highlightColor3.z, 1.f ); - /// \todo This checks the penumbra / cone angles only for the last selected location, causing - /// repeated checks of the same location when `visible()` is called in a loop over multiple scene - /// locations. We rely on history caching to make this relatively fast, but ideally this could be - /// tested only once per selection list. + const bool enabled = allInspectionsEnabled(); - const auto &[coneInspection, coneAngle, penumbraInspection, penumbraAngle] = spotLightHandleAngles(); - if( m_handleType == HandleType::Penumbra && penumbraAngle ) + group->getState()->add( + new IECoreGL::Color( + enabled ? ( highlighted ? g_lightToolHighlightColor4 : highlightColor4 ) : g_lightToolDisabledColor4 + ) + ); + + addHandleVisualisation( group.get(), selectionPass, highlighted ); + + group->render( glState ); + + if( highlighted ) { - const float radius = m_visualiserScale * m_frustumScale * -10.f; - const V2f coneRaster = m_view->viewportGadget()->gadgetToRasterSpace( - V3f( 0, 0, radius ), - this - ); - const M44f rot = M44f().rotate( V3f( 0, degreesToRadians( penumbraAngle.value() ), 0 ) ); - const V2f penumbraRaster = m_view->viewportGadget()->gadgetToRasterSpace( - V3f( 0, 0, radius ) * rot, - this + std::vector inspections = handleValueInspections(); + + drawSelectionTips( + m_tooltipPosition, + inspections, + tipPlugSuffix(), + tipInfoSuffix(), + this, + m_view->viewportGadget(), + style ); + } + } - if( ( coneRaster - penumbraRaster ).length() < ( 2.f * g_circleHandleWidthLarge ) ) + void dragBegin( const DragDropEvent &event ) override + { + for( const auto &m : m_metaParameters ) + { + if( Inspector::ResultPtr i = handleInspection( m ) ) { - return false; + m_dragStartInspection[m] = i; } } - return true; + setupDrag( event ); } - bool enabled() const override + // Returns the inspection for `metaParameter` in the current context. + // Returns `nullptr` if no inspector or no inspection exists. + Inspector::ResultPtr inspection( const InternedString &metaParameter ) const { - if( !m_coneAngleInspector ) + auto it = m_inspectors.find( metaParameter ); + if( it == m_inspectors.end() ) { - return false; + return nullptr; } - // Return true without checking the `enabled()` state of our inspections. - // This allows the tooltip-on-highlight behavior to show a tooltip explaining - // why an edit is not possible. The alternative is to draw the tooltip for all - // handles regardless of mouse position because a handle can only be in a disabled - // or highlighted drawing state. - // The drawing code takes care of graying out uneditable handles and the inspections - // prevent the value from being changed. + if( Inspector::ResultPtr inspection = it->second->inspect() ) + { + return inspection; + } - return true; + return nullptr; } - std::vector inspectors() const override + bool allInspectionsEnabled() const { - if( m_handleType == HandleType::Cone ) + std::vector requiredInspections = handleValueInspections(); + if( requiredInspections.empty() ) { - return {m_coneAngleInspector.get()}; + return false; } - if( - ( - !m_penumbraType || - m_penumbraType == g_insetPenumbraType || - m_penumbraType == g_outsetPenumbraType - ) - ) + + bool enabled = true; + for( const auto &i : requiredInspections ) { - return {m_coneAngleInspector.get(), m_penumbraAngleInspector.get()}; + enabled &= i && i->editable(); } - return {m_penumbraAngleInspector.get()}; + + return enabled; } - protected : + ScenePlugPtr m_scene; + const Context *m_context; + ScenePlug::ScenePath m_handlePath; - void renderHandle( const Style *style, Style::State state ) const override + const std::string m_lightTypePattern; + SceneView *m_view; + Gaffer::PlugPtr m_editScope; + const std::vector m_metaParameters; + + using InspectorMap = std::unordered_map; + InspectorMap m_inspectors; + + std::vector m_inspections; + InspectionMap m_dragStartInspection; + V3f m_tooltipPosition; + + bool m_lookThroughLight; +}; + +// ============================================================================ +// SpotLightHandle +// ============================================================================ + +class SpotLightHandle : public LightToolHandle +{ + public : + + enum class HandleType { - State::bindBaseState(); - auto glState = const_cast( State::defaultState() ); + Cone, + Penumbra + }; - IECoreGL::GroupPtr group = new IECoreGL::Group; + SpotLightHandle( + const std::string &lightType, + HandleType handleType, + SceneView *view, + const float zRotation, + const std::string &name + ) : + LightToolHandle( lightType, view, { g_coneAngleParameter, g_penumbraAngleParameter }, name ), + m_zRotation( zRotation ), + m_handleType( handleType ), + m_angleHandleRatio( 2.f ), + m_visualiserScale( 1.f ), + m_frustumScale( 1.f ), + m_lensRadius( 0 ) + { + } + ~SpotLightHandle() override + { - const bool highlighted = state == Style::State::HighlightedState; + } + + protected : + void addHandleVisualisation( IECoreGL::Group *rootGroup, const bool selectionPass, const bool highlighted ) const override + { // Line along cone. Use a cylinder because GL_LINE with width > 1 // are not reliably selected. GroupPtr spokeGroup = new Group; - spokeGroup->getState()->add( - new IECoreGL::ShaderStateComponent( - ShaderLoader::defaultShaderLoader(), - TextureLoader::defaultTextureLoader(), - "", - "", - constantFragSource(), - new CompoundObject - ) - ); - float spokeRadius = 0; float handleRadius = 0; - if( IECoreGL::Selector::currentSelector() ) + if( selectionPass ) { spokeRadius = g_lineSelectionWidth; handleRadius = g_circleHandleSelectionWidth; @@ -1250,8 +1381,11 @@ class SpotLightHandle : public LightToolHandle handleRadius = highlighted ? g_circleHandleWidthLarge : g_circleHandleWidth; } + spokeRadius *= g_spotLightHandleSizeMultiplier; + handleRadius *= g_spotLightHandleSizeMultiplier; + const V3f farP = V3f( 0, 0, m_frustumScale * m_visualiserScale * -10.f ); - const auto &[coneInspection, coneHandleAngle, penumbraInspection, penumbraHandleAngle] = spotLightHandleAngles(); + const auto &[coneHandleAngle, penumbraHandleAngle] = handleAngles(); const float angle = m_handleType == HandleType::Cone ? coneHandleAngle : penumbraHandleAngle.value(); const M44f handleTransform = M44f().rotate( V3f( 0, degreesToRadians( angle ), 0 ) ); @@ -1264,20 +1398,7 @@ class SpotLightHandle : public LightToolHandle ) ); - auto standardStyle = runTimeCast( style ); - assert( standardStyle ); - const Color3f highlightColor3 = standardStyle->getColor( StandardStyle::Color::HighlightColor ); - const Color4f highlightColor4 = Color4f( highlightColor3.x, highlightColor3.y, highlightColor3.z, 1.f ); - - const bool enabled = allInspectionsEnabled(); - - spokeGroup->getState()->add( - new IECoreGL::Color( - enabled ? ( highlighted ? g_lightToolHighlightColor4 : highlightColor4 ) : g_lightToolDisabledColor4 - ) - ); - - group->addChild( spokeGroup ); + rootGroup->addChild( spokeGroup ); // Circles at end of cone and frustum @@ -1296,7 +1417,7 @@ class SpotLightHandle : public LightToolHandle IECoreGL::MeshPrimitivePtr decoration; if( - ( m_handleType == HandleType::Cone && m_penumbraAngleInspector && ( !m_penumbraType || m_penumbraType == g_insetPenumbraType ) ) || + ( m_handleType == HandleType::Cone && inspector( g_penumbraAngleParameter ) && ( !m_penumbraType || m_penumbraType == g_insetPenumbraType ) ) || ( m_handleType == HandleType::Penumbra && ( m_penumbraType == g_outsetPenumbraType || m_penumbraType == g_absolutePenumbraType ) ) ) { @@ -1327,23 +1448,21 @@ class SpotLightHandle : public LightToolHandle ); iconGroup->addChild( farIconGroup ); - iconGroup->getState()->add( - new IECoreGL::Color( - enabled ? ( highlighted ? g_lightToolHighlightColor4 : highlightColor4 ) : g_lightToolDisabledColor4 - ) - ); - - group->addChild( iconGroup ); + rootGroup->addChild( iconGroup ); // Drag arcs if( m_drag && !getLookThroughLight() ) { const float currentFraction = angle / 360.f; - const float previousFraction = !m_inspections.empty() ? + + Inspector::ResultPtr coneInspection = dragStartInspection( g_coneAngleParameter ); + Inspector::ResultPtr penumbraInspection = dragStartInspection( g_penumbraAngleParameter ); + + const float previousFraction = !inspections().empty() ? ( m_handleType == HandleType::Cone ? - m_dragStartData.originalConeHandleAngle : - m_dragStartData.originalPenumbraHandleAngle.value() + this->coneHandleAngle( coneInspection->typedValue( 0.f ) ) : + this->penumbraHandleAngle( penumbraInspection->typedValue( 0.f ) ) ) / 360.f : currentFraction; IECoreScene::MeshPrimitivePtr previousSolidArc = nullptr; @@ -1391,73 +1510,296 @@ class SpotLightHandle : public LightToolHandle solidAngleGroup->addChild( runTimeCast( meshConverter->convert() ) ); } - group->addChild( solidAngleGroup ); + rootGroup->addChild( solidAngleGroup ); + } + + rootGroup->setTransform( handleTransform ); + } + + std::vector handleValueInspections() + { + std::vector result; + for( const auto &i : inspections() ) + { + for( const auto &p: i ) + { + if( p.first == ( m_handleType == HandleType::Cone ? g_coneAngleParameter : g_penumbraAngleParameter ) ) + { + result.push_back( p.second.get() ); + } + } } - group->setTransform( handleTransform ); + return result; + } - group->render( glState ); + std::string tipPlugSuffix() const override + { + return m_handleType == HandleType::Cone ? "cone angles" : "penumbra angles"; + } - // Selection info + void handlePathChanged() override + { + ConstCompoundObjectPtr attributes = scene()->fullAttributes( handlePath() ); - if( highlighted ) + float defaultVisualiserScale = 1.f; + if( auto p = view()->descendant( "drawingMode.visualiser.scale" ) ) + { + defaultVisualiserScale = p->getValue(); + } + auto visualiserScaleData = attributes->member( g_lightVisualiserScaleAttributeName ); + m_visualiserScale = visualiserScaleData ? visualiserScaleData->readable() : defaultVisualiserScale; + + float defaultFrustumScale = 1.f; + if( auto p = view()->descendant( "drawingMode.light.frustumScale" ) ) + { + defaultFrustumScale = p->getValue(); + } + auto frustumScaleData = attributes->member( g_frustumScaleAttributeName ); + m_frustumScale = frustumScaleData ? frustumScaleData->readable() : defaultFrustumScale; + + /// \todo This can be simplified and some of the logic, especially getting the inspectors, can + /// be moved to the constructor when we standardize on a single USDLux light representation. + + for( const auto &[attributeName, value] : attributes->members() ) { - std::vector inspections; - for( const auto &inspectionPair : m_inspections ) + if( + StringAlgo::matchMultiple( attributeName, g_lightAttributePattern ) && + value->typeId() == (IECore::TypeId)ShaderNetworkTypeId + ) { - inspections.push_back( - m_handleType == HandleType::Cone ? inspectionPair.coneInspection.get() : - inspectionPair.penumbraInspection.get() - ); + const auto shader = attributes->member( attributeName )->outputShader(); + std::string shaderAttribute = shader->getType() + ":" + shader->getName(); + + if( !isLightType( shaderAttribute ) ) + { + continue; + } + + auto penumbraTypeData = Metadata::value( shaderAttribute, "penumbraType" ); + m_penumbraType = penumbraTypeData ? std::optional( InternedString( penumbraTypeData->readable() ) ) : std::nullopt; + + m_lensRadius = 0; + if( auto lensRadiusParameterName = Metadata::value( shaderAttribute, "lensRadiusParameter" ) ) + { + if( auto lensRadiusData = shader->parametersData()->member( lensRadiusParameterName->readable() ) ) + { + m_lensRadius = lensRadiusData->readable(); + } + } + + auto angleType = Metadata::value( shaderAttribute, "coneAngleType" ); + if( angleType && angleType->readable() == "half" ) + { + m_angleHandleRatio = 1.f; + } + else + { + m_angleHandleRatio = 2.f; + } + + break; } + } + } - drawSelectionTips( - V3f( 0, 0, !getLookThroughLight() ? -m_arcRadius : 1.f ) * handleTransform, - inspections, - fmt::format( "{} angles", m_handleType == HandleType::Cone ? "cone" : "penumbra" ), - "", // infoSuffix - this, - m_view->viewportGadget(), - style + bool handleDragMoveInternal( const GafferUI::DragDropEvent &event ) override + { + float newHandleAngle = 0; + if( getLookThroughLight() ) + { + // When looking through a light, the viewport field of view changes + // with the cone angle. When dragging, taking just the `event` coordinates + // causes a feedback loop where the `event` coordinates as a fraction of + // the viewport cause the viewport to get smaller / larger, which causes the fraction + // to get smaller / larger, quickly going to zero / 180. + // We can avoid the feedback loop by using raster coordinates, which unproject + // the local coordinates to a fixed frame of reference (the screen). + const Line3f dragLine( event.line.p0, event.line.p1 ); + + newHandleAngle = radiansToDegrees( + atan2( rasterDragDistance( dragLine ) + m_rasterXOffset, m_rasterZPosition ) ); } - } + else if( m_drag.value().isLinearDrag() ) + { + // Intersect the gadget-local `event` line with the sphere centered at the gadget + // origin with radius equal to the distance along the handle where the user clicked. + // `Imath::Sphere3::intersect()` returns the closest (if any) intersection, but we + // want the intersection closest to the handle line, so we do the calculation here. - private : + const Line3f eventLine( event.line.p0, event.line.p1 ); - bool mouseMove( const ButtonEvent &event ) - { - if( m_drag || !m_coneAngleInspector || handleScenePath()->isEmpty() ) + const auto &[coneHandleAngle, penumbraHandleAngle] = handleAngles(); + const float angle = m_handleType == HandleType::Cone ? coneHandleAngle : penumbraHandleAngle.value(); + + if( !sphereSpokeClickAngle( eventLine, m_arcRadius, angle, newHandleAngle ) ) + { + return true; + } + } + else { - return false; + // All other drags can use the `AngularDrag` directly. + newHandleAngle = radiansToDegrees( m_drag.value().updatedRotation( event ) ); } - const auto &[coneInspection, coneHandleAngle, penumbraInspection, penumbraHandleAngle] = spotLightHandleAngles(); + // Clamp the handle being dragged, then calculate the angle delta. - const float angle = m_handleType == HandleType::Cone ? coneHandleAngle : penumbraHandleAngle.value(); + Inspector::ResultPtr coneDragStartInspection = dragStartInspection( g_coneAngleParameter ); + Inspector::ResultPtr penumbraDragStartInspection = dragStartInspection( g_penumbraAngleParameter ); - const M44f r = M44f().rotate( V3f( 0, degreesToRadians( angle ), 0 ) ); - const Line3f rayLine( - V3f( 0 ), - V3f( 0, 0, m_visualiserScale * m_frustumScale * -10.f ) * r + const float clampedPlugAngle = clampPlugAngle( + m_handleType == HandleType::Cone ? conePlugAngle( newHandleAngle ) : penumbraPlugAngle( newHandleAngle ), + coneDragStartInspection->typedValue( 0.f ), + penumbraDragStartInspection ? penumbraDragStartInspection->typedValue( 0.f ) : 0.f ); - const V3f dragPoint = rayLine.closestPointTo( Line3f( event.line.p0, event.line.p1 ) ); - m_arcRadius = dragPoint.length(); + const float angleDelta = + clampedPlugAngle - + ( + m_handleType == HandleType::Cone ? + coneDragStartInspection->typedValue( 0.f ) : + penumbraDragStartInspection->typedValue( 0.f ) + ) + ; - dirty( DirtyType::Render ); + for( const auto &i : inspections() ) + { + auto coneIt = i.find( g_coneAngleParameter ); + if( coneIt == i.end() ) + { + continue; + } + + auto penumbraIt = i.find( g_penumbraAngleParameter ); + if( penumbraIt == i.end() && m_handleType == HandleType::Penumbra ) + { + continue; + } + + float penumbraHandleAngle = 0; + if( penumbraIt != i.end() ) + { + penumbraHandleAngle = penumbraIt->second->typedValue( 0.f ); + } + + auto it = m_handleType == HandleType::Cone ? coneIt : penumbraIt; + + ValuePlugPtr plug = it->second->acquireEdit(); + auto floatPlug = runTimeCast( activeValuePlug( plug.get() ) ); + if( !floatPlug ) + { + throw Exception( + fmt::format( + "Invalid type for \"{}\"", + m_handleType == HandleType::Cone ? g_coneAngleParameter.string() : g_penumbraAngleParameter.string() + ) + ); + } + + // Clamp each individual cone angle as well + setValueOrAddKey( + floatPlug, + view()->getContext()->getTime(), + clampPlugAngle( + it->second->typedValue( 0.f ) + angleDelta, + coneIt->second->typedValue( 0.f ), + penumbraHandleAngle + ) + ); + + } + + return true; + } + + bool handleDragEndInternal() override + { + m_drag = std::nullopt; return false; } - void dragBegin( const DragDropEvent &event ) override + void updateLocalTransformInternal( const V3f &, const V3f & ) override + { + M44f transform; + if( m_handleType == HandleType::Penumbra && ( !m_penumbraType || m_penumbraType == g_insetPenumbraType ) ) + { + // Rotate 180 on the Z-axis to make positive rotations inset + transform *= M44f().rotate( V3f( 0, 0, M_PI ) ); + } + + if( m_handleType == HandleType::Penumbra ) + { + // For inset and outset penumbras, transform the handle so the -Z axis + // points along the cone line, making all angles relative to the cone angle. + const auto &[coneHandleAngle, penumbraHandleAngle] = handleAngles(); + if( !m_penumbraType || m_penumbraType == g_insetPenumbraType || m_penumbraType == g_outsetPenumbraType ) + { + transform *= M44f().rotate( V3f( 0, degreesToRadians( coneHandleAngle ), 0 ) ); + } + } + + transform *= M44f().translate( V3f( -m_lensRadius, 0, 0 ) ); + transform *= M44f().rotate( V3f( 0, 0, degreesToRadians( m_zRotation ) ) ); + + setTransform( transform ); + } + + bool visibleInternal() const override { - const auto &[ coneInspection, coneHandleAngle, penumbraInspection, penumbraHandleAngle] = spotLightHandleAngles(); + const Inspector *coneInspector = inspector( g_coneAngleParameter ); + const Inspector *penumbraInspector = inspector( g_penumbraAngleParameter ); + if( !coneInspector || ( m_handleType == HandleType::Penumbra && !penumbraInspector ) ) + { + return false; + } - m_dragStartData.coneInspection = coneInspection; - m_dragStartData.originalConeHandleAngle = coneHandleAngle; - m_dragStartData.penumbraInspection = penumbraInspection; - m_dragStartData.originalPenumbraHandleAngle = penumbraHandleAngle; + // We can be called to check visibility for any scene location set in the current context, spot light + // or otherwise. If there isn't an inspection, this handle should be hidden (likely because the scene + // location is not a spot light). + Inspector::ResultPtr contextConeInspection = coneInspector->inspect(); + Inspector::ResultPtr contextPenumbraInspection = penumbraInspector ? penumbraInspector->inspect() : nullptr; + + if( !contextConeInspection || ( m_handleType == HandleType::Penumbra && !contextPenumbraInspection ) ) + { + return false; + } + + // We are a spot light, but the penumbra will be hidden if it's too close to the cone angle, for + // the location we're attaching the handles to. + + /// \todo This checks the penumbra / cone angles only for the last selected location, causing + /// repeated checks of the same location when `visible()` is called in a loop over multiple scene + /// locations. We rely on history caching to make this relatively fast, but ideally this could be + /// tested only once per selection list. + + const auto &[coneAngle, penumbraAngle] = handleAngles(); + if( m_handleType == HandleType::Penumbra && penumbraAngle ) + { + const float radius = m_visualiserScale * m_frustumScale * -10.f; + const V2f coneRaster = view()->viewportGadget()->gadgetToRasterSpace( + V3f( 0, 0, radius ), + this + ); + const M44f rot = M44f().rotate( V3f( 0, degreesToRadians( penumbraAngle.value() ), 0 ) ); + const V2f penumbraRaster = view()->viewportGadget()->gadgetToRasterSpace( + V3f( 0, 0, radius ) * rot, + this + ); + + if( ( coneRaster - penumbraRaster ).length() < ( 2.f * g_circleHandleWidthLarge ) ) + { + return false; + } + } + + return true; + } + + void setupDrag( const DragDropEvent &event ) override + { m_drag = AngularDrag( this, V3f( 0, 0, 0 ), @@ -1468,6 +1810,8 @@ class SpotLightHandle : public LightToolHandle if( getLookThroughLight() ) { + const auto &[ coneHandleAngle, penumbraHandleAngle] = handleAngles(); + const float dragStartAngle = m_handleType == HandleType::Cone ? coneHandleAngle : penumbraHandleAngle.value(); const Line3f clickLine( event.line.p0, event.line.p1 ); @@ -1488,58 +1832,96 @@ class SpotLightHandle : public LightToolHandle } } - DragStartData spotLightHandleAngles() const + std::vector handleValueInspections() const override { - ScenePlug::PathScope pathScope( handleScenePath()->getContext() ); - pathScope.setPath( &handleScenePath()->names() ); + std::vector result; + for( const auto &i : inspections() ) + { + for( const auto &p : i ) + { + if( m_handleType == HandleType::Cone && p.first == g_coneAngleParameter ) + { + result.push_back( p.second.get() ); + } + else if( m_handleType == HandleType::Penumbra && p.first == g_penumbraAngleParameter ) + { + result.push_back( p.second.get() ); + } + } + } + return result; + } - Inspector::ResultPtr coneInspection = m_coneAngleInspector->inspect(); - if( !coneInspection ) + void updateTooltipPosition( const LineSegment3f &eventLine ) override + { + if( !hasInspectors() ) { - return {nullptr, 0, nullptr, std::nullopt}; + return; } - const FloatData *coneAngleData = runTimeCast( coneInspection->value() ); - if( !coneAngleData ) + const auto &[coneHandleAngle, penumbraHandleAngle] = handleAngles(); + + const float angle = m_handleType == HandleType::Cone ? coneHandleAngle : penumbraHandleAngle.value(); + + const M44f r = M44f().rotate( V3f( 0, degreesToRadians( angle ), 0 ) ); + + if( getLookThroughLight() ) { - return {nullptr, 0, nullptr, std::nullopt}; + setTooltipPosition( V3f( 0, 0, -1.f ) * r ); + return; } - const FloatData *penumbraAngleData = nullptr; + if( m_drag ) + { + setTooltipPosition( V3f( 0, 0, -m_arcRadius ) * r ); + return; + } + + const Line3f rayLine( + V3f( 0 ), + V3f( 0, 0, m_visualiserScale * m_frustumScale * -10.f ) * r + ); + const V3f dragPoint = rayLine.closestPointTo( Line3f( eventLine.p0, eventLine.p1 ) ); + + setTooltipPosition( dragPoint ); - Inspector::ResultPtr penumbraInspection = m_penumbraAngleInspector ? m_penumbraAngleInspector->inspect() : nullptr; - if( penumbraInspection ) + if( !m_drag ) { - penumbraAngleData = runTimeCast( penumbraInspection->value() ); - assert( penumbraAngleData ); + m_arcRadius = dragPoint.length(); } + } + + private : - const auto &[coneAngle, penumbraAngle] = handleAngles( coneAngleData, penumbraAngleData ); + std::pair> handleAngles() const + { + Inspector::ResultPtr coneHandleInspection = handleInspection( g_coneAngleParameter ); + Inspector::ResultPtr penumbraHandleInspection = handleInspection( g_penumbraAngleParameter ); - return {coneInspection, coneAngle, penumbraInspection, penumbraAngle}; + return { + coneHandleAngle( coneHandleInspection->typedValue( 0.f ) ), + penumbraHandleInspection ? std::optional( penumbraHandleAngle( penumbraHandleInspection->typedValue( 0.f ) ) ) : std::nullopt + }; } // Convert from the angle representation used by plugs to that used by handles. - std::pair> handleAngles( const FloatData *coneAngleData, const FloatData *penumbraAngleData ) const + float coneHandleAngle( const float angle ) const + { + return angle / m_angleHandleRatio; + } + + float penumbraHandleAngle( const float angle ) const { - std::optional penumbraAngle = std::nullopt; - if( penumbraAngleData ) + if( m_penumbraType != g_absolutePenumbraType ) { - if( m_penumbraType != g_absolutePenumbraType ) - { - penumbraAngle = penumbraAngleData->readable(); - } - else - { - penumbraAngle = penumbraAngleData->readable() * 0.5f; - } + return angle; } - return {coneAngleData->readable() * 0.5f * m_angleMultiplier, penumbraAngle}; + return angle * 0.5f; } float conePlugAngle(const float a ) const { - return a * 2.f / m_angleMultiplier; + return a * m_angleHandleRatio; } float penumbraPlugAngle(const float a ) const @@ -1555,9 +1937,9 @@ class SpotLightHandle : public LightToolHandle V3f sphereIntersection; Sphere3f( V3f( 0 ), 1.f ).intersect( ray, sphereIntersection ); - const V2f gadgetRasterOrigin = m_view->viewportGadget()->gadgetToRasterSpace( V3f( 0, 0, -1.f ), this ); - const V2f rasterSphereIntersection = m_view->viewportGadget()->gadgetToRasterSpace( sphereIntersection, this ); - const V2f rasterNormal = ( m_view->viewportGadget()->gadgetToRasterSpace( V3f( 0, 1.f, -1.f ), this ) - gadgetRasterOrigin ).normalized(); + const V2f gadgetRasterOrigin = view()->viewportGadget()->gadgetToRasterSpace( V3f( 0, 0, -1.f ), this ); + const V2f rasterSphereIntersection = view()->viewportGadget()->gadgetToRasterSpace( sphereIntersection, this ); + const V2f rasterNormal = ( view()->viewportGadget()->gadgetToRasterSpace( V3f( 0, 1.f, -1.f ), this ) - gadgetRasterOrigin ).normalized(); const V2f projectedPoint = rasterSphereIntersection - (rasterSphereIntersection - gadgetRasterOrigin).dot( rasterNormal ) * rasterNormal; @@ -1573,22 +1955,22 @@ class SpotLightHandle : public LightToolHandle return rasterNormal.x > 0 ? rasterDistance.y : -rasterDistance.y; } - float clampHandleAngle( + float clampPlugAngle( const float angle, const float originalConeAngle, const std::optional originalPenumbraAngle ) { - float result = std::clamp( angle, 0.f, 90.f ); + float result = std::clamp( angle, 0.f, 180.f ); if( m_handleType == HandleType::Cone ) { if( originalPenumbraAngle && ( !m_penumbraType || m_penumbraType == g_insetPenumbraType ) ) { - result = std::max( result, originalPenumbraAngle.value() ); + result = std::max( result, originalPenumbraAngle.value() * 2.f ); } else if( m_penumbraType == g_outsetPenumbraType ) { - result = std::min( result, 90.f - originalPenumbraAngle.value() ); + result = std::min( result, 180.f - originalPenumbraAngle.value() * 2.f ); } } @@ -1596,55 +1978,29 @@ class SpotLightHandle : public LightToolHandle { if( !m_penumbraType || m_penumbraType == g_insetPenumbraType ) { - result = std::min( result, originalConeAngle ); + result = std::min( result, originalConeAngle * 0.5f ); } else if( m_penumbraType == g_outsetPenumbraType ) { - result = std::min( result, 90.f - originalConeAngle ); + result = std::min( result, ( 180.f - originalConeAngle ) * 0.5f ); } } return result; } - bool allInspectionsEnabled() const - { - bool enabled = true; - for( auto &[coneInspection, originalConeAngle, penumbraInspection, originalPenumbraAngle] : m_inspections ) - { - if( m_handleType == HandleType::Cone ) - { - enabled &= coneInspection ? coneInspection->editable() : false; - } - else - { - enabled &= penumbraInspection ? penumbraInspection->editable() : false; - } - } - - return enabled; - } - - ParameterInspectorPtr m_coneAngleInspector; - ParameterInspectorPtr m_penumbraAngleInspector; - - const SceneView *m_view; - const float m_zRotation; - std::vector m_inspections; - std::optional m_drag; HandleType m_handleType; std::optional m_penumbraType; - float m_angleMultiplier; + float m_angleHandleRatio; float m_visualiserScale; float m_frustumScale; float m_lensRadius; - DragStartData m_dragStartData; // The reference coordinates of the start of a drag // when looking through a light. `x` is the x distance, in raster // space, on the plane of the gadget. `y` is the depth, into the @@ -1655,159 +2011,377 @@ class SpotLightHandle : public LightToolHandle float m_arcRadius; }; -// ============================================================================ -// QuadLightHandle -// ============================================================================ - -class QuadLightHandle : public LightToolHandle +class EdgeHandle : public LightToolHandle { public : - - enum HandleType + enum class LightAxis { Width = 1, Height = 2 }; - QuadLightHandle( - const std::string &attributePattern, - unsigned handleType, - const SceneView *view, - const float xSign, - const float ySign, - const std::string &name = "QuadLightHandle" + EdgeHandle( + const std::string &lightType, + SceneView *view, + const InternedString &edgeParameter, + const V3f &edgeAxis, + const float edgeToHandleRatio, + const InternedString &oppositeParameter, + const V3f &oppositeAxis, + const float oppositeToHandleRatio, + const InternedString &oppositeScaleAttributeName, + const float edgeMargin, + const std::string &tipPlugSuffix, + const std::string &name ) : - LightToolHandle( attributePattern, name ), - m_view( view ), - m_handleType( handleType ), - m_dragStartInfo(), - m_xSign( xSign ), - m_ySign( ySign ), - m_edgeCursorPoint( V3f( 0 ) ), - m_scale( V2f( 1.f ) ) - { - mouseMoveSignal().connect( boost::bind( &QuadLightHandle::mouseMove, this, ::_2 ) ); - } + LightToolHandle( lightType, view, {edgeParameter, oppositeParameter}, name ), + m_edgeParameter( edgeParameter ), + m_edgeAxis( edgeAxis ), + m_edgeToHandleRatio( edgeToHandleRatio ), + m_oppositeParameter( oppositeParameter ), + m_oppositeAxis( oppositeAxis ), + m_oppositeToHandleRatio( oppositeToHandleRatio ), + m_oppositeScaleAttributeName( oppositeScaleAttributeName ), + m_edgeMargin( edgeMargin ), + m_tipPlugSuffix( tipPlugSuffix ), - ~QuadLightHandle() override + m_edgeScale( 1.f ), + m_oppositeScale( 1.f ), + m_orientation(), + m_oppositeAdditionalScale( 1.f ), + m_tooltipT( 0 ) { - } - void update( ScenePathPtr scenePath, const PlugPtr &editScope ) override + ~EdgeHandle() override { - LightToolHandle::update( scenePath, editScope ); - m_widthInspector.reset(); - m_heightInspector.reset(); + } - if( !handleScenePath()->isValid() ) - { - return; - } + protected : + void handlePathChanged() override + { /// \todo This can be simplified and some of the logic, especially getting the inspectors, can /// be moved to the constructor when we standardize on a single USDLux light representation. - ConstCompoundObjectPtr attributes = handleScenePath()->getScene()->fullAttributes( handleScenePath()->names() ); + ConstCompoundObjectPtr attributes = scene()->fullAttributes( handlePath() ); - for( const auto &[attributeName, value ] : attributes->members() ) + for( const auto &[attributeName, value] : attributes->members() ) { if( - StringAlgo::match( attributeName, attributePattern() ) && + StringAlgo::matchMultiple( attributeName, g_lightAttributePattern ) && value->typeId() == (IECore::TypeId)ShaderNetworkTypeId ) { const auto shader = attributes->member( attributeName )->outputShader(); std::string shaderAttribute = shader->getType() + ":" + shader->getName(); - auto widthParameterName = Metadata::value( shaderAttribute, "widthParameter" ); - auto heightParameterName = Metadata::value( shaderAttribute, "heightParameter" ); - if( !widthParameterName || !heightParameterName ) + if( !isLightType( shaderAttribute ) ) { continue; } - m_widthInspector = new ParameterInspector( - handleScenePath()->getScene(), - this->editScope(), - attributeName, - ShaderNetwork::Parameter( "", widthParameterName->readable() ) - ); - m_heightInspector = new ParameterInspector( - handleScenePath()->getScene(), - this->editScope(), - attributeName, - ShaderNetwork::Parameter( "", heightParameterName->readable() ) - ); + m_orientation = M44f(); + if( auto orientationData = Metadata::value( shaderAttribute, "visualiserOrientation" ) ) + { + m_orientation = orientationData->readable(); + } + + m_oppositeAdditionalScale = 1.f; + if( auto scaleData = Metadata::value( shaderAttribute, m_oppositeScaleAttributeName ) ) + { + m_oppositeAdditionalScale = scaleData->readable(); + } break; } } } - void addDragInspection() override + bool handleDragMoveInternal( const GafferUI::DragDropEvent &event ) override { - InspectionInfo i = inspectionInfo(); - const auto &[widthInspection, originalWidth, heightInspection, originalHeight] = i; - if( !widthInspection || !heightInspection ) + Inspector::ResultPtr edgeInspection = dragStartInspection( m_edgeParameter ); + if( !edgeInspection ) + { + return true; + } + + const float nonZeroValue = edgeInspection->typedValue( 0.f ) == 0 ? 1.f : edgeInspection->typedValue( 0.f ); + const float newValue = m_drag.value().updatedPosition( event ) - m_drag.value().startPosition(); + + float mult = std::max( ( newValue * m_edgeToHandleRatio ) / ( nonZeroValue * m_edgeScale ) + 1.f, 0.f ); + + applyMultiplier( m_edgeParameter, mult ); + + return true; + } + + void updateLocalTransformInternal( const V3f &scale, const V3f & ) override + { + m_edgeScale = abs( scale.dot( m_edgeAxis * m_orientation ) ); + m_oppositeScale = abs( scale.dot( m_oppositeAxis * m_orientation ) ) * m_oppositeAdditionalScale; + } + + bool handleDragEndInternal() override + { + m_drag = std::nullopt; + return false; + } + + void addHandleVisualisation( IECoreGL::Group *rootGroup, const bool selectionPass, const bool highlighted ) const override + { + if( getLookThroughLight() ) + { + return; + } + + Inspector::ResultPtr edgeInspection = handleInspection( m_edgeParameter ); + if( !edgeInspection ) { return; } - m_inspections.push_back( i ); + float spokeRadius = 0; + float coneSize = 0; + + if( selectionPass ) + { + spokeRadius = g_lineSelectionWidth; + coneSize = g_arrowHandleSelectionSize; + } + else + { + spokeRadius = highlighted ? g_lineHandleWidthLarge : g_lineHandleWidth; + coneSize = highlighted ? g_arrowHandleSizeLarge : g_arrowHandleSize; + } + + LineSegment3f edgeSegment = this->edgeSegment( + edgeInspection->typedValue( 0.f ), + oppositeInspectionValue() + ); + + M44f edgeTransform; + this->edgeTransform( edgeInspection->typedValue( 0.f ), edgeSegment, edgeTransform ); + M44f coneTransform; + this->coneTransform( edgeInspection->typedValue( 0.f ), coneTransform ); + + IECoreGL::GroupPtr coneGroup = new IECoreGL::Group; + coneGroup->setTransform( M44f().scale( V3f( coneSize ) ) * coneTransform ); + coneGroup->addChild( unitCone() ); + rootGroup->addChild( coneGroup ); + + IECoreGL::GroupPtr edgeGroup = new IECoreGL::Group; + edgeGroup->addChild( + cone( + edgeSegment.length(), + spokeRadius * ::rasterScaleFactor( this, edgeSegment.p0 ), + spokeRadius * ::rasterScaleFactor( this, edgeSegment.p1 ) + ) + ); + edgeGroup->setTransform( edgeTransform ); + + rootGroup->addChild( edgeGroup ); + rootGroup->setTransform( m_orientation ); } - void clearDragInspections() override + void setupDrag( const DragDropEvent &event ) override { - m_inspections.clear(); + m_drag = LinearDrag( this, LineSegment3f( V3f( 0 ), m_edgeAxis * m_orientation ), event ); } - bool handleDragMove( const GafferUI::DragDropEvent &event ) override + std::vector handleValueInspections() const override { - if( m_inspections.empty() || !allInspectionsEnabled() ) + std::vector result; + for( const auto &i : inspections() ) { - return true; + for( const auto &p: i ) + { + if( p.first == m_edgeParameter ) + { + result.push_back( p.second.get() ); + } + } } - float xMult = 1.f; - float yMult = 1.f; + return result; + } + + std::string tipPlugSuffix() const override + { + return m_tipPlugSuffix; + } - float nonZeroWidth = m_dragStartInfo.originalWidth == 0 ? 1.f : m_dragStartInfo.originalWidth; - float nonZeroHeight = m_dragStartInfo.originalHeight == 0 ? 1.f : m_dragStartInfo.originalHeight; + std::string tipInfoSuffix() const override + { + return ""; + } - if( m_handleType & HandleType::Width && m_handleType & HandleType::Height ) + void updateTooltipPosition( const LineSegment3f &eventLine ) override + { + if( !hasInspectors() ) { - auto &drag = std::get( m_drag ); - V2f newPosition = drag.updatedPosition( event ) - drag.startPosition(); - xMult = ( newPosition.x * 2.f ) / ( nonZeroWidth * m_scale.x ) + 1.f; - yMult = ( newPosition.y * 2.f ) / ( nonZeroHeight * m_scale.y ) + 1.f; + return; } - else if( m_handleType & HandleType::Width ) + + Inspector::ResultPtr edgeInspection = handleInspection( m_edgeParameter ); + if( !edgeInspection ) { - auto &drag = std::get( m_drag ); - float newPosition = drag.updatedPosition( event ) - drag.startPosition(); - xMult = ( newPosition * 2.f ) / ( nonZeroWidth * m_scale.x ) + 1.f; + return; } - else if( m_handleType &HandleType::Height ) + + LineSegment3f edgeSegment = this->edgeSegment( edgeInspection->typedValue( 0.f ), oppositeInspectionValue() ); + V3f offset = edgeToGadgetSpace( edgeInspection->typedValue( 0.f ) ); + edgeSegment.p0 += offset; + edgeSegment.p1 += offset; + edgeSegment *= m_orientation; + + if( !m_drag ) { - auto &drag = std::get( m_drag ); - float newPosition = drag.updatedPosition( event ) - drag.startPosition(); - yMult = ( newPosition * 2.f ) / ( nonZeroHeight * m_scale.y ) + 1.f; + V3f eventClosest; + const V3f closestPoint = edgeSegment.closestPoints( LineSegment3f( eventLine.p0, eventLine.p1 ), eventClosest ); + m_tooltipT = ( closestPoint - edgeSegment.p0 ).length() / edgeSegment.length(); } - if( - event.modifiers == g_quadLightConstrainAspectRatioKey && - m_handleType & HandleType::Width && - m_handleType & HandleType::Height - ) + setTooltipPosition( edgeSegment( m_tooltipT ) ); + } + + private : + + float oppositeInspectionValue() const + { + Inspector::ResultPtr oppositeInspection = handleInspection( m_oppositeParameter ); + return oppositeInspection ? oppositeInspection->typedValue( 0.f ) : 1.f; + } + + V3f edgeToGadgetSpace( const float edge ) const + { + return ( ( m_edgeAxis * edge * m_edgeScale ) / m_edgeToHandleRatio ); + } + + LineSegment3f edgeSegment( const float edgeLength, const float oppositeLength ) const + { + float fullEdgeLength = 0; + float fullEdgeLengthHalf = 0; + float radius0 = 0; + float radius1 = 0; + + fullEdgeLength = oppositeLength * m_oppositeScale; + fullEdgeLengthHalf = fullEdgeLength * 0.5f; + + radius0 = m_edgeMargin * ::rasterScaleFactor( this, -fullEdgeLengthHalf * m_oppositeAxis ); + radius1 = m_edgeMargin * ::rasterScaleFactor( this, fullEdgeLengthHalf * m_oppositeAxis ); + + LineSegment3f result; + + result.p0 = std::min( 0.f, -fullEdgeLengthHalf + radius0 ) * m_oppositeAxis; + result.p1 = std::max( 0.f, fullEdgeLengthHalf - radius1 ) * m_oppositeAxis; + + return result; + } + + void edgeTransform( const float edgeLength, const LineSegment3f &edgeSegment, M44f &edgeTransform ) const + { + edgeTransform = + rotationMatrix( V3f( 0, 0, 1.f ), m_oppositeAxis ) * + M44f().translate( + edgeSegment.p0 * m_oppositeAxis + edgeToGadgetSpace( edgeLength ) + ) + ; + } + + void coneTransform( const float edgeLength, M44f &coneTransform ) const + { + const V3f gadgetSpaceEdge = edgeToGadgetSpace( edgeLength ); + // Rotate the cone 90 degrees around the axis that is the width axis rotated 90 degrees around the z axis. + coneTransform = + rotationMatrix( V3f( 0, 0, 1.f ), m_edgeAxis ) * + M44f().scale( V3f( ::rasterScaleFactor( this, gadgetSpaceEdge ) ) ) * + M44f().translate( gadgetSpaceEdge ) + ; + } + + const InternedString m_edgeParameter; + const V3f m_edgeAxis; + const float m_edgeToHandleRatio; + const InternedString m_oppositeParameter; + const V3f m_oppositeAxis; + const float m_oppositeToHandleRatio; + const InternedString m_oppositeScaleAttributeName; + const float m_edgeMargin; + const std::string m_tipPlugSuffix; + + float m_edgeScale; + float m_oppositeScale; + M44f m_orientation; + float m_oppositeAdditionalScale; + float m_tooltipT; // Parameter `t` along the edge set at the start of the drag + std::optional m_drag; +}; + +class CornerHandle : public LightToolHandle +{ + public : + + CornerHandle( + const std::string &lightType, + SceneView *view, + const InternedString &widthParameter, + const V3f &widthAxis, + const float widthToHandleRatio, + const InternedString &heightParameter, + const V3f &heightAxis, + const float heightToHandleRatio, + const std::string &name + ) : + LightToolHandle( lightType, view, {widthParameter, heightParameter}, name ), + m_widthParameter( widthParameter ), + m_widthAxis( widthAxis ), + m_widthToHandleRatio( widthToHandleRatio ), + m_heightParameter( heightParameter ), + m_heightAxis( heightAxis ), + m_heightToHandleRatio( heightToHandleRatio ), + m_scale( V2f( 1.f ) ), + m_drag() + { + } + + ~CornerHandle() override + { + + } + + protected : + + bool handleDragMoveInternal( const GafferUI::DragDropEvent &event ) override + { + if( !m_drag ) + { + return true; + } + + Inspector::ResultPtr widthInspection = dragStartInspection( m_widthParameter ); + Inspector::ResultPtr heightInspection = dragStartInspection( m_heightParameter ); + if( !widthInspection || !heightInspection ) + { + return true; + } + + const float nonZeroWidth = widthInspection->typedValue( 0.f ) == 0 ? 1.f : widthInspection->typedValue( 0.f ); + const float nonZeroHeight = heightInspection->typedValue( 0.f ) == 0 ? 1.f : heightInspection->typedValue( 0.f ); + + const V2f newPosition = m_drag.value().updatedPosition( event ) - m_drag.value().startPosition(); + + float xMult = ( newPosition.x * m_widthToHandleRatio ) / ( nonZeroWidth * m_scale.x ) + 1.f; + float yMult = ( newPosition.y * m_heightToHandleRatio ) / ( nonZeroHeight * m_scale.y ) + 1.f; + + if( event.modifiers == g_quadLightConstrainAspectRatioKey ) { - if( m_dragStartInfo.originalWidth > m_dragStartInfo.originalHeight ) + if( widthInspection->typedValue( 0.f ) > heightInspection->typedValue( 0.f ) ) { yMult = xMult; } - else - { + else{ xMult = yMult; } } @@ -1815,180 +2389,228 @@ class QuadLightHandle : public LightToolHandle xMult = std::max( xMult, 0.f ); yMult = std::max( yMult, 0.f ); - for( auto &[widthInspection, originalWidth, heightInspection, originalHeight] : m_inspections ) + applyMultiplier( m_widthParameter, xMult ); + applyMultiplier( m_heightParameter, yMult ); + + return true; + } + + void updateLocalTransformInternal( const V3f &scale, const V3f & ) override + { + m_scale = V2f( scale.x, scale.y ); + } + + bool handleDragEndInternal() override + { + m_drag = std::nullopt; + return false; + } + + void addHandleVisualisation( IECoreGL::Group *rootGroup, const bool selectionPass, const bool highlighted ) const override + { + if( getLookThroughLight() ) + { + return; + } + + Inspector::ResultPtr widthInspection = handleInspection( m_widthParameter ); + Inspector::ResultPtr heightInspection = handleInspection( m_heightParameter ); + if( !widthInspection || !heightInspection ) { - nonZeroWidth = originalWidth == 0 ? 1.f : originalWidth; - nonZeroHeight = originalHeight == 0 ? 1.f : originalHeight; + return; + } - if( m_handleType & HandleType::Width && widthInspection && widthInspection->editable() ) - { - ValuePlugPtr widthPlug = widthInspection->acquireEdit(); - auto widthFloatPlug = runTimeCast( activeValuePlug( widthPlug.get() ) ); - if( !widthFloatPlug ) - { - throw Exception( "Invalid type of \"widthParameter\"" ); - } + float cornerRadius = 0; - setValueOrAddKey( - widthFloatPlug, - m_view->getContext()->getTime(), - nonZeroWidth * xMult - ); - } + if( selectionPass ) + { + cornerRadius = g_circleHandleSelectionWidth; + } + else + { + cornerRadius = highlighted ? g_circleHandleWidthLarge : g_circleHandleWidth; + } - if( m_handleType & HandleType::Height && heightInspection && heightInspection->editable() ) - { - ValuePlugPtr heightPlug = heightInspection->acquireEdit(); - auto heightFloatPlug = runTimeCast( activeValuePlug( heightPlug.get() ) ); - if( !heightFloatPlug ) - { - throw Exception( "Invalid type of \"heightParameter\"" ); - } + IECoreGL::GroupPtr iconGroup = new IECoreGL::Group; + iconGroup->getState()->add( + new IECoreGL::ShaderStateComponent( + ShaderLoader::defaultShaderLoader(), + TextureLoader::defaultTextureLoader(), + faceCameraVertexSource(), + "", + constantFragSource(), + new CompoundObject + ) + ); - setValueOrAddKey( - heightFloatPlug, - m_view->getContext()->getTime(), - nonZeroHeight * yMult - ); + const V3f widthOffset = ( ( m_widthAxis * widthInspection->typedValue( 0.f ) * m_scale.x ) / m_widthToHandleRatio ); + const V3f heightOffset = (( m_heightAxis * heightInspection->typedValue( 0.f ) * m_scale.y ) / m_heightToHandleRatio ); + + iconGroup->setTransform( + M44f().scale( V3f( cornerRadius ) * ::rasterScaleFactor( this, V3f( 0 ) ) ) * + M44f().translate( widthOffset + heightOffset ) + ); + iconGroup->addChild( circle() ); + + rootGroup->addChild( iconGroup ); + } + + void setupDrag( const DragDropEvent &event ) override + { + m_drag = PlanarDrag( this, V3f( 0 ), m_widthAxis, m_heightAxis, event, true ); + } + + std::vector handleValueInspections() const override + { + std::vector result; + for( const auto &i : inspections() ) + { + for( const auto &p: i ) + { + result.push_back( p.second.get() ); } } - return true; + return result; } - bool handleDragEnd() override - { - m_drag = std::monostate{}; - return false; + std::string tipPlugSuffix() const override + { + return "plugs"; + } + + std::string tipInfoSuffix() const override + { + return "Hold Ctrl to maintain aspect ratio"; + } + + void updateTooltipPosition( const LineSegment3f &eventLine ) override + { + if( !hasInspectors() ) + { + return; + } + + Inspector::ResultPtr widthInspection = handleInspection( m_widthParameter ); + Inspector::ResultPtr heightInspection = handleInspection( m_heightParameter ); + if( !widthInspection || !heightInspection ) + { + return; + } + + setTooltipPosition( edgeTooltipPosition( widthInspection->typedValue( 0.f ), heightInspection->typedValue( 0.f ) ) ); } - void updateLocalTransform( const V3f &scale, const V3f & ) override + private : + + V3f edgeTooltipPosition( const float width, const float height ) const { - // Translate the handle to the center of the appropriate edge or corner. - const auto &[widthInspection, originalWidth, heightInspection, originalHeight] = handleInspections(); - m_scale = V2f( scale.x, scale.y ); + return ( width * 0.5f * m_widthAxis * m_scale.x ) + ( height * 0.5f * m_heightAxis * m_scale.y ); + } - M44f transform; - if( m_handleType & HandleType::Width ) - { - transform *= M44f().translate( V3f( originalWidth * 0.5f * m_xSign * m_scale.x, 0, 0 ) ); - } - if( m_handleType & HandleType::Height ) - { - transform *= M44f().translate( V3f( 0, originalHeight * 0.5f * m_ySign * m_scale.y, 0 ) ); - } + const InternedString m_widthParameter; + const V3f m_widthAxis; + const float m_widthToHandleRatio; + const InternedString m_heightParameter; + const V3f m_heightAxis; + const float m_heightToHandleRatio; + V2f m_scale; + std::optional m_drag; +}; - setTransform( transform ); +class RadiusHandle : public LightToolHandle +{ + public : + RadiusHandle( + const std::string &lightType, + SceneView *view, + const InternedString &radiusParameter, + const float radiusToHandleRatio, + const bool faceCamera, + const bool useScale, + const std::string &name + ) : + LightToolHandle( lightType, view, {radiusParameter}, name ), + m_radiusParameter( radiusParameter ), + m_radiusToHandleRatio( radiusToHandleRatio ), + m_faceCamera( faceCamera ), + m_useScale( useScale ), + m_dragDirection() + { } - bool visible() const override + protected : + bool handleDragMoveInternal( const GafferUI::DragDropEvent &event ) override { - // We require both width and height to be present to be a valid quad light - if( !m_widthInspector || !m_heightInspector ) + if( !m_drag ) { - return false; + return true; } - Inspector::ResultPtr contextWidthInspection = m_widthInspector->inspect(); - Inspector::ResultPtr contextHeightInspection = m_heightInspector->inspect(); - - if( !contextWidthInspection || !contextHeightInspection ) + if( !dragStartInspection( m_radiusParameter ) ) { - return false; + return true; } + const float increment = + ( + ( m_drag.value().updatedPosition( event ) ) - + ( m_drag.value().startPosition() ) + ) * m_radiusToHandleRatio + ; + + applyIncrement( m_radiusParameter, increment, 0, std::numeric_limits::max() ); + return true; } - bool enabled() const override + void updateLocalTransformInternal( const V3f &scale, const V3f & ) override { - if( !m_widthInspector || !m_heightInspector ) + if( m_useScale ) { - return false; + setTransform( M44f().scale( scale ) ); } - - // Return true without checking the `enabled()` state of our inspections. - // This allows the tooltip-on-highlight behavior to show a tooltip explaining - // why an edit is not possible. The alternative is to draw the tooltip for all - // handles regardless of mouse position because a handle can only be in a disabled - // or highlighted drawing state. - // The drawing code takes care of graying out uneditable handles and the inspections - // prevent the value from being changed. - return true; } - std::vector inspectors() const override + bool handleDragEndInternal() override { - return {m_widthInspector.get(), m_heightInspector.get()}; + m_drag = std::nullopt; + return false; } - protected : - - void renderHandle( const Style *style, Style::State state ) const override + void addHandleVisualisation( IECoreGL::Group *rootGroup, const bool selectionPass, const bool highlighted ) const override { if( getLookThroughLight() ) { return; } - State::bindBaseState(); - auto glState = const_cast( State::defaultState() ); - - IECoreGL::GroupPtr group = new IECoreGL::Group; - - const bool highlighted = state == Style::State::HighlightedState; + Inspector::ResultPtr radiusInspection = handleInspection( m_radiusParameter ); + if( !radiusInspection ) + { + return; + } - float spokeRadius = 0; - float coneSize = 0; - float cornerRadius = 0; + float thickness = 0.f; + float iconRadius = 0.f; - if( IECoreGL::Selector::currentSelector() ) + if( selectionPass ) { - spokeRadius = g_lineSelectionWidth; - coneSize = g_arrowHandleSelectionSize; - cornerRadius = g_circleHandleSelectionWidth; + thickness = g_lineSelectionWidth; + iconRadius = g_circleHandleSelectionWidth; } else { - spokeRadius = highlighted ? g_lineHandleWidthLarge : g_lineHandleWidth; - coneSize = highlighted ? g_arrowHandleSizeLarge : g_arrowHandleSize; - cornerRadius = highlighted ? g_circleHandleWidthLarge : g_circleHandleWidth; + thickness = highlighted ? g_lineHandleWidthLarge : g_lineHandleWidth; + iconRadius = highlighted ? g_circleHandleWidthLarge : g_circleHandleWidth; } - spokeRadius *= g_quadLightHandleSizeMultiplier; - coneSize *= g_quadLightHandleSizeMultiplier; - cornerRadius *= g_quadLightHandleSizeMultiplier; - - group->getState()->add( - new IECoreGL::ShaderStateComponent( - ShaderLoader::defaultShaderLoader(), - TextureLoader::defaultTextureLoader(), - "", - "", - constantFragSource(), - new CompoundObject - ) - ); - - auto standardStyle = runTimeCast( style ); - assert( standardStyle ); - const Color3f highlightColor3 = standardStyle->getColor( StandardStyle::Color::HighlightColor ); - const Color4f highlightColor4 = Color4f( highlightColor3.x, highlightColor3.y, highlightColor3.z, 1.f ); - - const bool enabled = allInspectionsEnabled(); - - group->getState()->add( - new IECoreGL::Color( - enabled ? ( highlighted ? g_lightToolHighlightColor4 : highlightColor4 ) : g_lightToolDisabledColor4 - ) - ); + const float radius = radiusInspection->typedValue( 0.f ) / m_radiusToHandleRatio; - if( ( m_handleType & HandleType::Width ) && ( m_handleType & HandleType::Height ) ) + IECoreGL::GroupPtr torusGroup = new IECoreGL::Group; + if( m_faceCamera ) { - // Circles at corners for planar drag - - IECoreGL::GroupPtr iconGroup = new IECoreGL::Group; - iconGroup->getState()->add( + torusGroup->getState()->add( new IECoreGL::ShaderStateComponent( ShaderLoader::defaultShaderLoader(), TextureLoader::defaultTextureLoader(), @@ -1998,274 +2620,301 @@ class QuadLightHandle : public LightToolHandle new CompoundObject ) ); - iconGroup->setTransform( - M44f().scale( V3f( cornerRadius ) * ::rasterScaleFactor( this, V3f( 0 ) ) ) - ); - iconGroup->addChild( circle() ); - group->addChild( iconGroup ); } - else - { - // Lines and arrows on edges for linear drag - LineSegment3f edgeSegment = this->edgeSegment( handleInspections() ); - - M44f coneTransform; - M44f edgeTransform; - edgeTransforms( edgeSegment, coneTransform, edgeTransform ); + V3f scale; + extractScaling( getTransform(), scale ); + torusGroup->addChild( + torus( + radius * scale.x, + radius * scale.y, + thickness, + this, + m_faceCamera ? Axis::X : Axis::Z + ) + ); + rootGroup->addChild( torusGroup ); - IECoreGL::GroupPtr coneGroup = new IECoreGL::Group; - coneGroup->setTransform( coneTransform * M44f().scale( V3f( coneSize ) ) ); - coneGroup->addChild( unitCone() ); - group->addChild( coneGroup ); + IECoreGL::GroupPtr iconGroup = new IECoreGL::Group; + iconGroup->getState()->add( + new IECoreGL::ShaderStateComponent( + ShaderLoader::defaultShaderLoader(), + TextureLoader::defaultTextureLoader(), + faceCameraVertexSource(), + "", + constantFragSource(), + new CompoundObject + ) + ); - IECoreGL::GroupPtr edgeGroup = new IECoreGL::Group; - edgeGroup->addChild( - cone( - edgeSegment.length(), - spokeRadius * ::rasterScaleFactor( this, edgeSegment.p0 ), - spokeRadius * ::rasterScaleFactor( this, edgeSegment.p1 ) - ) - ); - edgeGroup->setTransform( edgeTransform ); + const float xOffset = radius * scale.x; - group->addChild( edgeGroup ); + const V3f iconScale = V3f( iconRadius ) * ::rasterScaleFactor( this, V3f( xOffset, 0, 0 ) ); + M44f transform = M44f().scale( iconScale ); + if( !m_faceCamera ) + { + // If the entire handle is not facing the camera, offset the icon in + // gadget space so the center of the rotation is the center of the circle icon. + // Otherwise we bake in the offset below into the circle geometry so the center + // of "facing" rotation is the center of the handle. + transform *= M44f().translate( V3f( xOffset, 0, 0 ) ); } + iconGroup->setTransform( transform ); + iconGroup->addChild( circle( Axis::X, m_faceCamera ? ( V3f( 0, 0, xOffset ) / iconScale ) : V3f( 0 ) ) ); - group->render( glState ); + rootGroup->addChild( iconGroup ); + rootGroup->setTransform( M44f().scale( V3f( 1.f / scale.x, 1.f / scale.y, 1.f / scale.z ) ) ); + } - if( highlighted ) - { - std::vector inspections; - for( const auto &[widthInspection, originalWidth, heightInspection, originalHeight] : m_inspections ) - { - if( m_handleType & HandleType::Width ) - { - inspections.push_back( widthInspection.get() ); - } - if( m_handleType & HandleType::Height ) - { - inspections.push_back( heightInspection.get() ); - } - } - std::string tipSuffix = ""; - if( m_handleType & HandleType::Width ) - { - tipSuffix = "widths"; - } - if( m_handleType & HandleType::Height ) - { - tipSuffix = m_handleType & HandleType::Width ? "plugs" : "heights"; - } + void setupDrag( const DragDropEvent &event ) override + { + m_dragDirection = circlePosition( event.line ).normalized(); + m_drag = Handle::LinearDrag( + this, + LineSegment3f( V3f( 0 ), m_dragDirection ), + event, + true + ); + } - drawSelectionTips( - m_edgeCursorPoint, - inspections, - tipSuffix, - ( m_handleType & HandleType::Width && m_handleType &HandleType::Height ) ? "Hold Ctrl to maintain aspect ratio" : "", - this, - m_view->viewportGadget(), - style - ); + std::string tipPlugSuffix() const override + { + return "radii"; + } + + void updateTooltipPosition( const LineSegment3f &eventLine ) override + { + if( m_drag ) + { + const Inspector::ResultPtr radiusInspection = handleInspection( m_radiusParameter ); + const float radius = radiusInspection->typedValue( 0 ); + setTooltipPosition( ( m_dragDirection * radius ) / m_radiusToHandleRatio ); + } + else + { + setTooltipPosition( circlePosition( eventLine ) ); } } private : - struct InspectionInfo - { - Inspector::ResultPtr widthInspection; - float originalWidth; - Inspector::ResultPtr heightInspection; - float originalHeight; - }; - - bool mouseMove( const ButtonEvent &event ) + V3f circlePosition( const LineSegment3f &line ) const { - if( !m_widthInspector || ! m_heightInspector ) + if( m_faceCamera ) { - return false; + // Closest intersection of the line and a sphere at the origin with our radius + Inspector::ResultPtr radiusInspection = handleInspection( m_radiusParameter ); + const float radius = radiusInspection->typedValue( 0.f ) / m_radiusToHandleRatio; + return lineSphereIntersection( line, V3f( 0 ), radius ); } - if( m_handleType & HandleType::Width && m_handleType &HandleType::Height ) + // If the line intersects the plane, the result is simply the intersection point + V3f planeIntersection; + if( line.intersect( Plane3f( V3f( 0 ), V3f( 0, 0, -1 ) ), planeIntersection ) ) { - m_edgeCursorPoint = V3f( 0, 0, 0 ); - return false; + return planeIntersection; } - LineSegment3f edgeSegment = this->edgeSegment( handleInspections() ); - - V3f eventClosest; - m_edgeCursorPoint = edgeSegment.closestPoints( LineSegment3f( event.line.p0, event.line.p1 ), eventClosest ); + // If no line / plane intersection, project the line to the Z plane and take + // the first intersection with the circle + const LineSegment2f projectedLine( + V2f( line.p0.x, line.p0.y ), V2f( line.p1.x, line.p1.y ) + ); - dirty( DirtyType::Render ); + Inspector::ResultPtr radiusInspection = handleInspection( m_radiusParameter ); + const float radius = radiusInspection->typedValue( 0.f ) / m_radiusToHandleRatio; + const V2f intersection = lineSphereIntersection( projectedLine, V2f( 0 ), radius ); - return false; + // We don't scale here on purpose : when used for a linear drag axis, we normalize + // the returned value, and when drawing the tooltip, the scale transform is part + // of the gadget transform already. + return V3f( intersection.x, intersection.y, 0 ); } - void dragBegin( const DragDropEvent &event ) override - { - const auto &[widthInspection, originalWidth, heightInspection, originalHeight] = handleInspections(); - - m_dragStartInfo.widthInspection = widthInspection; - m_dragStartInfo.originalWidth = originalWidth; - m_dragStartInfo.heightInspection = heightInspection; - m_dragStartInfo.originalHeight = originalHeight; + const InternedString m_radiusParameter; + const float m_radiusToHandleRatio; + const bool m_faceCamera; + const float m_useScale; + V3f m_dragDirection; + std::optional m_drag; +}; - if( m_handleType & HandleType::Width && m_handleType & HandleType::Height ) - { - m_drag = Handle::PlanarDrag( this, V3f( 0 ), V3f( m_xSign, 0, 0 ), V3f( 0, m_ySign, 0 ), event, true ); - } - else if( m_handleType & HandleType::Width ) - { - m_drag = Handle::LinearDrag( this, LineSegment3f( V3f( 0 ), V3f( m_xSign, 0, 0 ) ), event, true ); - } - else if( m_handleType & HandleType::Height ) - { - m_drag = Handle::LinearDrag( this, LineSegment3f( V3f( 0 ), V3f( 0, m_ySign, 0 ) ), event, true ); - } +class LengthHandle : public LightToolHandle +{ + public : + LengthHandle( + const std::string &lightType, + SceneView *view, + const InternedString ¶meter, + const V3f &axis, + const float lengthToHandleRatio, + const std::string &name + ) : + LightToolHandle( lightType, view, {parameter}, name ), + m_parameter( parameter ), + m_axis( axis ), + m_lengthToHandleRatio( lengthToHandleRatio ), + m_orientation(), + m_scale() + { } - InspectionInfo handleInspections() const + ~LengthHandle() override { - ScenePlug::PathScope pathScope( handleScenePath()->getContext() ); - pathScope.setPath( &handleScenePath()->names() ); - return inspectionInfo(); } - // Returns a `InspectionInfo` object for the current context. - InspectionInfo inspectionInfo() const + protected : + void handlePathChanged() override { - Inspector::ResultPtr widthInspection = nullptr; - float originalWidth = 0; + /// \todo This can be simplified and some of the logic, especially getting the inspectors, can + /// be moved to the constructor when we standardize on a single USDLux light representation. - // Get an inspection if possible regardless of the handle type because drawing - // edge lines requires the opposite dimension's value. - if( m_widthInspector ) - { - widthInspection = m_widthInspector->inspect(); - if( widthInspection ) - { - auto originalWidthData = runTimeCast( widthInspection->value() ); - assert( originalWidthData ); - originalWidth = originalWidthData->readable(); - } - } + ConstCompoundObjectPtr attributes = scene()->fullAttributes( handlePath() ); - Inspector::ResultPtr heightInspection = nullptr; - float originalHeight = 0; - if( m_heightInspector ) + for( const auto &[attributeName, value] : attributes->members() ) { - heightInspection = m_heightInspector->inspect(); - if( heightInspection ) + if( + StringAlgo::matchMultiple( attributeName, g_lightAttributePattern ) && + value->typeId() == (IECore::TypeId)ShaderNetworkTypeId + ) { - auto originalHeightData = runTimeCast( heightInspection->value() ); - assert( originalHeightData ); - originalHeight = originalHeightData->readable(); + const auto shader = attributes->member( attributeName )->outputShader(); + std::string shaderAttribute = shader->getType() + ":" + shader->getName(); + + if( !isLightType( shaderAttribute ) ) + { + continue; + } + + m_orientation = M44f(); + if( auto orientationData = Metadata::value( shaderAttribute, "visualiserOrientation" ) ) + { + m_orientation = orientationData->readable(); + } + + break; } } - - return { widthInspection, originalWidth, heightInspection, originalHeight }; } - bool allInspectionsEnabled() const + bool handleDragMoveInternal( const GafferUI::DragDropEvent &event ) override { - bool enabled = true; - for( auto &[widthInspection, originalWidth, heightInspection, originalHeight] : m_inspections ) + if( !m_drag ) { - if( m_handleType & HandleType::Width ) - { - enabled &= widthInspection ? widthInspection->editable() : false; - } - if( m_handleType & HandleType::Height ) - { - enabled &= heightInspection ? heightInspection->editable() : false; - } + return true; } - return enabled; + if( !dragStartInspection( m_parameter ) ) + { + return true; + } + + const float updatedPosition = m_drag.value().updatedPosition( event ) / m_scale; + const float startPosition = m_drag.value().startPosition() / m_scale; + const float increment = ( updatedPosition - startPosition ) * m_lengthToHandleRatio; + + applyIncrement( m_parameter, increment, 0, std::numeric_limits::max() ); + + return true; } - LineSegment3f edgeSegment( const InspectionInfo &inspectionInfo ) const + void updateLocalTransformInternal( const V3f &scale, const V3f & ) override { - const auto &[widthInspection, width, heightInspection, height] = inspectionInfo; + m_scale = abs( m_axis.dot( scale * m_orientation ) ); + } - float fullEdgeLength = 0; - float fullEdgeLengthHalf = 0; - float radius0 = 0; - float radius1 = 0; - if( m_handleType & HandleType::Width ) + bool handleDragEndInternal() override + { + m_drag = std::nullopt; + return false; + } + + void addHandleVisualisation( IECoreGL::Group *rootGroup, const bool selectionPass, const bool highlighted ) const override + { + if( getLookThroughLight() ) { - fullEdgeLength = height * m_scale.y; - fullEdgeLengthHalf = fullEdgeLength * 0.5f; - radius0 = g_circleHandleWidthLarge * ::rasterScaleFactor( this, V3f( 0, -fullEdgeLengthHalf, 0 ) ) * g_quadLightHandleSizeMultiplier; - radius1 = g_circleHandleWidthLarge * ::rasterScaleFactor( this, V3f( 0, fullEdgeLengthHalf, 0 ) ) * g_quadLightHandleSizeMultiplier; + return; } - else + + Inspector::ResultPtr inspection = handleInspection( m_parameter ); + if( !inspection ) { - fullEdgeLength = width * m_scale.x; - fullEdgeLengthHalf = fullEdgeLength * 0.5f; - radius0 = g_circleHandleWidthLarge * ::rasterScaleFactor( this, V3f( -fullEdgeLengthHalf, 0, 0 ) ) * g_quadLightHandleSizeMultiplier; - radius1 = g_circleHandleWidthLarge * ::rasterScaleFactor( this, V3f( fullEdgeLengthHalf, 0, 0 ) ) * g_quadLightHandleSizeMultiplier; + return; } - LineSegment3f result; - - if( m_handleType & HandleType::Width ) + float coneSize = 0.f; + if( selectionPass ) { - result.p0 = V3f( 0, std::min( 0.f, -fullEdgeLengthHalf + radius0 ), 0 ); - result.p1 = V3f( 0, std::max( 0.f, fullEdgeLengthHalf - radius1 ), 0 ); + coneSize = g_arrowHandleSelectionSize; } else { - result.p0 = V3f( std::min( 0.f, -fullEdgeLengthHalf + radius0 ), 0, 0 ); - result.p1 = V3f( std::max( 0.f, fullEdgeLengthHalf - radius1 ), 0, 0 ); + coneSize = highlighted ? g_arrowHandleSizeLarge : g_arrowHandleSize; } - return result; + const V3f offset = this->offset( inspection.get() ); + + IECoreGL::GroupPtr coneGroup = new IECoreGL::Group; + coneGroup->setTransform( + M44f().scale( V3f( coneSize ) * ::rasterScaleFactor( this, offset ) ) * + rotationMatrix( V3f( 0, 0, 1.f ), m_axis ) * + M44f().translate( offset ) * + m_orientation + ); + coneGroup->addChild( unitCone() ); + + rootGroup->addChild( coneGroup ); } - void edgeTransforms( const LineSegment3f &edgeSegment, M44f &coneTransform, M44f &edgeTransform ) const + void setupDrag( const DragDropEvent &event ) override { - if( m_handleType & HandleType::Width ) - { - coneTransform = M44f().rotate( V3f( 0, M_PI * 0.5f * m_xSign, 0 ) ); - edgeTransform = - M44f().rotate( V3f( -M_PI * 0.5f, 0, 0 ) ) * - M44f().translate( V3f( 0, edgeSegment.p0.y, 0 ) ) - ; - } - else - { - coneTransform = M44f().rotate( V3f( M_PI * 0.5f * -m_ySign, 0, 0 ) ); - edgeTransform = - M44f().rotate( V3f( 0, M_PI * 0.5f, 0 ) ) * - M44f().translate( V3f( edgeSegment.p0.x, 0, 0 ) ) - ; - } - coneTransform *= M44f().scale( V3f( ::rasterScaleFactor( this, V3f( 0.0 ) ) ) ); + Inspector::ResultPtr inspection = handleInspection( m_parameter ); + V3f offset = this->offset( inspection.get() ); + + m_drag = Handle::LinearDrag( + this, + LineSegment3f( V3f( 0 ), ( m_axis * m_orientation ) ), + event, + true + ); } - ParameterInspectorPtr m_widthInspector; - ParameterInspectorPtr m_heightInspector; + std::string tipPlugSuffix() const override + { + return "lengths"; + } - const SceneView *m_view; + void updateTooltipPosition( const LineSegment3f &eventLine ) override + { + if( !hasInspectors() ) + { + return; + } - std::vector m_inspections; + Inspector::ResultPtr inspection = handleInspection( m_parameter ); - std::variant m_drag; + const M44f transform = + M44f().translate( offset( inspection.get() ) ) * + m_orientation + ; - const unsigned m_handleType; + setTooltipPosition( V3f( 0 ) * transform ); + } - InspectionInfo m_dragStartInfo; + private : - // The sign for each axis of the handle - const float m_xSign; - const float m_ySign; + V3f offset( Inspector::Result *inspection ) const + { + return ( m_axis * inspection->typedValue( 0.f ) * m_scale ) / m_lengthToHandleRatio; + } - V3f m_edgeCursorPoint; - V2f m_scale; // width and height scale of the light's transform + const InternedString m_parameter; + const V3f m_axis; + const float m_lengthToHandleRatio; + M44f m_orientation; + float m_scale; + std::optional m_drag; }; // ============================================================================ @@ -2353,26 +3002,40 @@ LightTool::LightTool( SceneView *view, const std::string &name ) : // Spotlight handles - m_handles->addChild( new SpotLightHandle( "*light", SpotLightHandle::HandleType::Penumbra, view, 0, "westConeAngleParameter" ) ); - m_handles->addChild( new SpotLightHandle( "*light", SpotLightHandle::HandleType::Cone, view, 0, "westPenumbraAngleParameter" ) ); - m_handles->addChild( new SpotLightHandle( "*light", SpotLightHandle::HandleType::Penumbra, view, 90, "southConeAngleParameter" ) ); - m_handles->addChild( new SpotLightHandle( "*light", SpotLightHandle::HandleType::Cone, view, 90, "southPenumbraAngleParameter" ) ); - m_handles->addChild( new SpotLightHandle( "*light", SpotLightHandle::HandleType::Penumbra, view, 180, "eastConeAngleParameter" ) ); - m_handles->addChild( new SpotLightHandle( "*light", SpotLightHandle::HandleType::Cone, view, 180, "eastPenumbraAngleParameter" ) ); - m_handles->addChild( new SpotLightHandle( "*light", SpotLightHandle::HandleType::Penumbra, view, 270, "northConeAngleParameter" ) ); - m_handles->addChild( new SpotLightHandle( "*light", SpotLightHandle::HandleType::Cone, view, 270, "northPenumbraAngleParameter" ) ); + m_handles->addChild( new SpotLightHandle( "spot quad point disk distant", SpotLightHandle::HandleType::Penumbra, view, 0, "westConeAngleParameter" ) ); + m_handles->addChild( new SpotLightHandle( "spot quad point disk distant", SpotLightHandle::HandleType::Cone, view, 0, "westPenumbraAngleParameter" ) ); + m_handles->addChild( new SpotLightHandle( "spot quad point disk distant", SpotLightHandle::HandleType::Penumbra, view, 90, "southConeAngleParameter" ) ); + m_handles->addChild( new SpotLightHandle( "spot quad point disk distant", SpotLightHandle::HandleType::Cone, view, 90, "southPenumbraAngleParameter" ) ); + m_handles->addChild( new SpotLightHandle( "spot quad point disk distant", SpotLightHandle::HandleType::Penumbra, view, 180, "eastConeAngleParameter" ) ); + m_handles->addChild( new SpotLightHandle( "spot quad point disk distant", SpotLightHandle::HandleType::Cone, view, 180, "eastPenumbraAngleParameter" ) ); + m_handles->addChild( new SpotLightHandle( "spot quad point disk distant", SpotLightHandle::HandleType::Penumbra, view, 270, "northConeAngleParameter" ) ); + m_handles->addChild( new SpotLightHandle( "spot quad point disk distant", SpotLightHandle::HandleType::Cone, view, 270, "northPenumbraAngleParameter" ) ); // Quadlight handles - m_handles->addChild( new QuadLightHandle( "*light", QuadLightHandle::HandleType::Width, view, -1.f, 0, "westParameter" ) ); - m_handles->addChild( new QuadLightHandle( "*light", QuadLightHandle::HandleType::Width | QuadLightHandle::HandleType::Height, view, -1.f, -1.f, "southWestParameter" ) ); - m_handles->addChild( new QuadLightHandle( "*light", QuadLightHandle::HandleType::Height, view, 0, -1.f, "southParameter" ) ); - m_handles->addChild( new QuadLightHandle( "*light", QuadLightHandle::HandleType::Width | QuadLightHandle::HandleType::Height, view, 1.f, -1.f, "soutEastParameter" ) ); - m_handles->addChild( new QuadLightHandle( "*light", QuadLightHandle::HandleType::Width, view, 1.f, 0.f, "eastParameter" ) ); - m_handles->addChild( new QuadLightHandle( "*light", QuadLightHandle::HandleType::Width | QuadLightHandle::HandleType::Height, view, 1.f, 1.f, "northEastParameter" ) ); - m_handles->addChild( new QuadLightHandle( "*light", QuadLightHandle::HandleType::Height, view, 0, 1.f, "northParameter" ) ); - m_handles->addChild( new QuadLightHandle( "*light", QuadLightHandle::HandleType::Width | QuadLightHandle::HandleType::Height, view, -1.f, 1.f, "northWestParameter" ) ); - + m_handles->addChild( new EdgeHandle( "quad", view, "widthParameter", V3f( -1.f, 0, 0 ), 2.f, "heightParameter", V3f( 0, 1.f, 0 ), 2.f, "", g_circleHandleWidthLarge, "widths", "westParameter" ) ); + m_handles->addChild( new CornerHandle( "quad", view, "widthParameter", V3f( -1.f, 0, 0 ), 2.f, "heightParameter", V3f( 0, -1.f, 0 ), 2.f, "southWestParameter" ) ); + m_handles->addChild( new EdgeHandle( "quad", view, "heightParameter", V3f( 0, -1.f, 0 ), 2.f, "widthParameter", V3f( 1.f, 0, 0 ), 2.f, "", g_circleHandleWidthLarge, "heights", "southParameter" ) ); + m_handles->addChild( new CornerHandle( "quad", view, "widthParameter", V3f( 1.f, 0, 0 ), 2.f, "heightParameter", V3f( 0, -1.f, 0 ), 2.f, "soutEastParameter" ) ); + m_handles->addChild( new EdgeHandle( "quad", view, "widthParameter", V3f( 1.f, 0, 0 ), 2.f, "heightParameter", V3f( 0, 1.f, 0 ), 2.f, "", g_circleHandleWidthLarge, "widths", "eastParameter" ) ); + m_handles->addChild( new CornerHandle( "quad", view, "widthParameter", V3f( 1.f, 0, 0 ), 2.f, "heightParameter", V3f( 0, 1.f, 0 ), 2.f, "northEastParameter" ) ); + m_handles->addChild( new EdgeHandle( "quad", view, "heightParameter", V3f( 0, 1.f, 0 ), 2.f, "widthParameter", V3f( 1.f, 0, 0 ), 2.f, "", g_circleHandleWidthLarge, "heights", "northParameter" ) ); + m_handles->addChild( new CornerHandle( "quad", view, "widthParameter", V3f( -1.f, 0, 0 ), 2.f, "heightParameter", V3f( 0, 1.f, 0 ), 2.f, "northWestParameter" ) ); + + // DiskLight handles + m_handles->addChild( new RadiusHandle( "disk", view, "radiusParameter", 1.f, false, true, "diskHandle" ) ); + m_handles->addChild( new RadiusHandle( "disk", view, "widthParameter", 2.f, false, true, "diskHandle" ) ); + + // Sphere / PointLight handles + m_handles->addChild( new RadiusHandle( "point", view, "radiusParameter", 1.f, true, false, "pointHandle" ) ); + + // CylinderLight handles + m_handles->addChild( new EdgeHandle( "cylinder", view, "radiusParameter", V3f( 0, 1.f, 0 ), 1.f, "lengthParameter", V3f( 0, 0, 1.f ), 2.f, "heightToScaleRatio", 0, "radii", "northRadiusParameter" ) ); + m_handles->addChild( new EdgeHandle( "cylinder", view, "radiusParameter", V3f( 1.f, 0, 0 ), 1.f, "lengthParameter", V3f( 0, 0, 1.f ), 2.f, "heightToScaleRatio", 0, "radii", "northRadiusParameter" ) ); + m_handles->addChild( new EdgeHandle( "cylinder", view, "radiusParameter", V3f( 0, -1.f, 0 ), 1.f, "lengthParameter", V3f( 0, 0, 1.f ), 2.f, "heightToScaleRatio", 0, "radii", "northRadiusParameter" ) ); + m_handles->addChild( new EdgeHandle( "cylinder", view, "radiusParameter", V3f( -1.f, 0, 0 ), 1.f, "lengthParameter", V3f( 0, 0, 1.f ), 2.f, "heightToScaleRatio", 0, "radii", "northRadiusParameter" ) ); + m_handles->addChild( new LengthHandle( "cylinder", view, "lengthParameter", V3f( 0, 0, 1.f ), 2.f, "cylinderLengthTop" ) ); + m_handles->addChild( new LengthHandle( "cylinder", view, "lengthParameter", V3f( 0, 0, -1.f ), 2.f, "cylinderLengthBottom" ) ); for( const auto &c : m_handles->children() ) { @@ -2508,10 +3171,7 @@ void LightTool::updateHandleInspections() auto handle = runTimeCast( c ); assert( handle ); - handle->update( - new ScenePath( scene, view()->getContext(), lastSelectedPath ), - view()->editScopePlug() - ); + handle->updateHandlePath( scene, view()->getContext(), lastSelectedPath ); bool handleVisible = true; bool handleEnabled = true; @@ -2529,14 +3189,14 @@ void LightTool::updateHandleInspections() handle->setEnabled( handleEnabled ); handle->setVisible( handleVisible ); - handle->clearDragInspections(); + handle->clearInspections(); if( handleVisible ) { for( PathMatcher::Iterator it = selection.begin(), eIt = selection.end(); it != eIt; ++it ) { pathScope.setPath( &(*it) ); - handle->addDragInspection(); + handle->addInspection(); } } } @@ -2567,6 +3227,8 @@ void LightTool::updateHandleTransforms( float rasterScale ) } const M44f fullTransform = scene->fullTransform( lastSelectedPath ); + /// \todo Should this be handled in the LightToolHandle derived classes + /// and make `updateLocalTransform()` a more general `setTransform()` method? m_handles->setTransform( sansScalingAndShear( fullTransform ) ); V3f scale; @@ -2697,25 +3359,7 @@ void LightTool::dirtyHandleTransforms() RunTimeTypedPtr LightTool::dragBegin( Gadget *gadget ) { m_dragging = true; - - auto handle = runTimeCast( gadget ); - assert( handle ); - const PathMatcher selection = this->selection(); - - std::vector inspectors = handle->inspectors(); - if( !inspectors.empty() ) - { - ScenePlug::PathScope pathScope( view()->getContext() ); - PathMatcher::Iterator it = selection.begin(); - pathScope.setPath( &( *it ) ); - if( Inspector::ResultPtr inspection = inspectors[0]->inspect() ) - { - if( ValuePlug *source = inspection->source() ) - { - m_scriptNode = source->ancestor(); - } - } - } + m_scriptNode = view()->inPlug()->source()->ancestor(); return nullptr; } diff --git a/startup/GafferScene/arnoldLights.py b/startup/GafferScene/arnoldLights.py index c323a6cfe1c..2aa4386aeee 100644 --- a/startup/GafferScene/arnoldLights.py +++ b/startup/GafferScene/arnoldLights.py @@ -83,6 +83,7 @@ Gaffer.Metadata.registerValue( "ai:light:cylinder_light", "colorParameter", "color" ) Gaffer.Metadata.registerValue( "ai:light:cylinder_light", "radiusParameter", "radius" ) Gaffer.Metadata.registerValue( "ai:light:cylinder_light", "visualiserOrientation", imath.M44f().rotate( imath.V3f( 0.5 * math.pi, 0 , 0 ) ) ) +Gaffer.Metadata.registerValue( "ai:light:cylinder_light", "heightToScaleRatio", 2.0 ) Gaffer.Metadata.registerValue( "ai:light:skydome_light", "intensityParameter", "intensity" ) Gaffer.Metadata.registerValue( "ai:light:skydome_light", "exposureParameter", "exposure" )