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 when 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( 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(); } } } }