Skip to content

Conversation

@SjurdurS
Copy link
Contributor

@SjurdurS SjurdurS commented Jan 24, 2025

This pull request adds a new Custom Shape Tool, called "Shape", to the Level Editor that allows you to quickly place predefined custom shapes into the current level.
Furthermore, this pull request also adds a new right click context menu option called "Save as shape..." which creates a new custom shape based on the current selection.

Custom shapes are saved as regular Elma levels (*.lev) on disk, and are grouped by sub-folders in the root folder for custom shapes. The root folder for custom shapes is called sle_shapes and will be created in the same directory as Elmanager.exe the first time a custom shape is saved.

The custom shape collection is just a folder structure with files, and therefore easily shareable / importable / exportable.

The following components can be added to custom shapes:

  • Polygons (always full polygons, if any vertex is selected on a polygon, the whole polygon is serialized).
  • Grass polygons
  • Objects (Apples, Killers, Flowers)
  • Pictures
  • Textures

The new Custom Shape Tool supports the following actions:

  • Right click to open the Shape Selection dialog
  • Anchoring using num 1-5 like Picture Tool.
  • Transformation
    • Mirroring - Vertical, Horizontal or Both (toggle with num 6)
    • Rotation - num 7 to rotate left, num 9 to rotate right, num 8 to reset rotation
    • Scaling - Resize using (+ / - ) on the keyboard
    • Reset anchoring and transformations: num 0
    • The transformation state is remembered between shapes, so if you rotate one shape, and select a different one, the same settings apply to the new shape as well.

Images

New Shape button, and Shape Selection dialog

Elmanager_6LHlIvoMYi

Shapes are grouped by folders.

Elmanager_ckvefUHXjf

Example shapes for quick testing

Here is an example collection of shapes you can use for trying it out.
Place the following sle_shapes folder in the same folder as the Elmanager.exe
sle_shapes.zip

@Smibu
Copy link
Owner

Smibu commented Jan 25, 2025

Wow nice! I'll take a look today.

Copy link
Owner

@Smibu Smibu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a few comments in the code, but at the same time I'd like to suggest a hopefully simpler implementation:

  • Instead of storing the shapes as JSON files, could they be just normal lev files? This way they would be easily editable, and we wouldn't need custom serialization logic for them.
  • Instead of storing a cached image of the shape, could we implement a LevelControl that inherits from GLControl that is able to directly render a given lev? So a CustomShapeControl would have a reference to a LevelControl instead of a PictureBox. I imagine this might not be a huge task because all the pieces are there already; we just need to glue them together. We could share the main window GL context. This maybe requires updating GLControl to the official version because it seems the current (vendored in repo) version might not support GL context sharing.

Comment on lines 328 to 329
internal static (Vector center, Vector min, Vector max) CalculateBoundingBox(List<Polygon> polygons, List<LevObject> objects,
List<GraphicElement> graphicElements)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this method is not needed: if you construct a Level, it already has a method for computing its bounds.


var (center, _, _) = GeometryUtils.CalculateBoundingBox(clonedPolygons, clonedObjects, clonedGraphicElements);

// Normalize positions
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this normalization required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just felt it would be easier, and more clean that shapes are centered around origin than arbitrary positions.

Also, you need to do this step during translation and rotation, so doing it once before saving, instead of each time the shape is in use reduces the amount of calculations you need to do, unless I'm mistaken?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that makes sense, but I think the normalization should probably be done when loading the shapes, rather than on save. Especially now that the shapes are editable as levs, we cannot assume the coordinates are normalized in the shape (lev) file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I'll look into changing it to on loading instead.


protected GraphicElementDto(GraphicElement element)
{
Type = element.GetType().Name;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid reflection. It's not trim-compatible, and my hope is that some day elmanager exe size could be greatly reduced via trimming as soon as WinForms supports trimming.


public override void Write(Utf8JsonWriter writer, GraphicElementDto value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we should avoid reflection.

@SjurdurS
Copy link
Contributor Author

Thank you looking at my changes, and thanks for the great feedback! :)

Using the LEV format makes sense, especially if they can be rendered directly in the selection dialog, but I have no experience with OpenGL which is why I didn't go down that road.

I think it would make sense to do this in two steps.

  1. First replace the JSON serialization with the LEV format, but keep the images for now.
  2. Then afterwards try to render the levels in a LevelControl.

SjurdurS and others added 3 commits January 25, 2025 20:50
Co-authored-by: Mika Lehtinen <Smibu@users.noreply.github.com>
I haven't removed the support for deserialization of JSON  shapes to help with converting between the old JSON and the new LEV format.
@SjurdurS
Copy link
Contributor Author

I did a quick change to serialize to and from LEV instead of JSON. Using LEV format works well.
I will still need to clean up and remove all the unnecessary code related to the JSON serialization.

I noticed that LEV objects require a start object (I got a random crash during conversion from JSON to LEV). It isn't a problem adding a start object as it is easy to filter them on load, but am I missing any other requirements for a Level to be "valid"?

Here is an updated example gallery should you want to test it.
GalleryWithLevFormat.zip

@Smibu
Copy link
Owner

Smibu commented Jan 26, 2025

I think just the start object and 1 (ground) polygon is required for validity.

By two steps, do you mean two PRs? I would like just this one PR where we do both steps. You can regardless send some intermediate version for people to test in discord if you want to gather early feedback.

@SjurdurS
Copy link
Contributor Author

SjurdurS commented Jan 26, 2025

I think just the start object and 1 (ground) polygon is required for validity.

Then the easiest solution is to limit custom shape creation to when the user has selected at least 1 polygon.
With my JSON implementation you could create shapes out of anything, pictures only, objects only etc. But this is not necessary.
Otherwise you could add some kind of identifiable placeholder polygon that you remove on load, but for now I'd rather just stick with the minimum.

By two steps, do you mean two PRs? I would like just this one PR where we do both steps. You can regardless send some intermediate version for people to test in discord if you want to gather early feedback.

No, I just mean refactoring-wise. Refactor first to use LEV, and get rid of all of the unnecessary JSON serialization related code, and only then try to implement the Level rendering. Since I have 0 experience with GL, I don't know if I will be able to implement the last part :)

@Smibu
Copy link
Owner

Smibu commented Jan 26, 2025

Ah ye ok, that sounds good 👍

If the GL thing turns out difficult, I can take a look at it sometime. The new control might be useful in other contexts also.

@SjurdurS
Copy link
Contributor Author

Ah ye ok, that sounds good 👍

If the GL thing turns out difficult, I can take a look at it sometime. The new control might be useful in other contexts also.

I haven’t started looking at it yet, but I have a question regarding how you see the LevelControl used. If you have a grid like the one I used in the shape selection dialog. Would the grid just consist of individual LevelControls, where each LevelControl has its own shared GL context? How would performance be in such a case?

@Smibu
Copy link
Owner

Smibu commented Jan 27, 2025

The grid would consist of CustomShapeControls (as it already is now?) and a CustomShapeControl would have a reference to a LevelControl (instead of a PictureBox).

So you'd pass the LevelEditorForm's GLControl to ShapeGalleryForm so that it can be further passed to each CustomShapeControl and LevelControl as the shared context.

I wouldn't expect performance problems; GL rendering should be pretty fast and hopefully the GLControl's paint method is only invoked when the control is actually visible (and not e.g. outside of a scroll area etc).

@SjurdurS
Copy link
Contributor Author

I've played around with making a minimal example of a custom GLControl, and trying to just draw a triangle in a form and I can't even get anything drawn on screen.

I tried changing to OpenTK.GLControl throughout, as it has SharedContext defined, and the Editor itself draws as usual. But I just get InvalidOperation errors in the test form/dialog.

Really at a loss for what to do here .. any pointers? :D

@Smibu
Copy link
Owner

Smibu commented Jan 30, 2025

I need to see some code, hard to say otherwise. Maybe push some wip branch to your fork?

@SjurdurS
Copy link
Contributor Author

I need to see some code, hard to say otherwise. Maybe push some wip branch to your fork?

See a very sloppy test implementation at:
2bcb9e9

The dialog is loaded when you trigger Create Custom Shape, but you can move it elsewhere if necessary.

I just wanted to create a minimal working example, where I'm using the shared context to do a basic drawing.
I managed to get it to draw a red background on the dialog, in the "LevelControl", but in the render loop I get nonstop InvalidOperation.

I had to install a different package "OpenTK.GLControl" to get the SharedContext property in GLControl.
Many examples online also refer to a "GraphicsMode" enum and other classes not available in the packages you are using.

@Smibu
Copy link
Owner

Smibu commented Feb 1, 2025

Thanks :) it works with a few changes: d71f140

