diff --git a/src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt b/src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt index 1071cb2..0633206 100644 --- a/src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt +++ b/src/commonMain/kotlin/algorithms/localsearch/LocalSearch.kt @@ -40,7 +40,6 @@ class LocalSearch(private val strategy: LocalSearchStra //Logger.log("Applied move: $bestNextStep for delta of $bestNextStepDelta") solution = strategy.applyMove(solution, bestNextStep!!) bestCost = strategy.scoreSolution(solution) - Logger.log("Best cost: $bestCost") noImprovement = 0 } diff --git a/src/commonMain/kotlin/binpack/BinPackProblem.kt b/src/commonMain/kotlin/binpack/BinPackProblem.kt index 9e67303..fd45f65 100644 --- a/src/commonMain/kotlin/binpack/BinPackProblem.kt +++ b/src/commonMain/kotlin/binpack/BinPackProblem.kt @@ -66,6 +66,15 @@ class PlacedBox(w: Int, h: Int, val x: Int, val y: Int) : Box(w, h) { return fragments } - fun intersects(box: PlacedBox) = !((endX <= box.x) || (box.endX <= x) || (endY <= box.y) || (box.endY <= y)) fun outOfBounds(size: Int) = x < 0 || y < 0 || endX > size || endY > size + fun intersects(box: PlacedBox) = !((endX <= box.x) || (box.endX <= x) || (endY <= box.y) || (box.endY <= y)) + + fun intersection(box: PlacedBox) : Int { + val xOverlap = max(0, min(endX, box.endX) - max(x, box.x)); + val yOverlap = max(0, min(endY, box.endY) - max(y, box.y)); + return xOverlap * yOverlap + } + fun relativeOverlap(box: PlacedBox) = intersection(box).toDouble() / max(area, box.area) + + } \ No newline at end of file diff --git a/src/commonMain/kotlin/binpack/SpaceContainer.kt b/src/commonMain/kotlin/binpack/SpaceContainer.kt index fa1d9ac..c8c76ec 100644 --- a/src/commonMain/kotlin/binpack/SpaceContainer.kt +++ b/src/commonMain/kotlin/binpack/SpaceContainer.kt @@ -32,8 +32,23 @@ open class SpaceContainer( spaces.addAll(space.shatter(placed)) } - open fun remove(box: PlacedBox) { + open fun remove(box: PlacedBox, overlapPossible: Boolean = false) { boxes.remove(box) - spaces.add(box) + if(overlapPossible) { + val candidateSpaces = mutableListOf(box) + val newSpaces = mutableListOf() + + while(candidateSpaces.isNotEmpty()) { + val space = candidateSpaces.removeAt(0) + val intersecting = boxes.firstOrNull { it.intersects(space) } + if(intersecting == null) + newSpaces.add(space) + else + candidateSpaces.addAll(space.shatter(intersecting)) + } + spaces.addAll(newSpaces) + } + else + spaces.add(box) } } diff --git a/src/commonMain/kotlin/binpack/configurations/GreedyConfigurations.kt b/src/commonMain/kotlin/binpack/configurations/GreedyConfigurations.kt index 1c3a980..f76d159 100644 --- a/src/commonMain/kotlin/binpack/configurations/GreedyConfigurations.kt +++ b/src/commonMain/kotlin/binpack/configurations/GreedyConfigurations.kt @@ -3,7 +3,6 @@ package binpack.configurations import algorithms.greedy.GreedyPacker import binpack.* import binpack.greedy.* -import ui.UIState abstract class GenericGreedyConfig : Algorithm() { protected lateinit var packer: GreedyPacker diff --git a/src/commonMain/kotlin/binpack/configurations/LocalSearchConfigurations.kt b/src/commonMain/kotlin/binpack/configurations/LocalSearchConfigurations.kt index 288b811..ca996aa 100644 --- a/src/commonMain/kotlin/binpack/configurations/LocalSearchConfigurations.kt +++ b/src/commonMain/kotlin/binpack/configurations/LocalSearchConfigurations.kt @@ -3,10 +3,7 @@ package binpack.configurations import algorithms.localsearch.LocalSearch import binpack.* import binpack.greedy.NormalPosCircTouchPacker -import binpack.localsearch.LSSMove -import binpack.localsearch.LocalSequenceStrategy -import binpack.localsearch.MSSMove -import binpack.localsearch.MaximalSpaceStrategy +import binpack.localsearch.* object LocalSearchLocalSequence : Algorithm() { override val name = "LocalSearch-LocalSequence" @@ -26,9 +23,9 @@ object LocalSearchLocalSequence : Algorithm() { } } -object LocalSearchMaximalSpace : Algorithm() { - override val name = "LocalSearch-MaxSpace" - override val shortName = "LS MaxSpace" +object LocalSearchRepackSpace : Algorithm() { + override val name = "LocalSearch-RepackSpace" + override val shortName = "LS RepackSpace" private lateinit var localsearch: LocalSearch override fun optimize(): BinPackSolution { @@ -40,6 +37,24 @@ object LocalSearchMaximalSpace : Algorithm() { } override fun init(instance: BinPackProblem) { - localsearch = LocalSearch(MaximalSpaceStrategy(), instance) + localsearch = LocalSearch(RepackSpaceStrategy(), instance) + } +} + +object LocalSearchRelaxedSpace : Algorithm() { + override val name = "LocalSearch-RelaxedSpace" + override val shortName = "LS RelaxedSpace" + private lateinit var localsearch: LocalSearch + + override fun optimize(): BinPackSolution { + return localsearch.optimize() + } + + override fun optimizeStep(limit: Int): Pair { + return localsearch.optimizeStep(limit) + } + + override fun init(instance: BinPackProblem) { + localsearch = LocalSearch(OverlapSpaceStrategy(), instance) } } \ No newline at end of file diff --git a/src/commonMain/kotlin/binpack/localsearch/GravityBinPackStrategy.kt b/src/commonMain/kotlin/binpack/localsearch/GravityBinPackStrategy.kt deleted file mode 100644 index 16b8af4..0000000 --- a/src/commonMain/kotlin/binpack/localsearch/GravityBinPackStrategy.kt +++ /dev/null @@ -1,14 +0,0 @@ -package binpack.localsearch - -import algorithms.localsearch.LocalSearchStrategy -import binpack.BinPackProblem -import kotlin.math.pow - -interface GravityPackMove - -data class RotateMove(val index: Int) : GravityPackMove { - override fun toString() = "[rotate $index]" -} -data class SwapMove(val a: Int, val b: Int) : GravityPackMove { - override fun toString() = "[swap $a<->$b]" -} \ No newline at end of file diff --git a/src/commonMain/kotlin/binpack/localsearch/OverlapSpaceStrategy.kt b/src/commonMain/kotlin/binpack/localsearch/OverlapSpaceStrategy.kt new file mode 100644 index 0000000..9e85e93 --- /dev/null +++ b/src/commonMain/kotlin/binpack/localsearch/OverlapSpaceStrategy.kt @@ -0,0 +1,248 @@ +package binpack.localsearch + +import Logger +import binpack.SpaceContainer +import binpack.SpaceContainerSolution +import kotlin.math.max + +class OverlapSpaceStrategy : RepackSpaceStrategy() { + override val estimateFactor = 1.0 + private var allowedOverlap = 1.0 + private var overlapPenalty = 1.0 + private var consecutivePlaceboMoves = 0 + private var forceLateNeighborhood = false + + override fun initialSolution(): SpaceContainerSolution { + // Place every box in its own container + val containers = instance.boxes.mapIndexed { i, box -> + val c = SpaceContainer(i, instance.containerSize) + c.add(box, c.spaces[0]) + c + } + return SpaceContainerSolution(instance.containerSize, containers) + } + + override fun perIterationSharedSetup(solution: SpaceContainerSolution) { + if(forceLateNeighborhood) { + repackEstimationAvailableSpaces = solution.containerObjs.flatMap { c -> c.spaces.map { Pair(c.ci, it) } } + } + } + + override fun neighboringSolutions(solution: SpaceContainerSolution): List { + val containers = solution.containerObjs + + // For fast progress early on, emulate the greedy algorithm, producing as few candidate moves as possible + // later on (once we have a halfway decent solution), produce more higher-effort moves + return if(!forceLateNeighborhood) + earlyNeighborhood(containers) + else + lateNeighborhood(containers) + } + + override fun earlyNeighborhood(containers: List) : List { + val cramMoves = mutableListOf() + + val target = containers.firstOrNull { it.hasAccessibleSpace } + + target?.spaces?.forEachIndexed { si, space -> + + val box = containers.subList(target.ci+1, containers.size).flatMap { c -> + c.boxes.mapIndexedNotNull { bi, b -> + val placed = optimalPlacement(b, space, target.size) + val condition = (placed == null) // || (allowedOverlap < 1.0 && c.boxes.any { it.relativeOverlap(placed) > allowedOverlap }) + if(condition) null else Triple(c.ci, bi, placed!!) + } + }.maxByOrNull { space.relativeOverlap(it.third) } + + if(box != null) + cramMoves.add(CramMove(box.first, box.second, target.ci, si)) + } + + return if(cramMoves.size == 0) { + forceLateNeighborhood = true + lateNeighborhood(containers) + } + else + listOf(PlaceboMove) + cramMoves + } + + override fun lateNeighborhood(containers: List) : List { + val baseMoves = mutableListOf() + var cramMoves = mutableListOf() + var localMoves = mutableListOf() + + // Ensure that local search never stops before all overlaps are removed + if(containers.any { c -> c.boxes.any{ b1 -> c.boxes.any{ b2 -> b1 != b2 && b1.intersects(b2) } } }) + baseMoves.add(PlaceboMove) + + // In the worst case, we have to add overlapping boxes to a new container + val escapeMoves = containers.flatMap { c -> + c.boxes.mapIndexedNotNull { bi, box -> if(c.boxes.any { box != it && box.relativeOverlap(it) > allowedOverlap }) EscapeMove(c.ci, bi) else null } + } + + val sourceCandidates = containers.filter { it.freeSpace.toDouble() / it.area > 0.1 } + + sourceCandidates.forEach { container -> + val ci = container.ci + container.boxes.forEachIndexed { bi, box -> + // Generate local moves only for a subset of containers + if(ci % 5 == moveIndex % 5) { + container.spaces.indices.forEach { si -> + localMoves.add(LocalMove(ci, bi, si)) + } + } + + // Generate cross-container cram moves + /*(0 until ci).forEach { tci -> + containers[tci].spaces.forEachIndexed { si, space -> + if(box.area * allowedOverlap < space.area) + cramMoves.add(CramMove(ci, bi, tci, si)) + } + }*/ + // Generate cross-container repack moves + (0 until ci).forEach { tci -> + containers[tci].spaces.indices.forEach { si -> + cramMoves.add(RepackMove(ci, bi, tci, si)) + } + } + } + } + + //Logger.log("Move count: ${cramMoves.size} cram; ${localMoves.size} local; ${escapeMoves.size} escape;") + + if(localMoves.size > moveBudget * 0.2) { + localMoves.shuffle() + localMoves = localMoves.subList(0, (moveBudget * 0.2).toInt()) + } + + if(cramMoves.size > moveBudget - localMoves.size) { + cramMoves.shuffle() + cramMoves = cramMoves.subList(0, moveBudget - localMoves.size) + } + + moveIndex += 1 + return baseMoves + escapeMoves + localMoves + cramMoves + } + + override fun deltaScoreMove(solution: SpaceContainerSolution, currentScore: Double, move: MSSMove): Double { + return when(move) { + is PlaceboMove -> -0.00001 + is LocalMove -> super.deltaScoreMove(solution, currentScore, move) + is RepackMove -> super.deltaScoreMove(solution, currentScore, move) + is EscapeMove -> { + val source = solution.containerObjs[move.sourceContainer] + val box = source.boxes[move.sourceBox] + val newContainerCost = (source.area - box.area).toDouble() / (1 + solution.containerObjs.size) + val overlapCost = - source.boxes.sumOf { b -> if(box.relativeOverlap(b) > allowedOverlap && box != b) box.intersection(b) else 0 } * overlapPenalty + + newContainerCost + overlapCost + } + is CramMove -> { + val source = solution.containerObjs[move.sourceContainer] + val target = solution.containerObjs[move.targetContainer] + val box = source.boxes[move.sourceBox] + val space = target.spaces[move.targetSpace] + + val placed = optimalPlacement(box, space, target.size) + + // box doesn't fit target space, like, _at all_ + if(placed == null || target.boxes.any { placed.relativeOverlap(it) > allowedOverlap }) + 0.0 + else { + val emptiesSource = source.boxes.size == 1 + val overlap = target.boxes.sumOf { it.intersection(placed) } + val overlapCost = - source.boxes.sumOf { b -> if(box.relativeOverlap(b) > allowedOverlap && box != b) box.intersection(b) else 0 } + + val targetCost = - max(1.0, (box.area.toDouble() - overlap)) + val sourceCost = if(emptiesSource) box.area - source.area.toDouble() else if(move.targetContainer < move.sourceContainer) 0.0 else box.area.toDouble() + + overlapCost * overlapPenalty + sourceCost / (move.sourceContainer + 1) + targetCost / (move.targetContainer + 1) + } + } + else -> 0.0 + } + } + + override fun applyMove(solution: SpaceContainerSolution, move: MSSMove): SpaceContainerSolution { + return when(move) { + is PlaceboMove -> { + // Accelerate overlap cost increase + consecutivePlaceboMoves += 1 + overlapPenalty += 0.3 * consecutivePlaceboMoves + allowedOverlap = max(0.0, allowedOverlap - 0.007 * consecutivePlaceboMoves) + //Logger.log("Allowed overlap now $allowedOverlap ($consecutivePlaceboMoves consecutive)") + solution + } + is RepackMove -> { + allowedOverlap = max(0.0, allowedOverlap - 0.001) + applyRepackMove(solution, move) + } + is LocalMove -> applyLocalMove(solution, move) + is CramMove -> applyCramMove(solution, move) + is EscapeMove -> applyEscapeMove(solution, move) + else -> throw Exception("Unknown or unsupported move $move") + } + } + + private fun applyCramMove(solution: SpaceContainerSolution, move: CramMove): SpaceContainerSolution { + consecutivePlaceboMoves = 0 + val source = solution.containerObjs[move.sourceContainer] + val target = solution.containerObjs[move.targetContainer] + val box = source.boxes[move.sourceBox] + val space = target.spaces[move.targetSpace] + + val placed = optimalPlacement(box, space, target.size) ?: throw Exception("No valid placement for $box") + + val newContainers = solution.containerObjs.toMutableList() + + // Remove box from source + source.remove(box, overlapPossible = true) + consolidateSpaces(source) + + // Place box and adjust spaces + target.boxes.add(placed) + target.spaces.filter { it.intersects(placed) }.forEach { + target.spaces.remove(it) + target.spaces.addAll(it.shatter(placed)) + } + + consolidateSpaces(target) + + // Remove container if now empty + if(source.boxes.isEmpty()) { + newContainers.removeAt(move.sourceContainer) + newContainers.subList(move.sourceContainer, newContainers.size).forEach { container -> container.ci -= 1 } + } + + return SpaceContainerSolution(solution.containerSize, newContainers) + } + + private fun applyEscapeMove(solution: SpaceContainerSolution, move: EscapeMove): SpaceContainerSolution { + consecutivePlaceboMoves = 0 + val source = solution.containerObjs[move.sourceContainer] + val box = source.boxes[move.sourceBox] + + val newContainers = solution.containerObjs.toMutableList() + source.remove(box, overlapPossible = true) + + // Try a shallow global repack first before creating a new container + if(!shallowGlobalRepack(newContainers, move.sourceContainer, listOf(box))) { + val target = SpaceContainer(solution.containerObjs.size, source.size) + target.add(box) + newContainers.add(target) + } + + return SpaceContainerSolution(solution.containerSize, newContainers) + } + + override fun scoreSolution(solution: SpaceContainerSolution): Double { + //renderDebugSpaces(solution.containerObjs) + return 0.0 + } +} + +object PlaceboMove : MSSMove { + override fun toString() = "PlaceboMove" +} +data class CramMove(val sourceContainer: Int, val sourceBox: Int, val targetContainer: Int, val targetSpace: Int) : MSSMove +data class EscapeMove(val sourceContainer: Int, val sourceBox: Int) : MSSMove \ No newline at end of file diff --git a/src/commonMain/kotlin/binpack/localsearch/MaximalSpaceStrategy.kt b/src/commonMain/kotlin/binpack/localsearch/RepackSpaceStrategy.kt similarity index 81% rename from src/commonMain/kotlin/binpack/localsearch/MaximalSpaceStrategy.kt rename to src/commonMain/kotlin/binpack/localsearch/RepackSpaceStrategy.kt index ad29f7c..a68df35 100644 --- a/src/commonMain/kotlin/binpack/localsearch/MaximalSpaceStrategy.kt +++ b/src/commonMain/kotlin/binpack/localsearch/RepackSpaceStrategy.kt @@ -2,13 +2,14 @@ package binpack.localsearch import algorithms.localsearch.LocalSearchStrategy import binpack.* +import ui.UIState -class MaximalSpaceStrategy : LocalSearchStrategy { - private lateinit var instance: BinPackProblem - private val estimateFactor: Double = 1.5 - private val moveBudget = 2000 - private var moveIndex = 0 - private var repackEstimationAvailableSpaces: List>? = null +open class RepackSpaceStrategy : LocalSearchStrategy { + protected lateinit var instance: BinPackProblem + protected open val estimateFactor: Double = 1.5 + protected val moveBudget = 2000 + protected var moveIndex = 0 + protected var repackEstimationAvailableSpaces: List>? = null override fun init(instance: BinPackProblem) { this.instance = instance @@ -36,7 +37,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy) : List { + protected open fun earlyNeighborhood(containers: List) : List { val moves = mutableListOf() containers.indices.forEach { source -> @@ -58,7 +59,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy) : List { + protected open fun lateNeighborhood(containers: List) : List { var repackMoves = mutableListOf() var localMoves = mutableListOf() @@ -76,11 +77,8 @@ class MaximalSpaceStrategy : LocalSearchStrategy - val target = containers[tci] - if(target.hasAccessibleSpace) { - target.spaces.indices.forEach { si -> - repackMoves.add(RepackMove(ci, bi, tci, si)) - } + containers[tci].spaces.indices.forEach { si -> + repackMoves.add(RepackMove(ci, bi, tci, si)) } } } @@ -121,13 +119,15 @@ class MaximalSpaceStrategy : LocalSearchStrategy - container.spaces.forEach { space -> - UIState.debugVisualizer!!.debugBox(space, ci) - } - }*/ - return SpaceContainerSolution(solution.containerSize, newContainers) } - override fun scoreSolution(solution: SpaceContainerSolution): Double { - return solution.containerObjs.sumOf { container -> - container.spaces.sumOf { it.area }.toDouble() / (container.ci + 1) - } - } + override fun scoreSolution(solution: SpaceContainerSolution) = 0.0 - private fun estimateShallowGlobalRepack(solution: SpaceContainerSolution, sourceContainer: Int, additionalAvoid: Int?, boxes: List): Double { + private fun estimateShallowGlobalRepack(sourceContainer: Int, additionalAvoid: Int?, boxes: List): Double { if(boxes.isEmpty()) return 0.0 @@ -277,7 +269,7 @@ class MaximalSpaceStrategy : LocalSearchStrategy, sourceContainer: Int, boxes: List) : Boolean { + protected fun shallowGlobalRepack(containers: List, sourceContainer: Int, boxes: List) : Boolean { boxes.sortedByDescending { it.area }.forEach { box -> val target = containers.firstOrNull { it.ci != sourceContainer && it.freeSpace >= box.area && it.spaces.any { space -> space.fitsRotated(box) } } ?: return false val space = target.spaces.first{ it.fitsRotated(box) } @@ -324,7 +316,21 @@ class MaximalSpaceStrategy : LocalSearchStrategy c.spaces.any{ b -> b != a && a.intersects(b)} }) - throw Exception("Invariant violated: Intersecting spaces @ $id") - if(c.boxes.any { a -> c.boxes.any{ b -> b != a && a.intersects(b)} }) - throw Exception("Invariant violated: Intersecting boxes @ $id") + protected fun renderDebugSpaces(containers: List) { + UIState.debugVisualizer!!.debugClear() + containers.forEachIndexed { ci, container -> + container.spaces.forEach { space -> + UIState.debugVisualizer!!.debugBox(space, ci) + } + } } } diff --git a/src/commonTest/kotlin/Stats.kt b/src/commonTest/kotlin/Stats.kt index 50c23e6..5b07dad 100644 --- a/src/commonTest/kotlin/Stats.kt +++ b/src/commonTest/kotlin/Stats.kt @@ -1,4 +1,4 @@ -class Stats() { +class Stats { private val executions = mutableListOf() fun add(entry: StatEntry) = executions.add(entry) @@ -11,6 +11,12 @@ class Stats() { val containersAboveLowerBound: Int get() = executions.sumOf{ it.containers - it.lowerBound } + + val runtimeByBoxCount: Map + get() = executions.groupBy { it.boxCount }.mapValues { entry -> entry.value.sumOf { it.runtime }.toDouble() / entry.value.size } + + val surplusByBoxCount: Map + get() = executions.groupBy { it.boxCount }.mapValues { entry -> entry.value.sumOf { it.containers - it.lowerBound } } } data class StatEntry(val boxCount: Int, val k1: Double, val runtime: Long, val containers: Int, val lowerBound: Int) \ No newline at end of file diff --git a/src/jsMain/kotlin/main.kt b/src/jsMain/kotlin/main.kt index 122b32c..77d2909 100644 --- a/src/jsMain/kotlin/main.kt +++ b/src/jsMain/kotlin/main.kt @@ -1,10 +1,10 @@ import kotlinx.browser.document import kotlinx.browser.window import org.w3c.dom.* -import ui.UIState import ui.BinPackVisualizer +import ui.UIState +import kotlin.random.Random import kotlin.time.ExperimentalTime -import kotlin.time.measureTime @OptIn(ExperimentalTime::class) fun main() { @@ -16,7 +16,7 @@ fun main() { val runBtn = document.getElementById("btnRun") as HTMLButtonElement val resetBtn = document.getElementById("btnReset") as HTMLButtonElement val genBtn = document.getElementById("btnGenInstance") as HTMLButtonElement - val statsBtn = document.getElementById("btnUpdateStats") as HTMLButtonElement + val genRanBtn = document.getElementById("btnGenInstanceRandom") as HTMLButtonElement val algoSelect = document.getElementById("inpAlgorithm") as HTMLSelectElement canvas.width = document.body!!.clientWidth @@ -41,18 +41,6 @@ fun main() { statsContainer.appendChild(statEntry) } - algoSelect.onchange = { - UIState.setActiveAlgorithm(algoSelect.selectedIndex) - } - - stepBtn.onclick = { - val stepTime = measureTime { - UIState.solution = UIState.activeAlgorithm.optimizeStep(1).first - } - Logger.log("Step time: ${stepTime.inWholeMilliseconds} ms") - UIState.visualizer.refresh(UIState.solution) - } - runBtn.onclick = { UIState.minFrameDelay = (document.getElementById("inpFrameDelay") as HTMLInputElement).value.toInt() @@ -81,7 +69,22 @@ fun main() { 0 } - statsBtn.onclick = { UIState.updateStats() } + genRanBtn.onclick = { + val genOpt = UIState.GeneratorOptions + genOpt.seed = Random.nextInt() + (document.getElementById("inpSeed") as HTMLInputElement).value = genOpt.seed.toString() + genOpt.boxCount = (document.getElementById("inpBoxCount") as HTMLInputElement).value.toInt() + genOpt.containerSize = (document.getElementById("inpContainerSize") as HTMLInputElement).value.toInt() + genOpt.minH = (document.getElementById("inpMinHeight") as HTMLInputElement).value.toInt() + genOpt.minW = (document.getElementById("inpMinWidth") as HTMLInputElement).value.toInt() + genOpt.maxH = (document.getElementById("inpMaxHeight") as HTMLInputElement).value.toInt() + genOpt.maxW = (document.getElementById("inpMaxWidth") as HTMLInputElement).value.toInt() + UIState.refreshInstance() + 0 + } + + algoSelect.onchange = { UIState.setActiveAlgorithm(algoSelect.selectedIndex) } + stepBtn.onclick = { UIState.singleStep() } resetBtn.onclick = { UIState.reset() } }) } diff --git a/src/jsMain/kotlin/ui/BinPackVisualizer.kt b/src/jsMain/kotlin/ui/BinPackVisualizer.kt index aef33d3..057784a 100644 --- a/src/jsMain/kotlin/ui/BinPackVisualizer.kt +++ b/src/jsMain/kotlin/ui/BinPackVisualizer.kt @@ -20,8 +20,8 @@ class BinPackVisualizer(override val ctx: CanvasRenderingContext2D, override val val sy = (displaySize + 20) * row renderContainer(sx, sy) - ctx.strokeStyle = "#f00" - ctx.fillStyle = "#a00" + ctx.strokeStyle = "#d05653" + ctx.fillStyle = if(fillTranslucent) "#a008" else "#820002" container.forEach { box -> renderBox(box, sx, sy) } diff --git a/src/jsMain/kotlin/ui/UIState.kt b/src/jsMain/kotlin/ui/UIState.kt index 505a6e7..bd4f60c 100644 --- a/src/jsMain/kotlin/ui/UIState.kt +++ b/src/jsMain/kotlin/ui/UIState.kt @@ -5,6 +5,7 @@ import binpack.BinPackProblem import binpack.BinPackSolution import binpack.BoxGenerator import binpack.configurations.* +import binpack.localsearch.OverlapSpaceStrategy import kotlinx.browser.document import kotlinx.browser.window import org.w3c.dom.HTMLButtonElement @@ -41,7 +42,8 @@ actual object UIState { GreedyAreaDescSpaceFF, GreedyAdaptiveBFSpace, LocalSearchLocalSequence, - LocalSearchMaximalSpace) + LocalSearchRepackSpace, + LocalSearchRelaxedSpace) var activeAlgorithm = algorithms[0] @@ -65,19 +67,32 @@ actual object UIState { val elapsed = measureTime { val res = activeAlgorithm.optimizeStep(stepSize) solution = res.first - visualizer.refresh(solution) - updateStats() if(res.second) stop() } runtime += elapsed.inWholeMilliseconds + visualizer.refresh(solution) + updateStats() + val delay = max(5, minFrameDelay - elapsed.inWholeMilliseconds).toInt() if(running) window.setTimeout({ tick() }, delay) } + @OptIn(ExperimentalTime::class) + fun singleStep() { + val elapsed = measureTime { + val res = activeAlgorithm.optimizeStep(stepSize) + solution = res.first + } + + runtime += elapsed.inWholeMilliseconds + visualizer.refresh(solution) + updateStats() + } + fun stop() { running = false (document.getElementById("btnRun") as HTMLButtonElement).innerText = "run" @@ -99,6 +114,9 @@ actual object UIState { fun setActiveAlgorithm(index: Int) { activeAlgorithm = algorithms[index] + + visualizer.fillTranslucent = activeAlgorithm is LocalSearchRelaxedSpace + reset() } @@ -117,6 +135,7 @@ actual object UIState { activeAlgorithm.init(instance) solution = BinPackSolution(instance.containerSize, emptyList()) visualizer.refresh(solution) + visualizer.debugClear() } } diff --git a/src/jsMain/kotlin/ui/Visualizer.kt b/src/jsMain/kotlin/ui/Visualizer.kt index d510e41..de47753 100644 --- a/src/jsMain/kotlin/ui/Visualizer.kt +++ b/src/jsMain/kotlin/ui/Visualizer.kt @@ -12,6 +12,7 @@ abstract class Visualizer : DebugVisualizer { protected abstract val ctx: CanvasRenderingContext2D protected abstract val debug: CanvasRenderingContext2D private var debugDisabled = false + var fillTranslucent = false abstract fun render(solution: Solution) diff --git a/src/jsMain/resources/back.jpg b/src/jsMain/resources/back.jpg new file mode 100644 index 0000000..56e1e0d Binary files /dev/null and b/src/jsMain/resources/back.jpg differ diff --git a/src/jsMain/resources/index.html b/src/jsMain/resources/index.html index f35b40e..9508d3b 100644 --- a/src/jsMain/resources/index.html +++ b/src/jsMain/resources/index.html @@ -6,13 +6,16 @@