Jump to content
cltom

Which library for "Small" Graphics Application, GR32, GDI+, Skia, ...

Recommended Posts

7 hours ago, cltom said:

Sorry, the TImage32Access and Stretchtransfer come from which unit?

The code was copied from TCustomBitmapLayer in GR32_Layers, so you can find it there:

type
  TImage32Access = class(TCustomImage32);

Stretchtransfer is declared in GR32_Resamplers.

 

I'm considering making TIndirectBitmapLayer the base class for TCustomBitmapLayer.

  • Like 1

Share this post


Link to post
On 3/18/2023 at 3:06 PM, Anders Melander said:

The code was copied from TCustomBitmapLayer in GR32_Layers, so you can find it there:


type
  TImage32Access = class(TCustomImage32);

Stretchtransfer is declared in GR32_Resamplers.

 

I'm considering making TIndirectBitmapLayer the base class for TCustomBitmapLayer.

Again, a huge thank you! As an update: I got this to run with the TIndirectBitmapLayer class which - both to my shame but also to my joy - works well with very little code. Also, I had some inspirational from the ImgView_Layers-Demo.

 

In general: I have now a main class which handles the Objectlist for all elements (which is then used to save the drawing, etc.) and another class that does the drawing. Consequently, some conceptual questions:

 

- When using the TLayerCollection, the whole movement/scaling of objects is handled (great!). What is a good method of getting information on the movement of the layer back to my main class where I need to keep track of the objects and their coordinates? I could access the TLayerCollection and the position of the layers. I can only rely though on an ID in either structure to get the same object. Alternatively I could follow track the selected object and the mouse move in the main class.

 

- also, when trying to restrict the movement of layers, how can I access the coordinates of a layer while moving it to mimic a SnapToGrid function?

 

- coming back to the rubberband-question: how to implement this? Should I create a TRubberband layer via the MouseDown/MouseMove events?

 

Share this post


Link to post
42 minutes ago, cltom said:

- When using the TLayerCollection, the whole movement/scaling of objects is handled (great!). What is a good method of getting information on the movement of the layer back to my main class where I need to keep track of the objects and their coordinates? I could access the TLayerCollection and the position of the layers. I can only rely though on an ID in either structure to get the same object. Alternatively I could follow track the selected object and the mouse move in the main class.

I would just store a reference to your object and the object owner in the layer and then notify the owner when the layer is moved.

Something like this:

type
  TObjectLayer = class;

  TObjectLayerNotification = (
    olnDestroy,         // Subscribers should remove reference to layer
    olnPosition         // Layer has moved
  );

  IObjectLayerNotification = interface
    ['{C5715B62-6D20-4BEE-841A-A898AA67D6F7}']
    procedure ObjectLayerNotification(ALayer: TObjectLayer; ANotification: TObjectLayerNotification);
  end;

  TObjectLayer = class(TIndirectBitmapLayer)
  private
    FSubscribers: TList<IObjectLayerNotification>;
    FObjectID: TSomeType;
  protected
    procedure Notify(ANotification: IObjectLayerNotification);
    procedure DoSetLocation(const NewLocation: TFloatRect); override;
  public
    destructor Destroy; override;

    procedure Subscribe(const ASubscriber: IObjectLayerNotification);
    procedure Unsubscribe(const ASubscriber: IObjectLayerNotification);

    property ObjectID: TSomeType read FObjectID write FObjectID;
  end;

destructor TObjectLayer.Destroy;
begin
  Notify(olnDestroy);
  FSubscribers.Free;
  inherited;
end;

procedure TObjectLayer.DoSetLocation(const NewLocation: TFloatRect);
begin
  inherited DoSetLocation(NewLocation);
  Notify(olnPosition);
end;

procedure TObjectLayer.Notify(ANotification: IObjectLayerNotification);
begin
  if (FSubscribers = nil) then
    exit;
  for var Subscriber in FSubscribers.ToArray do // ToArray for stability
    Subscriber.ObjectLayerNotification(Self, ANotification);
end;

procedure TObjectLayer.Subscribe(const ASubscriber: IObjectLayerNotification);
begin
  if (FSubscribers = nil) then
    FSubscribers := TList<IObjectLayerNotification>.Create;
  FSubscribers.Add(ASubscriber);
end;

procedure TObjectLayer.Unsubscribe(const ASubscriber: IObjectLayerNotification);
begin
  if (FSubscribers <> nil) then
    FSubscribers.Remove(ASubscriber);
end;

The object ID is stored in the ObjectID property (change the type to whatever type you use an an ID). The owner must implement the IObjectLayerNotification interface and call Subscribe on the layer to get notifications. This is a pretty standard observer pattern.

 

1 hour ago, cltom said:

- also, when trying to restrict the movement of layers, how can I access the coordinates of a layer while moving it to mimic a SnapToGrid function?

If you are using the TRubberbandLayer then there's a OnConstrain event where you can examine and modify the move/resize. If you are doing move/resize with some other method then I'll need some information about that.

 

1 hour ago, cltom said:

- coming back to the rubberband-question: how to implement this? Should I create a TRubberband layer via the MouseDown/MouseMove events?

I usually implement rubber-band selection via the TImgView32 mouse events (i.e. I'm not using a layer).

So I manage the selection-in-progress state (usually just the mouse-down position) and any current selection on the form. In the mouse-up handler, I create a rectangle polygon from the mouse-down pos and the mouse-up pos and then either replace the current selection with the new one or merge the two (union), depending on the keyboard shift state. The selection is stored as a polygon. You can also use a polypolygon depending on your needs.

The selection is drawn by a custom layer (visible only when there actually is a selection). The layer has a copy of the polygon and draws a marching ants (btw, try googling "marching ants") animated line using a stipple pattern and a timer.

Here's the Paint method of the layer:

procedure TSelectionLayer.Paint(Buffer: TBitmap32);
begin
  try
    // Update local copy of selection polygon
    UpdateCache;

    if (BitmapEditor.HasSelection) then
    begin
      Buffer.SetStipple(SelectionStipple);
      Buffer.StippleCounter := FSelectionStippleCounter;
      Buffer.StippleStep := 1;

      PolylineXSP(Buffer, FCachedSelection, not SelectionInProgress);
    end;
  except
    // Prevent AV flood due to repaint
    Visible := False;
    raise;
  end;
end;

and the setup and control of the stipple pattern:

constructor TSelectionLayer.Create(ABitmapEditor: TBitmapEditor);
begin
  inherited Create(ABitmapEditor);
  FCacheValid := False;
  FTimer := TTimer.Create(nil);
  FTimer.Interval := 50;
  FTimer.OnTimer := OnTimer;
  FTimer.Enabled := False;
  CreateStipple(FSelectionStipple, $F0F0F0F0, clBlack32, clWhite32);
end;

procedure TSelectionLayer.OnTimer(Sender: TObject);
begin
  // TODO : Remove dependency on Forms unit
  if (not Application.Active) then
    exit;

  FSelectionStippleCounter := FSelectionStippleCounter+1.5;
  if (FSelectionStippleCounter >= Length(SelectionStipple)) then
    FSelectionStippleCounter := FSelectionStippleCounter - Length(SelectionStipple);

  Update(FBitmapRect);
end;

 

  • Like 1

Share this post


Link to post
18 hours ago, Anders Melander said:

I would just store a reference to your object and the object owner in the layer and then notify the owner when the layer is moved.

Something like this:

The object ID is stored in the ObjectID property (change the type to whatever type you use an an ID). The owner must implement the IObjectLayerNotification interface and call Subscribe on the layer to get notifications. This is a pretty standard observer pattern.

 

If you are using the TRubberbandLayer then there's a OnConstrain event where you can examine and modify the move/resize. If you are doing move/resize with some other method then I'll need some information about that.

 

I usually implement rubber-band selection via the TImgView32 mouse events (i.e. I'm not using a layer).

So I manage the selection-in-progress state (usually just the mouse-down position) and any current selection on the form. In the mouse-up handler, I create a rectangle polygon from the mouse-down pos and the mouse-up pos and then either replace the current selection with the new one or merge the two (union), depending on the keyboard shift state. The selection is stored as a polygon. You can also use a polypolygon depending on your needs.

The selection is drawn by a custom layer (visible only when there actually is a selection). The layer has a copy of the polygon and draws a marching ants (btw, try googling "marching ants") animated line using a stipple pattern and a timer.

Here's the Paint method of the layer:

and the setup and control of the stipple pattern:

 

A massive thank you for your support and work! I will dive into it and try to get it implemented! As a quick reaction, again: thank you!!

Share this post


Link to post
On 3/23/2023 at 12:24 AM, Anders Melander said:

This should of course not have been an abstract class.

I have just committed an implementation of the class to the main branch: https://github.com/graphics32/graphics32/commit/aec3713e187d1300b111f0315380a38b50033fef

Would this ObjectLayer class also be the right place to add a TAffineTransformation for rotating or flipping an element? Should then the transformation take place in the Paint event?

Share this post


Link to post
2 hours ago, cltom said:

Would this ObjectLayer class also be the right place to add a TAffineTransformation for rotating or flipping an element? Should then the transformation take place in the Paint event?

Yes, and yes.
See TCustomAffineLayer in Examples\Layers\RotLayer\GR32_RotLayer.pas

Share this post


Link to post

Despite the help even in the form of source code that I could just paste, I am struggling with some fundamentals:

 

On 3/21/2023 at 6:42 PM, Anders Melander said:

If you are using the TRubberbandLayer then there's a OnConstrain event where you can examine and modify the move/resize. 

How to do that? Do I just write a new OnConstrain event that I assign to the TRubberband-Layer?

 

On 3/21/2023 at 6:42 PM, Anders Melander said:

type TObjectLayer = class; TObjectLayerNotification = ( olnDestroy, // Subscribers should remove reference to layer olnPosition // Layer has moved ); ....

Here, I guess I am lacking the fundamentals on how this should work. I can't get this to run. 

 

On 3/21/2023 at 6:42 PM, Anders Melander said:

property ObjectID: TSomeType read FObjectID write FObjectID;

on this one, field definition not allowed after methods or properties, no matter where I put it.

On 3/21/2023 at 6:42 PM, Anders Melander said:

Notify(olnDestroy);

Here it says incompatible types IObjectLayerNotification and TObjectLayernotification.

 

On 3/21/2023 at 6:42 PM, Anders Melander said:

procedure TSelectionLayer.Paint(Buffer: TBitmap32); begin try // Update local copy of selection polygon UpdateCache; if (BitmapEditor.HasSelection) then begin Buffer.SetStipple(SelectionStipple); Buffer.StippleCounter := FSelectionStippleCounter; Buffer.StippleStep := 1; PolylineXSP(Buffer, FCachedSelection, not SelectionInProgress); end; except // Prevent AV flood due to repaint Visible := False; raise; end; end;

should this be a separate class of its own? TSelectionLayer?

 

And another one: in order to save all objects including the background, do I need to flatten all layers? Essentially I guess I have to make a copy of the bitmap and all layers flatten the copies and continue to work with the original one?

 

I was thinking about sharing the project as I think this generic functionality could be a useful template, at this point I it is still rather a fragment though.

Share this post


Link to post
1 hour ago, cltom said:

Do I just write a new OnConstrain event that I assign to the TRubberband-Layer?

Yes. You write an event handler and assign it to the event:

procedure TMyForm.LayerConstrainHandler(Sender: TObject;
  const OldLocation: TFloatRect;
  var NewLocation: TFloatRect;
  DragState: TRBDragState;
  Shift: TShiftState);
begin
  // Snap to nearest 10 pixels if [Shift] is down.
  // Note that the roQuantized layer option provides similar functionality.
  if (ssShift in Shift) then
  begin
    NewLocation.Left := Trunc((NewLocation.Left + 5) / 10) * 10;
    NewLocation.Top := Trunc((NewLocation.Top + 5) / 10) * 10;
    NewLocation.Right := Trunc((NewLocation.Right + 5) / 10) * 10;
    NewLocation.Bottom := Trunc((NewLocation.Bottom + 5) / 10) * 10;
  end;
end;

...

  var Layer := TRubberbandLayer.Create(MyImage.Layers);
  Layer.OnConstrain := LayerConstrainHandler;
  Layer.Options := Layer.Options + [roConstrained];
  
...

 

 

1 hour ago, cltom said:

Here, I guess I am lacking the fundamentals on how this should work. I can't get this to run. 

The code I posted isn't runnable as-is. It's just an example from the top of my head.

 

1 hour ago, cltom said:

on this one, field definition not allowed after methods or properties, no matter where I put it. 

Replace TSomeType with whatever type you use to identify your object.

 

1 hour ago, cltom said:

Here it says incompatible types IObjectLayerNotification and TObjectLayernotification.

Typo on my part. The Notify method should take a TObjectLayernotification as a parameter.

 

1 hour ago, cltom said:

should this be a separate class of its own? TSelectionLayer?

It's just an example of how to draw a marching ants selection in a layer but I suggest you postpone that until you have a better grasp of the fundamentals and have gotten the other stuff working.

I just remembered that it is it's actually possible to animate TRubberBandLayer via the FrameStippleCounter property. For example, if you modify the ImgView_Layers example and place a 100 mS TTimer on the mainform with this code:

procedure TMainForm.Timer1Timer(Sender: TObject);
const
  StippleSize = 4; // The size of the default stipple pattern
begin
  if (RBLayer = nil) or (not RBLayer.Visible) then
    exit;

  var NewStippleCounter := RBLayer.FrameStippleCounter+0.5;

  // Handle overflow
  if (NewStippleCounter >= StippleSize) then
    NewStippleCounter := NewStippleCounter - StippleSize;

  RBLayer.FrameStippleCounter := NewStippleCounter;
  RBLayer.Changed;
end;

 

2 hours ago, cltom said:

And another one: in order to save all objects including the background, do I need to flatten all layers? Essentially I guess I have to make a copy of the bitmap and all layers flatten the copies and continue to work with the original one?

That really depends on your use case. If you just need a bitmap with the final image, then you can use the PaintTo method to draw the flattened image on another bitmap. If you want to save your image as objects then you're on your own; You'll have to write code to save the image state, including layers, to some custom file format.

 

2 hours ago, cltom said:

I was thinking about sharing the project as I think this generic functionality could be a useful template, at this point I it is still rather a fragment though.

Sure. Create a github (or whatever you prefer) repository for it.

Share this post


Link to post

Hej Anders, 

 

thank you again for the dedicated help!

 

As I had some to do some clean-up in another project, this one received a little less attention. But some progress is there:

 

- the save I could just use from the Layers-example 😉

- the OnConstraint works (I was using OldLocation instead of new - along the lines of NewLocation := OldLocation div x etc.

 

So I have quite a bit of work left to do but the project is alive! 😄 

  • Like 1

Share this post


Link to post

Hej,

since some other projects took over, there is quite some delay now. However, I still should make some progress on this, so I put it on github as promised:

 

tomonsight/PMDraw: Symbolic Drawing App (github.com)

 

Needless to say this is not a ready application and it surely reflects all sorts of incompetencies ...

 

Anyway, some issues that I am facing:

- the constraint to the grid is "jumpy"

- I cannot select text

- the functionality to select multiple objects is missing

 

But still, the idea is probably visible of what I am trying to do.

 

Thanks for attention/help, maybe you can spot some easy fixes.

 

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×