image

It was just missing the compat profile:

Profile = ContextProfile.Compatability;

(Works also with the timer thing, but probably OnPaint is enough, as shapes are just static drawings)

I had to install a different package "OpenTK.GLControl" to get the SharedContext property in GLControl.

Ye, I mentioned you'll need to use the official newest version

@SjurdurS
Copy link
Contributor Author

SjurdurS commented Feb 2, 2025

Great! I’ll try to get level rendering to work when I have time later this week. The timer thingy was just me being desperate to get something calling as I had problems with the OnPaint method initially.

Copy link
Owner

@Smibu Smibu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates, it's getting closer to finish line :) I added a few more comments here.

Found a bug (imo): if I place a shape, reopen form and cancel, it forgets the last selected shape. I think this used to work so maybe broke in refactoring.


if (!IsHandleCreated)
{
SharedContext = sharedContext; // Set shared context before initialization
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this answers the question:

simply calling a control's constructor does not create the Handle.

So let's remove the if :)

@SjurdurS
Copy link
Contributor Author

SjurdurS commented Mar 6, 2025

Thanks for the updates, it's getting closer to finish line :) I added a few more comments here.

Found a bug (imo): if I place a shape, reopen form and cancel, it forgets the last selected shape. I think this used to work so maybe broke in refactoring.

Yeah I know, it happened after refactoring to storing the state in the _shapeSelection object. I left it for now but I can see if I can fix it.

SjurdurS and others added 10 commits March 6, 2025 20:45
ExtraRendering shouldn't be needed because we have transient elements already and we don't have to render anything extra

Co-authored-by: Mika Lehtinen <Smibu@users.noreply.github.com>
Co-authored-by: Mika Lehtinen <Smibu@users.noreply.github.com>
The handle should never be able to be created at this point anyways
…election after closing Shape Selection From with the Cancel button
@SjurdurS
Copy link
Contributor Author

SjurdurS commented Mar 6, 2025

Thanks for the updates, it's getting closer to finish line :) I added a few more comments here.
Found a bug (imo): if I place a shape, reopen form and cancel, it forgets the last selected shape. I think this used to work so maybe broke in refactoring.

Yeah I know, it happened after refactoring to storing the state in the _shapeSelection object. I left it for now but I can see if I can fix it.

The bug has been fixed here: 3fe32b0

@Smibu Smibu marked this pull request as ready for review March 13, 2025 21:41
Copy link
Owner

@Smibu Smibu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This seems almost ready for merging. Can you update PR description to reflect current state? I will squash merge this, so PR title and description will be used as the commit message.

Co-authored-by: Mika Lehtinen <Smibu@users.noreply.github.com>
@SjurdurS
Copy link
Contributor Author

Thanks! This seems almost ready for merging. Can you update PR description to reflect current state? I will squash merge this, so PR title and description will be used as the commit message.

I've updated the pull request description (the first message in this pull request). Let me know if I should change anything :)

@Smibu
Copy link
Owner

Smibu commented Mar 15, 2025

Big thanks and congrats for getting this over the finish line. :)

I will create a build later today and announce in discord.

@Smibu Smibu merged commit 5ee3333 into Smibu:master Mar 15, 2025
2 checks passed
@SjurdurS
Copy link
Contributor Author

Thank you for all your help! Very much appreciated the time you put in to helping me get this finished :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants