From 494b31895c565fcabc90fb21bd487f0f170ce609 Mon Sep 17 00:00:00 2001 From: Maximilian Seitz Date: Mon, 24 Mar 2025 23:47:51 +0100 Subject: [PATCH 1/8] Logically separate layer-instances and layer-definitions, so that a variable number of instances can exist per definition --- app/assets/tpl/editLayerDefs.html | 4 +- src/electron.renderer/LevelTimeline.hx | 65 ++-- src/electron.renderer/data/Definitions.hx | 14 +- src/electron.renderer/data/Level.hx | 64 ++-- src/electron.renderer/data/Project.hx | 16 +- src/electron.renderer/data/def/LayerDef.hx | 2 + .../data/inst/LayerInstance.hx | 59 +++- src/electron.renderer/display/LevelRender.hx | 101 ++++-- src/electron.renderer/display/WorldRender.hx | 10 +- src/electron.renderer/exporter/Tiled.hx | 5 +- src/electron.renderer/importer/OgmoLoader.hx | 3 +- src/electron.renderer/page/Editor.hx | 326 +++++++++--------- src/electron.renderer/ui/LevelInstanceForm.hx | 5 +- src/electron.renderer/ui/ProjectSaver.hx | 16 +- .../modal/dialog/MoveEntitiesBetweenLayers.hx | 5 +- .../ui/modal/panel/EditAllAutoLayerRules.hx | 5 +- .../ui/modal/panel/EditLayerDefs.hx | 54 +-- .../ui/palette/IntGridPalette.hx | 2 +- 18 files changed, 401 insertions(+), 355 deletions(-) diff --git a/app/assets/tpl/editLayerDefs.html b/app/assets/tpl/editLayerDefs.html index 5c9a49194..569e52e49 100644 --- a/app/assets/tpl/editLayerDefs.html +++ b/app/assets/tpl/editLayerDefs.html @@ -320,11 +320,11 @@

