diff --git a/src/electron.renderer/EditorTypes.hx b/src/electron.renderer/EditorTypes.hx index 542520abc..e44237f61 100644 --- a/src/electron.renderer/EditorTypes.hx +++ b/src/electron.renderer/EditorTypes.hx @@ -18,6 +18,7 @@ enum GlobalEvent { LevelRestoredFromHistory(level:data.Level); LevelJsonCacheInvalidated(level:data.Level); + WorldLevelSelectionChanged; WorldLevelMoved(level:data.Level, isFinal:Bool, prevNeighbourIids:Null>); WorldSettingsChanged; WorldCreated(w:data.World); @@ -309,4 +310,4 @@ enum DebugFlag { F_MainDebug; F_IntGridUseCounts; F_ProjectImgCache; -} \ No newline at end of file +} diff --git a/src/electron.renderer/WorldTool.hx b/src/electron.renderer/WorldTool.hx index 6800ad723..dae9fca97 100644 --- a/src/electron.renderer/WorldTool.hx +++ b/src/electron.renderer/WorldTool.hx @@ -13,6 +13,11 @@ class WorldTool extends dn.Process { var origin : Coords; var clicked = false; var dragStarted = false; + var boxSelecting = false; + var activeDragLevels : Array = []; + var dragOriginXs : Map = new Map(); + var dragOriginYs : Map = new Map(); + var dragInitialNeighbours : Map> = new Map(); var worldMode(get,never) : Bool; inline function get_worldMode() return editor.worldMode; var tmpRender : h2d.Graphics; @@ -156,6 +161,11 @@ class WorldTool extends dn.Process { origin = m; initialNeighbours = null; dragStarted = false; + boxSelecting = false; + activeDragLevels = []; + dragOriginXs = new Map(); + dragOriginYs = new Map(); + dragInitialNeighbours = new Map(); clicked = true; if( !worldMode && editor.curLevel.inBoundsWorld(m.worldX,m.worldY) ) clickedLevel = null; @@ -166,6 +176,14 @@ class WorldTool extends dn.Process { clickedLevel = null; if( clickedLevel!=null ) { + if( worldMode && App.ME.isCtrlCmdDown() && !App.ME.isAltDown() ) { + editor.toggleWorldLevelSelection(clickedLevel); + clickedLevel = null; + clicked = false; + ev.cancel = true; + return; + } + levelOriginX = clickedLevel.worldX; levelOriginY = clickedLevel.worldY; ev.cancel = true; @@ -173,23 +191,36 @@ class WorldTool extends dn.Process { initialNeighbours = clickedLevel.getNeighboursIids(); // Pick level - editor.selectLevel(clickedLevel); + if( worldMode && editor.isWorldLevelSelected(clickedLevel) && editor.countWorldLevelSelection()>1 ) + editor.selectLevel(clickedLevel, false, true); + else if( worldMode ) + editor.replaceWorldLevelSelection([clickedLevel], clickedLevel); + else + editor.selectLevel(clickedLevel); } } public function onMouseUp(m:Coords) { tmpRender.clear(); + if( boxSelecting ) { + var selected = getLevelsInSelectionBox(m); + if( App.ME.isCtrlCmdDown() ) + editor.toggleWorldLevelsInSelection(selected); + else + editor.replaceWorldLevelSelection(selected); + } + else if( clicked && clickedLevel==null && worldMode && !App.ME.isCtrlCmdDown() && origin.getPageDist(m)<=getDragThreshold() ) + editor.clearWorldLevelSelection(); + if( clickedLevel!=null ) { if( dragStarted ) { // Drag complete - var initialX = clickedLevel.worldX; - var initialY = clickedLevel.worldY; - switch curWorld.worldLayout { case Free, GridVania: curWorld.applyAutoLevelIdentifiers(); - editor.ge.emit( WorldLevelMoved(clickedLevel, true, initialNeighbours) ); + for(l in activeDragLevels) + editor.ge.emit( WorldLevelMoved(l, true, dragInitialNeighbours.get(l.uid)) ); case LinearHorizontal: var i = ui.vp.LevelSpotPicker.getLinearInsertPoint(project, curWorld, m, clickedLevel, levelOriginX); @@ -227,14 +258,29 @@ class WorldTool extends dn.Process { // Cleanup clickedLevel = null; dragStarted = false; + boxSelecting = false; + activeDragLevels = []; + dragOriginXs = new Map(); + dragOriginYs = new Map(); + dragInitialNeighbours = new Map(); clicked = false; } inline function getLevelSnapDist() return App.ME.isShiftDown() || App.ME.isCtrlCmdDown() ? 0 : project.getSmartLevelGridSize() / ( editor.camera.adjustedZoom * 0.4 ); + function willOverlapAnyDraggedLevel(cur:data.Level, newWorldX:Int, newWorldY:Int) { + for(l in curWorld.levels) { + if( l==cur || activeDragLevels.contains(l) ) + continue; + if( dn.Lib.rectangleOverlaps(newWorldX, newWorldY, cur.pxWid, cur.pxHei, l.worldX, l.worldY, l.pxWid, l.pxHei) ) + return true; + } + return false; + } + inline function snapLevelX(cur:data.Level, offset:Int, at:Int) { if( M.fabs(cur.worldX + offset - at) <= getLevelSnapDist() ) { - if( cur.willOverlapAnyLevel(at-offset, cur.worldY) ) + if( willOverlapAnyDraggedLevel(cur, at-offset, cur.worldY) ) return false; else { cur.worldX = at-offset; @@ -247,7 +293,7 @@ class WorldTool extends dn.Process { inline function snapLevelY(l:data.Level, offset:Int, with:Int) { if( M.fabs(l.worldY + offset - with) <= getLevelSnapDist() ) { - if( l.willOverlapAnyLevel(l.worldX, with-offset) ) + if( willOverlapAnyDraggedLevel(l, l.worldX, with-offset) ) return false; else { l.worldY = with-offset; @@ -294,6 +340,7 @@ class WorldTool extends dn.Process { } if( allow ) { dragStarted = true; + boxSelecting = clickedLevel==null; ev.cancel = true; // if( clickedLevel!=null ) // editor.selectLevel(clickedLevel); @@ -301,12 +348,42 @@ class WorldTool extends dn.Process { if( clickedLevel!=null && App.ME.isAltDown() && App.ME.isCtrlCmdDown() ) { var copy = curWorld.duplicateLevel(clickedLevel); editor.ge.emit( LevelAdded(copy) ); - editor.selectLevel(copy); + editor.replaceWorldLevelSelection([copy], copy); clickedLevel = copy; } + + activeDragLevels = []; + if( clickedLevel!=null ) { + var canMoveGroup = switch curWorld.worldLayout { + case Free, GridVania: true; + case LinearHorizontal, LinearVertical: false; + } + if( canMoveGroup && editor.isWorldLevelSelected(clickedLevel) && editor.countWorldLevelSelection()>1 ) + activeDragLevels = editor.getWorldLevelSelection(); + else + activeDragLevels = [clickedLevel]; + + dragOriginXs = new Map(); + dragOriginYs = new Map(); + dragInitialNeighbours = new Map(); + for(l in activeDragLevels) { + dragOriginXs.set(l.uid, l.worldX); + dragOriginYs.set(l.uid, l.worldY); + dragInitialNeighbours.set(l.uid, l.getNeighboursIids()); + } + levelOriginX = dragOriginXs.get(clickedLevel.uid); + levelOriginY = dragOriginYs.get(clickedLevel.uid); + } } } + if( boxSelecting && dragStarted ) { + renderSelectionBox(m); + App.ME.requestCpu(); + ev.cancel = true; + return; + } + // Drag if( clickedLevel!=null && dragStarted ) { // Init tmpRender render @@ -326,8 +403,6 @@ class WorldTool extends dn.Process { case LinearHorizontal: false; case LinearVertical: true; } - var initialX = clickedLevel.worldX; - var initialY = clickedLevel.worldY; if( allowX ) clickedLevel.worldX = levelOriginX + ( m.worldX - origin.worldX ); else @@ -349,7 +424,7 @@ class WorldTool extends dn.Process { // Snap to other levels for(l in curWorld.levels) { - if( l==clickedLevel ) + if( l==clickedLevel || activeDragLevels.contains(l) ) continue; if( clickedLevel.getBoundsDist(l) > getLevelSnapDist() ) @@ -393,21 +468,68 @@ class WorldTool extends dn.Process { tmpRender.lineTo(i.coord, curWorld.getWorldHeight(clickedLevel)+100); } - case LinearVertical: - var i = ui.vp.LevelSpotPicker.getLinearInsertPoint(project, curWorld, m, clickedLevel, levelOriginY); - if( i!=null ) { - tmpRender.moveTo(-100, i.coord); - tmpRender.lineTo(curWorld.getWorldWidth(clickedLevel)+100, i.coord); - } + case LinearVertical: + var i = ui.vp.LevelSpotPicker.getLinearInsertPoint(project, curWorld, m, clickedLevel, levelOriginY); + if( i!=null ) { + tmpRender.moveTo(-100, i.coord); + tmpRender.lineTo(curWorld.getWorldWidth(clickedLevel)+100, i.coord); + } + } + + var moveDx = clickedLevel.worldX - dragOriginXs.get(clickedLevel.uid); + var moveDy = clickedLevel.worldY - dragOriginYs.get(clickedLevel.uid); + if( activeDragLevels.length>1 ) { + for(l in activeDragLevels) { + if( l==clickedLevel ) + continue; + l.worldX = dragOriginXs.get(l.uid) + moveDx; + l.worldY = dragOriginYs.get(l.uid) + moveDy; + } } // Refresh render - editor.ge.emit( WorldLevelMoved(clickedLevel, false, null) ); + for(l in activeDragLevels) + editor.ge.emit( WorldLevelMoved(l, false, null) ); App.ME.requestCpu(); ev.cancel = true; } } + function getSelectionBox(m:Coords) { + var left = M.imin(origin.worldX, m.worldX); + var top = M.imin(origin.worldY, m.worldY); + var right = M.imax(origin.worldX, m.worldX); + var bottom = M.imax(origin.worldY, m.worldY); + return { + x: left, + y: top, + wid: right-left, + hei: bottom-top, + }; + } + + function renderSelectionBox(m:Coords) { + var r = getSelectionBox(m); + tmpRender.clear(); + tmpRender.lineStyle(2/editor.camera.adjustedZoom, 0x72feff, 0.8); + tmpRender.beginFill(0x72feff, 0.12); + tmpRender.drawRect(r.x, r.y, r.wid, r.hei); + tmpRender.endFill(); + } + + function getLevelsInSelectionBox(m:Coords) { + var r = getSelectionBox(m); + var out : Array = []; + if( r.wid<=0 || r.hei<=0 ) + return out; + + for(l in curWorld.levels) + if( l.worldDepth==editor.curWorldDepth && dn.Lib.rectangleOverlaps(r.x, r.y, r.wid, r.hei, l.worldX, l.worldY, l.pxWid, l.pxHei) ) + out.push(l); + + return out; + } + function getLevelAt(worldX:Int, worldY:Int, ?except:data.Level) { var i = curWorld.levels.length-1; var l : data.Level = null; @@ -421,4 +543,4 @@ class WorldTool extends dn.Process { return null; } -} \ No newline at end of file +} diff --git a/src/electron.renderer/display/LevelRender.hx b/src/electron.renderer/display/LevelRender.hx index 2f4dc0fbd..95968eca1 100644 --- a/src/electron.renderer/display/LevelRender.hx +++ b/src/electron.renderer/display/LevelRender.hx @@ -116,6 +116,8 @@ class LevelRender extends dn.Process { updateGridPos(); invalidateGrid(); + case WorldLevelSelectionChanged: + case ProjectSaved, BeforeProjectSaving: case ProjectSelected: diff --git a/src/electron.renderer/display/WorldRender.hx b/src/electron.renderer/display/WorldRender.hx index f7c5e27d9..afb122f7a 100644 --- a/src/electron.renderer/display/WorldRender.hx +++ b/src/electron.renderer/display/WorldRender.hx @@ -184,6 +184,10 @@ class WorldRender extends dn.Process { } } + case WorldLevelSelectionChanged: + for(l in curWorld.levels) + getWorldLevel(l).boundsInvalidated = true; + case ProjectSaved: invalidateAllLevelFields(); invalidateAllLevelIdentifiers(); @@ -995,10 +999,12 @@ class WorldRender extends dn.Process { var wl = getWorldLevel(l); if( wl!=null ) { wl.outline.clear(); - if( !settings.v.showDetails ) + var isCur = l==editor.curLevel; + var isSelected = editor.isWorldLevelSelected(l); + if( !settings.v.showDetails && !isSelected ) return; - var thick = (l==editor.curLevel?3:2)*camera.pixelRatio / camera.adjustedZoom; + var thick = (isCur?3:2)*camera.pixelRatio / camera.adjustedZoom; var c : dn.Col = l.getSmartColor(false); var error = l.getFirstError(); @@ -1007,6 +1013,11 @@ class WorldRender extends dn.Process { c = 0xff0000; } + if( isSelected && !isCur && error==NoError ) { + thick = 3*camera.pixelRatio / camera.adjustedZoom; + c = 0x72feff; + } + var pad = 1; wl.outline.beginFill(c); wl.outline.drawRect(pad, pad, l.pxWid-pad*2, thick); // top diff --git a/src/electron.renderer/page/Editor.hx b/src/electron.renderer/page/Editor.hx index 476a38120..d2334c1c2 100644 --- a/src/electron.renderer/page/Editor.hx +++ b/src/electron.renderer/page/Editor.hx @@ -31,6 +31,7 @@ class Editor extends Page { public var curWorldIid : String; public var curLevelId : Int; var curLayerDefUid : Int; + var worldLevelSelection : Map = new Map(); // Tools public var worldTool : WorldTool; @@ -1353,6 +1354,7 @@ class Editor extends Page { if( worldMode ) setWorldMode(false); + clearWorldLevelSelection(false); curWorldIid = w.iid; invalidateCachedLevelErrors(); @@ -1369,18 +1371,23 @@ class Editor extends Page { } - public function selectLevel(l:data.Level, fitView=false) { + public function selectLevel(l:data.Level, fitView=false, keepWorldLevelSelection=false) { if( curTool.isRunning() ) curTool.stopUsing(getMouse()); if( l._world!=curWorld ) selectWorld(l._world); + if( !keepWorldLevelSelection ) + clearWorldLevelSelection(); + if( curLevel!=null ) worldRender.invalidateLevelRender(curLevel); curLevelId = l.uid; ge.emit( LevelSelected(l) ); + if( keepWorldLevelSelection ) + ge.emit( WorldLevelSelectionChanged ); ge.emit( ViewportChanged(true) ); saveLastProjectInfos(); @@ -1434,9 +1441,104 @@ class Editor extends Page { return; curWorldDepth = depth; + clearWorldLevelSelection(); ge.emit( WorldDepthSelected(curWorldDepth) ); } + public function isWorldLevelSelected(l:data.Level) { + return l!=null && worldLevelSelection.get(l.uid)==true; + } + + public function countWorldLevelSelection() { + var n = 0; + for(uid in worldLevelSelection.keys()) { + var l = project.getLevelAnywhere(uid); + if( l!=null && l.isInWorld(curWorld) && l.worldDepth==curWorldDepth ) + n++; + } + return n; + } + + public function getWorldLevelSelection() { + var out : Array = []; + for(uid in worldLevelSelection.keys()) { + var l = project.getLevelAnywhere(uid); + if( l!=null && l.isInWorld(curWorld) && l.worldDepth==curWorldDepth ) + out.push(l); + } + return out; + } + + public function clearWorldLevelSelection(?notify=true) { + var any = false; + for(_ in worldLevelSelection.keys()) { + any = true; + break; + } + if( !any ) + return; + + worldLevelSelection = new Map(); + if( notify ) + ge.emit( WorldLevelSelectionChanged ); + } + + public function replaceWorldLevelSelection(levels:Array, ?primary:data.Level) { + worldLevelSelection = new Map(); + for(l in levels) + if( l!=null && l.isInWorld(curWorld) && l.worldDepth==curWorldDepth ) + worldLevelSelection.set(l.uid, true); + + if( primary==null && levels.length>0 ) + primary = levels[0]; + if( primary!=null ) + selectLevel(primary, false, true); + else + ge.emit( WorldLevelSelectionChanged ); + } + + public function toggleWorldLevelSelection(l:data.Level) { + if( l==null ) + return; + + if( worldLevelSelection.get(l.uid)==true ) + worldLevelSelection.remove(l.uid); + else + worldLevelSelection.set(l.uid, true); + + selectLevel(l, false, true); + } + + public function addWorldLevelsToSelection(levels:Array, ?primary:data.Level) { + for(l in levels) + if( l!=null && l.isInWorld(curWorld) && l.worldDepth==curWorldDepth ) + worldLevelSelection.set(l.uid, true); + + if( primary==null && levels.length>0 ) + primary = levels[0]; + if( primary!=null ) + selectLevel(primary, false, true); + else + ge.emit( WorldLevelSelectionChanged ); + } + + public function toggleWorldLevelsInSelection(levels:Array, ?primary:data.Level) { + for(l in levels) + if( l!=null && l.isInWorld(curWorld) && l.worldDepth==curWorldDepth ) { + if( worldLevelSelection.get(l.uid)==true ) + worldLevelSelection.remove(l.uid); + else + worldLevelSelection.set(l.uid, true); + } + + if( primary==null && levels.length>0 ) + primary = levels[0]; + if( primary!=null ) + selectLevel(primary, false, true); + else + ge.emit( WorldLevelSelectionChanged ); + } + public function setProjectFlag(flag:ldtk.Json.ProjectFlag, v:Bool) { project.setFlag(flag, v); @@ -1992,6 +2094,7 @@ class Editor extends Page { return switch(e) { case ViewportChanged(_): false; case WorldLevelMoved(_): false; + case WorldLevelSelectionChanged: false; // case LayerInstanceChangedGlobally(_): false; case WorldMode(_): false; case GridChanged(_): false; @@ -2021,6 +2124,7 @@ class Editor extends Page { case LevelResized(l): extra = l.uid; case LevelRestoredFromHistory(l): case LevelJsonCacheInvalidated(l): + case WorldLevelSelectionChanged: case WorldLevelMoved(l,isFinal, _): if( isFinal ) extra = l.uid; case WorldSettingsChanged: case LayerDefAdded: @@ -2093,6 +2197,7 @@ class Editor extends Page { case ProjectSaved: case LevelSelected(level): LOG.userAction("Opened level "+level); + case WorldLevelSelectionChanged: case LevelSettingsChanged(l): invalidateLevelCache(l); case LevelAdded(l): for(nl in l.getNeighbours()) @@ -2215,6 +2320,7 @@ class Editor extends Page { // Check if events changes the NeedSaving flag switch e { case WorldMode(_): + case WorldLevelSelectionChanged: case WorldDepthSelected(_): case AppSettingsChanged: case ViewportChanged(_): @@ -2246,6 +2352,8 @@ class Editor extends Page { case AppSettingsChanged: case WorldMode(active): + if( !active ) + clearWorldLevelSelection(); if( !active && curWorldDepth!=curLevel.worldDepth ) selectWorldDepth(curLevel.worldDepth); updateWorldDepthsUI(); @@ -2253,6 +2361,8 @@ class Editor extends Page { case WorldDepthSelected(worldDepth): updateWorldDepthsUI(); + case WorldLevelSelectionChanged: + case ViewportChanged(zoomChanged): case EnumDefAdded, EnumDefRemoved, EnumDefChanged, EnumDefSorted, EnumDefValueRemoved: