rokojori_action_library/Runtime/Procedural/HeightMaps/Layers/HeightMapDrawLayer.cs

140 lines
4.3 KiB
C#

using System.Collections.Generic;
using Godot;
using System;
using Rokojori.Extensions;
namespace Rokojori
{
/** Base class for anything that draws into a HeightMapGenerator's map.
A Node3D, so a layer is placed, rotated and scaled in the 3D viewport
rather than in texel coordinates - that is the whole point of the design.
The generator converts the layer's world position into texels when it
draws.
Subclasses only implement RenderToHeightMap(). Everything about <marked>when
</marked> that happens is handled here: this class watches its own transform
and visibility and flags the nearest HeightMapGenerator above it, which
re-renders once on the next frame.
Two kinds sit under this: HeightMapGeneratorLayer for procedural shapes with
a blend mode, and - later - layers that are not shapes at all, like an
image or a nested map, which is why the blend mode is not here.
*/
[Tool]
public abstract partial class HeightMapDrawLayer:Node3D
{
/** Draws this layer into `generator`'s map.
Called in tree order during a render, with the map already allocated and
cleared. Implementations are expected to go through the generator for
coordinate conversion and for the blit itself - see
CircleHeightMapGeneratorLayer for the shape case.
*/
public abstract void RenderToHeightMap( HeightMapGenerator generator );
/* FINDING THE GENERATOR
------------------------------------------------------------------------
Resolved lazily rather than in _Ready, because _Ready order between a
layer and its generator is not something a layer should depend on, and
because reparenting a layer in the editor has to keep working.
*/
HeightMapGenerator _generator;
public HeightMapGenerator generator
{
get
{
if ( IsStillOurGenerator() )
{
return _generator;
}
_generator = NodesWalker.Get().GetParentWithType<HeightMapGenerator>( this );
return _generator;
}
}
/** The cached reference survives only while it is still a live node and
still above us - a freed generator or a reparented layer has to walk
again, and IsInstanceValid is what keeps the freed case from throwing.
*/
bool IsStillOurGenerator()
{
return _generator != null
&& GodotObject.IsInstanceValid( _generator )
&& _generator.IsInsideTree()
&& IsInsideTree()
&& _generator.IsAncestorOf( this );
}
/** Flags the generator above this layer for a re-render. Safe to call as
often as the editor emits changes - the generator collapses them into one
render per frame.
*/
protected void SetDirty()
{
var g = generator;
if ( g == null )
{
return;
}
g.SetDirty();
}
/* CHANGE NOTIFICATION
------------------------------------------------------------------------
Transform changes need SetNotifyTransform( true ) - Godot does not send
NotificationTransformChanged otherwise, and without it dragging a layer in
the viewport would not update the map at all.
Visibility rides on the eye icon in the scene tree, so the layer has no
"enabled" export of its own.
Exported values on a subclass are the subclass's business: a plain [Export]
field edited in the inspector notifies nothing, so every one of them has to
be a property whose setter calls SetDirty(). That is a convention, not
something this class can enforce.
*/
public override void _Ready()
{
SetNotifyTransform( true );
SetDirty();
}
public override void _EnterTree()
{
// A fresh walk next time - the layer may have been reparented under a
// different generator.
_generator = null;
SetDirty();
}
public override void _ExitTree()
{
// Flag the generator it is leaving, so the map loses the layer's
// contribution. Taken before the cached reference is dropped.
SetDirty();
_generator = null;
}
public override void _Notification( int what )
{
base._Notification( what );
if ( what == NotificationTransformChanged || what == NotificationVisibilityChanged )
{
SetDirty();
}
}
}
}