-
+
-
+
-
+ + +
    + + +
    + Each LEVEL in your project has LAYERS, as seen below. The layer's content can be of various types, including Image Tiles or Integer values. +
    + + +
    + +
    General layer settings
    +
    +
    + + + Each layer in a level has a project-wide definition it refers to. + +
    +
    + + +
    + +
    + + +
    +
    + +
    +
    + + +
    Display options
    +
    +
    + +
    +
    + +
    +
    + + + +
    diff --git a/app/assets/tpl/help.html b/app/assets/tpl/help.html index 13f39680b..d18c91ed6 100644 --- a/app/assets/tpl/help.html +++ b/app/assets/tpl/help.html @@ -99,7 +99,8 @@

    General commands

    Sections

    %OpenProjectPanel%
    Open "Project" panel
    -
    %OpenLayerPanel%
    Open "Layers" panel
    +
    %OpenLayerInstancePanel%
    Open "Layer Instances" panel
    +
    %OpenLayerDefPanel%
    Open "Layer Definitions" panel
    %OpenEntityPanel%
    Open "Entities" panel
    %OpenEnumPanel%
    Open "Enumerations" panel
    %OpenTilesetPanel%
    Open "Tileset" panel
    diff --git a/app/assets/tpl/pages/editor.html b/app/assets/tpl/pages/editor.html index 8efa4af79..b4e1b1107 100644 --- a/app/assets/tpl/pages/editor.html +++ b/app/assets/tpl/pages/editor.html @@ -13,7 +13,8 @@ - + + diff --git a/src/electron.renderer/App.hx b/src/electron.renderer/App.hx index ce24bebc2..9ca7c2d0d 100644 --- a/src/electron.renderer/App.hx +++ b/src/electron.renderer/App.hx @@ -628,7 +628,8 @@ class App extends dn.Process { case C_MoveLevelToPreviousWorldLayer: case C_MoveLevelToNextWorldLayer: case C_OpenProjectPanel: - case C_OpenLayerPanel: + case C_OpenLayerInstancePanel: + case C_OpenLayerDefPanel: case C_OpenEntityPanel: case C_OpenEnumPanel: case C_OpenTilesetPanel: diff --git a/src/electron.renderer/EditorTypes.hx b/src/electron.renderer/EditorTypes.hx index f7cb63b0f..b145871f4 100644 --- a/src/electron.renderer/EditorTypes.hx +++ b/src/electron.renderer/EditorTypes.hx @@ -45,6 +45,8 @@ enum GlobalEvent { LayerRuleGroupSorted; LayerRuleGroupCollapseChanged(rg:data.def.AutoLayerRuleGroupDef); + LayerInstanceAdded(li:data.inst.LayerInstance); + LayerInstanceRemoved(li:data.inst.LayerInstance); LayerInstanceSelected(li:data.inst.LayerInstance); LayerInstanceEditedByTool(li:data.inst.LayerInstance); LayerInstanceChangedGlobally(li:data.inst.LayerInstance); @@ -221,6 +223,8 @@ enum ClipboardType { CRuleGroup; CRule; + + CLayerInstance; } typedef CachedIID = { @@ -280,7 +284,8 @@ enum AppCommand { @k("ctrl pageup, shift pageup") C_MoveLevelToNextWorldLayer; @k("p") C_OpenProjectPanel; - @k("l") C_OpenLayerPanel; + @k("l") C_OpenLayerInstancePanel; + @k("d") C_OpenLayerDefPanel; @k("e") C_OpenEntityPanel; @k("u") C_OpenEnumPanel; @k("t") C_OpenTilesetPanel; diff --git a/src/electron.renderer/data/Clipboard.hx b/src/electron.renderer/data/Clipboard.hx index 277cfe514..9fb97f23c 100644 --- a/src/electron.renderer/data/Clipboard.hx +++ b/src/electron.renderer/data/Clipboard.hx @@ -167,6 +167,10 @@ class Clipboard { case CFieldDef: var json : ldtk.Json.FieldDefJson = jsonObj; 'Field definition "${json.identifier}"'; + + case CLayerInstance: + var json : ldtk.Json.LayerInstanceJson = jsonObj; + 'Layer instance "${json.__identifier}"'; } } diff --git a/src/electron.renderer/data/Level.hx b/src/electron.renderer/data/Level.hx index 4e2760bae..303d5c6b9 100644 --- a/src/electron.renderer/data/Level.hx +++ b/src/electron.renderer/data/Level.hx @@ -578,27 +578,61 @@ class Level { return ld!=null ? getLayerInstances(ld) : []; } - - public function sortLayerInstances(from:Int, to:Int) { + public function sortLayerInstances(from:Int, to:Int) : Null { if( from<0 || from>=layerInstances.length || from==to ) - return; + return null; if( to<0 || to>=layerInstances.length ) - return; + return null; var moved = layerInstances.splice(from,1)[0]; layerInstances.insert(to, moved); - invalidateJsonCache(); + return moved; } + public function createLayerInstance(layerDef:data.def.LayerDef, ?id:String) : data.inst.LayerInstance { + var identifier = _project.fixUniqueIdStr(id==null ? layerDef.identifier : id, (id)->isLayerNameUnique(id)); - function createLayerInstance(ld:data.def.LayerDef) : data.inst.LayerInstance { - var li = new data.inst.LayerInstance(_project, this.uid, ld.uid, _project.generateUniqueId_UUID(), ld.identifier); + var li = new data.inst.LayerInstance(_project, this.uid, layerDef.uid, _project.generateUniqueId_UUID(), identifier); layerInstances.push(li); return li; } + public function duplicateLayerInstance(li:data.inst.LayerInstance, ?baseName:String) : Null { + return pasteLayerInstance( Clipboard.createTemp(CLayerInstance, li.toJson()), li, baseName ); + } + + public function pasteLayerInstance(c:Clipboard, ?after:data.inst.LayerInstance, ?baseName:String) : Null { + if( !c.is(CLayerInstance) ) + return null; + + var json : ldtk.Json.LayerInstanceJson = c.getParsedJson(); + var copy = data.inst.LayerInstance.fromJson( _project, json ); + copy.iid = _project.generateUniqueId_UUID(); + copy.identifier = _project.fixUniqueIdStr(baseName==null ? json.__identifier : baseName, (id)->isLayerNameUnique(id)); + + if( after!=null ) + layerInstances.insert( dn.Lib.getArrayIndex(after, layerInstances)+1, copy ); + else + layerInstances.push(copy); + _project.tidy(); + return copy; + } + + public function removeLayerInstance(li:data.inst.LayerInstance) { + layerInstances.remove(li); + } + + + public function isLayerNameUnique(id:String, ?exclude:data.inst.LayerInstance) { + var id = Project.cleanupIdentifier(id, _project.identifierStyle); + for(li in layerInstances) + if( li.identifier==id && li!=exclude ) + return false; + return true; + } + /** diff --git a/src/electron.renderer/display/LevelRender.hx b/src/electron.renderer/display/LevelRender.hx index 44890e078..d7ac57af9 100644 --- a/src/electron.renderer/display/LevelRender.hx +++ b/src/electron.renderer/display/LevelRender.hx @@ -149,6 +149,19 @@ class LevelRender extends dn.Process { li.applyAllRules(); invalidateAll(); + case LayerInstanceAdded(li): + // TODO: check if 'LayerInstancesSorted' should be repliacted here + invalidateLayer(li); + + case LayerInstanceRemoved(li): + if( layerRenders.exists(li.layerDefUid) ) { + var lrs = layerRenders.get(li.layerDefUid); + if ( lrs.exists(li.iid) ) { + lrs.get(li.iid).dispose(); + lrs.remove(li.iid); + } + } + case LayerInstanceVisiblityChanged(li): applyLayerVisibility(li); diff --git a/src/electron.renderer/display/WorldRender.hx b/src/electron.renderer/display/WorldRender.hx index 215059410..8d05f7147 100644 --- a/src/electron.renderer/display/WorldRender.hx +++ b/src/electron.renderer/display/WorldRender.hx @@ -278,6 +278,12 @@ class WorldRender extends dn.Process { case LayerDefIntGridValueRemoved(defUid,value,used): + case LayerInstanceAdded(li): + invalidateLevelRender(li.level); + + case LayerInstanceRemoved(li): + invalidateLevelRender(li.level); + case LayerInstanceSelected(curLi): updateEdgeLayersOpacity(); diff --git a/src/electron.renderer/misc/JsTools.hx b/src/electron.renderer/misc/JsTools.hx index 043fb92ef..e7191bf58 100644 --- a/src/electron.renderer/misc/JsTools.hx +++ b/src/electron.renderer/misc/JsTools.hx @@ -64,12 +64,6 @@ class JsTools { Sortable.create( jSortable.get(0), options); } - public static function resetSortable(jSortable:js.jquery.JQuery) { - jSortable.removeClass("sortable"); - jSortable.removeClass("customHandle"); - jSortable.removeClass("onlyDraggables"); - } - public static function createValuesSelect(?jSelect:js.jquery.JQuery, cur:Null, allValues:Array, allowNull:Bool, ?def:T, ?printer:T->String, onSelect:T->Void) { if( jSelect==null ) diff --git a/src/electron.renderer/page/Editor.hx b/src/electron.renderer/page/Editor.hx index c771a0b7a..c5a414ac6 100644 --- a/src/electron.renderer/page/Editor.hx +++ b/src/electron.renderer/page/Editor.hx @@ -183,8 +183,12 @@ class Editor extends Page { App.ME.executeAppCommand(C_OpenLevelPanel); }); - jMainPanel.find("button.editLayers").click( function(_) { - App.ME.executeAppCommand(C_OpenLayerPanel); + jMainPanel.find("button.editLayerInstances").click( function(_) { + App.ME.executeAppCommand(C_OpenLayerInstancePanel); + }); + + jMainPanel.find("button.editLayerDefs").click( function(_) { + App.ME.executeAppCommand(C_OpenLayerDefPanel); }); jMainPanel.find("button.editEntities").click( function(_) { @@ -854,7 +858,13 @@ class Editor extends Page { else new ui.modal.panel.EditProject(); - case C_OpenLayerPanel: + case C_OpenLayerInstancePanel: + if( ui.Modal.isOpen(ui.modal.panel.EditLayerInstances) ) + ui.Modal.closeAll(); + else + new ui.modal.panel.EditLayerInstances(); + + case C_OpenLayerDefPanel: if( ui.Modal.isOpen(ui.modal.panel.EditLayerDefs) ) ui.Modal.closeAll(); else @@ -936,15 +946,21 @@ class Editor extends Page { return allLayerTools.get( curLayerIid ); } + function deleteLayerTool(layerInstanceIid:String) { + if( allLayerTools.exists(layerInstanceIid) ) { + allLayerTools.get(layerInstanceIid).destroy(); + allLayerTools.remove(layerInstanceIid); + return true; + } + + return false; + } + function deleteLayerTools(layerDefUid:Int) { var wasDeleted = false; for( li in curLevel.getLayerInstances(layerDefUid) ) { - if( allLayerTools.exists(li.iid) ) { - allLayerTools.get(li.iid).destroy(); - allLayerTools.remove(li.iid); - wasDeleted = true; - } + wasDeleted = deleteLayerTool(li.iid) || wasDeleted; } return wasDeleted; @@ -2049,9 +2065,12 @@ class Editor extends Page { case LayerRuleGroupChangedActiveState(rg): extra = rg.uid; case LayerRuleGroupSorted: case LayerRuleGroupCollapseChanged(rg): extra = rg.uid; + case LayerInstanceAdded(li): extra = li.iid; + case LayerInstanceRemoved(li): extra = li.iid; case LayerInstanceSelected(li): extra = li.iid; case LayerInstanceChangedGlobally(li): extra = li.iid; case LayerInstanceVisiblityChanged(li): extra = li.iid; + case LayerInstancesSorted(l): extra = l.uid; case LayerInstancesRestoredFromHistory(lis): extra = lis.map( li->li.iid ).join(","); case LayerInstanceTilesetChanged(li): extra = li.iid; case AutoLayerRenderingChanged(lis): extra = lis.map( li->li.iid ).join(","); @@ -2170,6 +2189,8 @@ class Editor extends Page { invalidateAllLevelsCache(); case LayerRuleGroupSorted: invalidateAllLevelsCache(); case LayerRuleGroupCollapseChanged(rg): + case LayerInstanceAdded(li): invalidateLevelCache(li.level); + case LayerInstanceRemoved(li): invalidateLevelCache(li.level); case LayerInstanceSelected(li): case LayerInstanceEditedByTool(li): invalidateLevelCache(li.level); case LayerInstanceChangedGlobally(li): invalidateLevelCache(li.level); @@ -2392,6 +2413,18 @@ class Editor extends Page { selectWorldDepth(l.worldDepth); l.invalidateCachedError(); + case LayerInstanceAdded(li): + if( curLevel==li.level && curLayerIid==null ) + selectLayerInstance( li ); + updateTool(); + updateGuide(); + updateLayerList(); + + case LayerInstanceRemoved(li): + deleteLayerTool(li.iid); + updateLayerList(); + updateTool(); + case LayerInstancesRestoredFromHistory(_), LevelRestoredFromHistory(_): selectionTool.clear(); clearSpecialTool(); @@ -2764,9 +2797,6 @@ class Editor extends Page { jLi.addClass("layer"); JsTools.applyListCustomColor(jLi, ld.uiColor, active); - if ( curTag==null ) - jLi.addClass("draggable"); - if( active ) jLi.addClass("active"); @@ -2855,32 +2885,21 @@ class Editor extends Page { iconId: "edit", cb: ()->{ selectLayerInstance(li); - App.ME.executeAppCommand(C_OpenLayerPanel); + App.ME.executeAppCommand(C_OpenLayerInstancePanel); + }, + }, + { + label: L.t._("Edit layer definition"), + iconId: "settings", + cb: ()->{ + selectLayerInstance(li); + App.ME.executeAppCommand(C_OpenLayerDefPanel); }, } ]; ui.modal.ContextMenu.attachTo(jLi, false, actions); } - if ( curTag==null ) { - // Make layer list sortable (only when no tags are selected) - JsTools.makeSortable( - jLayerList, - (ev)->{ - // Offset oldIndex and newIndex by the first layer, - // since other elements may be present in the jLayerList (e.g. the filters). - var firstLayerIndex = jLayerList.find(".layer").index(); - - curLevel.sortLayerInstances( ev.oldIndex-firstLayerIndex, ev.newIndex-firstLayerIndex ); - ge.emit(LayerInstancesSorted(curLevel)); - curLevelTimeline.saveFullLevelState(); - }, - { onlyDraggables: true } - ); - } else { - JsTools.resetSortable(jLayerList); - } - updateLayerVisibilities(); } diff --git a/src/electron.renderer/ui/CommandPalette.hx b/src/electron.renderer/ui/CommandPalette.hx index 0f2720c26..f5c795136 100644 --- a/src/electron.renderer/ui/CommandPalette.hx +++ b/src/electron.renderer/ui/CommandPalette.hx @@ -14,6 +14,7 @@ enum ElementCategory { SE_Definition; SE_World; SE_Level; + SE_Layer; SE_Entity; } @@ -165,36 +166,50 @@ class CommandPalette { onPick: ()->editor.selectLevel(l, true), }); - // Entities - for(li in l.layerInstances) - for(ei in li.entityInstances) { - var searchElem : SearchElement = { - id: ei.iid, - cat: SE_Entity, - desc: ei.def.identifier, + // Layers + for(li in l.layerInstances) { + allElements.push({ + id: li.iid, + cat: SE_Layer, + desc: li.identifier, ctxDesc: l.identifier, keywords: [], onPick: ()->{ editor.selectLevel(l, true); - var b = editor.levelRender.bleepEntity(ei); - b.delayS = 0.2; - b.remainCount = 5; + editor.selectLayerInstance(li); + }, + }); + + // Entities + for(ei in li.entityInstances) { + var searchElem : SearchElement = { + id: ei.iid, + cat: SE_Entity, + desc: ei.def.identifier, + ctxDesc: l.identifier, + keywords: [], + onPick: ()->{ + editor.selectLevel(l, true); + var b = editor.levelRender.bleepEntity(ei); + b.delayS = 0.2; + b.remainCount = 5; + } } - } - allElements.push(searchElem); - - // Entity fields - for(fi in ei.fieldInstances) { - if( !fi.def.searchable ) - continue; - for(i in 0...fi.getArrayLength()) { - if( fi.valueIsNull(i) ) + allElements.push(searchElem); + + // Entity fields + for(fi in ei.fieldInstances) { + if( !fi.def.searchable ) continue; - searchElem.desc += "."+fi.getForDisplay(i); - searchElem.keywords.push( fi.getForDisplay(i) ); + for(i in 0...fi.getArrayLength()) { + if( fi.valueIsNull(i) ) + continue; + searchElem.desc += "."+fi.getForDisplay(i); + searchElem.keywords.push( fi.getForDisplay(i) ); + } } - } + } } } } @@ -208,6 +223,7 @@ class CommandPalette { case SE_Definition: "definition"; case SE_World: "world"; case SE_Level: "level"; + case SE_Layer: "layer"; case SE_Entity: "entity"; }); e.keywords.push(e.desc.toLowerCase()); @@ -261,6 +277,7 @@ class CommandPalette { case SE_Definition: "project"; case SE_World: "world"; case SE_Level: "level"; + case SE_Layer: "layer"; case SE_Entity: "entity"; } jElement.append(''); diff --git a/src/electron.renderer/ui/modal/Dialog.hx b/src/electron.renderer/ui/modal/Dialog.hx index 7b07566bd..b1851d1bf 100644 --- a/src/electron.renderer/ui/modal/Dialog.hx +++ b/src/electron.renderer/ui/modal/Dialog.hx @@ -89,29 +89,32 @@ class Dialog extends ui.Modal { return b; } - public function addConfirm(cb:Void->Void) { + public function addConfirm(cb:Void->Void) : js.jquery.JQuery { var b = addButton(L.t._("Confirm"), "confirm", function() { cb(); close(); }); b.detach(); jButtons.prepend(b); + return b; } - public function addCancel(?cb:Void->Void) { + public function addCancel(?cb:Void->Void) : js.jquery.JQuery { var b = addButton(L.t._("Cancel"), "cancel", function() { if( cb!=null ) cb(); close(); }); + return b; } - public function addClose(?cb:Void->Void) { + public function addClose(?cb:Void->Void) : js.jquery.JQuery { var b = addButton(L.t._("Close"), "confirm", function() { if( cb!=null ) cb(); close(); }); + return b; } override function update() { diff --git a/src/electron.renderer/ui/modal/dialog/SelectLayerDef.hx b/src/electron.renderer/ui/modal/dialog/SelectLayerDef.hx new file mode 100644 index 000000000..79e092b50 --- /dev/null +++ b/src/electron.renderer/ui/modal/dialog/SelectLayerDef.hx @@ -0,0 +1,57 @@ +package ui.modal.dialog; + +class SelectLayerDef extends ui.modal.Dialog { + private var jDefSelect : js.jquery.JQuery; + private var jConfirm : js.jquery.JQuery; + + public function new(?target:js.jquery.JQuery, onConfirm:data.def.LayerDef->Void) { + super(target, "selectLayerDef"); + + // LayerDef select + jDefSelect = new J("