diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsAbstractBankCompositionFamilySupport.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsAbstractBankCompositionFamilySupport.java new file mode 100644 index 00000000..6416781f --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsAbstractBankCompositionFamilySupport.java @@ -0,0 +1,80 @@ +package p.studio.workspaces.assets.details.bank; + +import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails; +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +abstract class AssetDetailsAbstractBankCompositionFamilySupport implements AssetDetailsBankCompositionFamilySupport { + @Override + public AssetDetailsBankCompositionDraft createDraft(AssetWorkspaceAssetDetails details) { + final List selectedFiles = details.bankComposition().selectedFiles(); + final var selectedPaths = selectedFiles.stream().map(AssetWorkspaceBankCompositionFile::path).collect(java.util.stream.Collectors.toSet()); + final List availableFiles = details.bankComposition().availableFiles().stream() + .filter(file -> !selectedPaths.contains(file.path())) + .toList(); + return new AssetDetailsBankCompositionDraft(availableFiles, selectedFiles); + } + + @Override + public AssetDetailsBankCompositionDraft moveToSelected( + AssetDetailsBankCompositionDraft draft, + List files) { + final List movableFiles = selectMovableFiles(draft, files); + return draft.removeFromAvailable(movableFiles).appendToSelected(movableFiles); + } + + @Override + public AssetDetailsBankCompositionDraft moveToAvailable( + AssetDetailsBankCompositionDraft draft, + List files) { + final var returnedFiles = new ArrayList<>(files); + returnedFiles.sort(java.util.Comparator.comparing(AssetWorkspaceBankCompositionFile::path, String.CASE_INSENSITIVE_ORDER)); + return draft.removeFromSelected(files).appendToAvailable(returnedFiles); + } + + @Override + public AssetDetailsBankCompositionDraft moveUp(AssetDetailsBankCompositionDraft draft, int selectedIndex) { + if (selectedIndex <= 0 || selectedIndex >= draft.selectedFiles().size()) { + return draft; + } + final List reordered = new ArrayList<>(draft.selectedFiles()); + final AssetWorkspaceBankCompositionFile moved = reordered.remove(selectedIndex); + reordered.add(selectedIndex - 1, moved); + return new AssetDetailsBankCompositionDraft(draft.availableFiles(), reordered); + } + + @Override + public AssetDetailsBankCompositionDraft moveDown(AssetDetailsBankCompositionDraft draft, int selectedIndex) { + if (selectedIndex < 0 || selectedIndex >= draft.selectedFiles().size() - 1) { + return draft; + } + final List reordered = new ArrayList<>(draft.selectedFiles()); + final AssetWorkspaceBankCompositionFile moved = reordered.remove(selectedIndex); + reordered.add(selectedIndex + 1, moved); + return new AssetDetailsBankCompositionDraft(draft.availableFiles(), reordered); + } + + protected abstract boolean canAdd( + AssetDetailsBankCompositionDraft currentDraft, + List accepted, + AssetWorkspaceBankCompositionFile candidate); + + private List selectMovableFiles( + AssetDetailsBankCompositionDraft draft, + List files) { + final var requested = new LinkedHashSet<>(files); + final List movable = new ArrayList<>(); + for (AssetWorkspaceBankCompositionFile file : draft.availableFiles()) { + if (!requested.contains(file)) { + continue; + } + if (canAdd(draft, movable, file)) { + movable.add(file); + } + } + return List.copyOf(movable); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCapacityState.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCapacityState.java new file mode 100644 index 00000000..222188d6 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCapacityState.java @@ -0,0 +1,20 @@ +package p.studio.workspaces.assets.details.bank; + +import p.studio.controls.banks.StudioAssetCapacitySeverity; + +import java.util.Objects; + +public record AssetDetailsBankCompositionCapacityState( + double progress, + StudioAssetCapacitySeverity severity, + boolean blocked, + String labelText, + String hintText) { + + public AssetDetailsBankCompositionCapacityState { + progress = Math.max(0.0d, Math.min(1.0d, progress)); + severity = severity == null ? StudioAssetCapacitySeverity.GREEN : severity; + labelText = Objects.requireNonNullElse(labelText, ""); + hintText = Objects.requireNonNullElse(hintText, ""); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java index 755a371e..c95ae378 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java @@ -5,17 +5,13 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import p.studio.Container; import p.studio.controls.banks.StudioAssetCapacityMeter; -import p.studio.controls.banks.StudioAssetCapacitySeverity; import p.studio.controls.forms.StudioFormActionBar; -import p.studio.controls.forms.StudioFormMode; -import p.studio.controls.forms.StudioFormSession; import p.studio.controls.lifecycle.StudioControlLifecycle; import p.studio.controls.lifecycle.StudioControlLifecycleSupport; import p.studio.events.StudioWorkspaceEventBus; import p.studio.projects.ProjectReference; import p.studio.utilities.i18n.I18n; import p.studio.workspaces.assets.details.AssetDetailsUiSupport; -import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails; import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState; import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent; import p.studio.workspaces.framework.StudioSubscriptionBag; @@ -32,9 +28,9 @@ public final class AssetDetailsBankCompositionControl extends VBox implements St this::cancelEdit); private final AssetDetailsBankCompositionDualListView dualListView = new AssetDetailsBankCompositionDualListView(); private final StudioAssetCapacityMeter capacityMeter = new StudioAssetCapacityMeter(); + private final AssetDetailsBankCompositionCoordinator coordinator = new AssetDetailsBankCompositionCoordinator(); private AssetWorkspaceDetailsViewState viewState; - private StudioFormSession formSession; public AssetDetailsBankCompositionControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) { StudioControlLifecycleSupport.install(this, this); @@ -46,7 +42,7 @@ public final class AssetDetailsBankCompositionControl extends VBox implements St public void subscribe() { subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> { viewState = event.viewState(); - syncFormSession(); + coordinator.replaceDetails(viewState == null ? null : viewState.selectedAssetDetails()); render(); })); } @@ -56,48 +52,46 @@ public final class AssetDetailsBankCompositionControl extends VBox implements St subscriptions.clear(); } - private void syncFormSession() { - if (viewState == null || viewState.selectedAssetDetails() == null) { - formSession = null; - return; - } - - final AssetWorkspaceBankCompositionDetails source = viewState.selectedAssetDetails().bankComposition(); - if (formSession == null) { - formSession = new StudioFormSession<>(source); - return; - } - if (!Objects.equals(formSession.source(), source)) { - formSession.replaceSource(source); - } - } - private void render() { if (viewState == null || viewState.selectedAssetDetails() == null) { getChildren().clear(); return; } - if (formSession == null) { - syncFormSession(); - } - if (formSession == null) { + if (!coordinator.ready()) { getChildren().clear(); return; } - final boolean editing = formSession.mode() == StudioFormMode.EDITING; - final AssetWorkspaceBankCompositionDetails draft = formSession.draft(); + final AssetDetailsBankCompositionViewModel viewModel = coordinator.viewModel(); - dualListView.setLeftItems(draft.availableFiles()); - dualListView.setRightItems(draft.selectedFiles()); - dualListView.setInteractionEnabled(false); + dualListView.setLeftItems(viewModel.availableFiles()); + dualListView.setRightItems(viewModel.selectedFiles()); + dualListView.setInteractionEnabled(viewModel.editing()); + dualListView.setOnMoveToRight(items -> { + coordinator.moveToSelected(items); + render(); + }); + dualListView.setOnMoveToLeft(items -> { + coordinator.moveToAvailable(items); + render(); + }); + dualListView.setOnMoveUp(index -> { + coordinator.moveUp(index); + render(); + }); + dualListView.setOnMoveDown(index -> { + coordinator.moveDown(index); + render(); + }); - capacityMeter.setProgress(0.0d); - capacityMeter.setSeverity(StudioAssetCapacitySeverity.GREEN); - capacityMeter.setLabelText(draft.measuredBankSizeBytes() + " bytes"); - capacityMeter.setHintText(editing - ? Container.i18n().text(I18n.ASSETS_DETAILS_BANK_COMPOSITION_EDITING_HINT) - : Container.i18n().text(I18n.ASSETS_DETAILS_BANK_COMPOSITION_READONLY_HINT)); + capacityMeter.setProgress(viewModel.capacityState().progress()); + capacityMeter.setSeverity(viewModel.capacityState().severity()); + capacityMeter.setLabelText(viewModel.capacityState().labelText()); + capacityMeter.setHintText(viewModel.capacityState().hintText().isBlank() + ? (viewModel.editing() + ? Container.i18n().text(I18n.ASSETS_DETAILS_BANK_COMPOSITION_EDITING_HINT) + : Container.i18n().text(I18n.ASSETS_DETAILS_BANK_COMPOSITION_READONLY_HINT)) + : viewModel.capacityState().hintText()); final HBox body = new HBox(16, dualListView, capacityMeter); body.getStyleClass().add("assets-details-bank-composition-body"); @@ -105,7 +99,7 @@ public final class AssetDetailsBankCompositionControl extends VBox implements St dualListView.setMaxWidth(Double.MAX_VALUE); final VBox content = new VBox(12, body); - actionBar.updateState(formSession.mode(), formSession.isDirty()); + actionBar.updateState(viewModel.editing() ? p.studio.controls.forms.StudioFormMode.EDITING : p.studio.controls.forms.StudioFormMode.READ_ONLY, viewModel.dirty()); content.getChildren().add(actionBar); final VBox section = AssetDetailsUiSupport.createSection( @@ -116,34 +110,22 @@ public final class AssetDetailsBankCompositionControl extends VBox implements St } private void beginEdit() { - if (formSession == null) { - return; - } - formSession.beginEdit(); + coordinator.beginEdit(); render(); } private void applyDraft() { - if (formSession == null) { - return; - } - formSession.apply(); + coordinator.apply(); render(); } private void resetDraft() { - if (formSession == null) { - return; - } - formSession.resetDraft(); + coordinator.reset(); render(); } private void cancelEdit() { - if (formSession == null) { - return; - } - formSession.cancelEdit(); + coordinator.cancel(); render(); } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinator.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinator.java new file mode 100644 index 00000000..e5b50281 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinator.java @@ -0,0 +1,115 @@ +package p.studio.workspaces.assets.details.bank; + +import p.studio.controls.forms.StudioFormMode; +import p.studio.controls.forms.StudioFormSession; +import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails; +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +import java.util.List; +import java.util.Objects; + +public final class AssetDetailsBankCompositionCoordinator { + private final List familySupports = List.of( + new AssetDetailsTileBankCompositionFamilySupport(), + new AssetDetailsSoundBankCompositionFamilySupport(), + new AssetDetailsFallbackBankCompositionFamilySupport()); + + private AssetDetailsBankCompositionFamilySupport familySupport; + private StudioFormSession formSession; + + public void replaceDetails(AssetWorkspaceAssetDetails details) { + if (details == null) { + familySupport = null; + formSession = null; + return; + } + + familySupport = familySupports.stream() + .filter(candidate -> candidate.supports(details.summary().assetFamily())) + .findFirst() + .orElseThrow(); + final AssetDetailsBankCompositionDraft source = familySupport.createDraft(details); + if (formSession == null) { + formSession = new StudioFormSession<>(source); + return; + } + if (!Objects.equals(formSession.source(), source)) { + formSession.replaceSource(source); + } + } + + public boolean ready() { + return formSession != null && familySupport != null; + } + + public void beginEdit() { + if (ready()) { + formSession.beginEdit(); + } + } + + public void apply() { + if (ready()) { + formSession.apply(); + } + } + + public void reset() { + if (ready()) { + formSession.resetDraft(); + } + } + + public void cancel() { + if (ready()) { + formSession.cancelEdit(); + } + } + + public void moveToSelected(List files) { + if (!ready() || formSession.mode() != StudioFormMode.EDITING) { + return; + } + formSession.updateDraft(current -> familySupport.moveToSelected(current, files)); + } + + public void moveToAvailable(List files) { + if (!ready() || formSession.mode() != StudioFormMode.EDITING) { + return; + } + formSession.updateDraft(current -> familySupport.moveToAvailable(current, files)); + } + + public void moveUp(int selectedIndex) { + if (!ready() || formSession.mode() != StudioFormMode.EDITING) { + return; + } + formSession.updateDraft(current -> familySupport.moveUp(current, selectedIndex)); + } + + public void moveDown(int selectedIndex) { + if (!ready() || formSession.mode() != StudioFormMode.EDITING) { + return; + } + formSession.updateDraft(current -> familySupport.moveDown(current, selectedIndex)); + } + + public AssetDetailsBankCompositionViewModel viewModel() { + if (!ready()) { + return new AssetDetailsBankCompositionViewModel( + false, + false, + List.of(), + List.of(), + new AssetDetailsBankCompositionCapacityState(0.0d, null, false, "", "")); + } + + final AssetDetailsBankCompositionDraft draft = formSession.draft(); + return new AssetDetailsBankCompositionViewModel( + formSession.mode() == StudioFormMode.EDITING, + formSession.isDirty(), + draft.availableFiles(), + draft.selectedFiles(), + familySupport.evaluate(draft)); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDraft.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDraft.java new file mode 100644 index 00000000..d018699a --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionDraft.java @@ -0,0 +1,43 @@ +package p.studio.workspaces.assets.details.bank; + +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; + +public record AssetDetailsBankCompositionDraft( + List availableFiles, + List selectedFiles) { + + public AssetDetailsBankCompositionDraft { + availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles")); + selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles")); + } + + public AssetDetailsBankCompositionDraft removeFromAvailable(List files) { + final var removed = new LinkedHashSet<>(Objects.requireNonNull(files, "files")); + return new AssetDetailsBankCompositionDraft( + availableFiles.stream().filter(file -> !removed.contains(file)).toList(), + selectedFiles); + } + + public AssetDetailsBankCompositionDraft appendToSelected(List files) { + final var nextSelected = new java.util.ArrayList<>(selectedFiles); + nextSelected.addAll(Objects.requireNonNull(files, "files")); + return new AssetDetailsBankCompositionDraft(availableFiles, nextSelected); + } + + public AssetDetailsBankCompositionDraft removeFromSelected(List files) { + final var removed = new LinkedHashSet<>(Objects.requireNonNull(files, "files")); + return new AssetDetailsBankCompositionDraft( + availableFiles, + selectedFiles.stream().filter(file -> !removed.contains(file)).toList()); + } + + public AssetDetailsBankCompositionDraft appendToAvailable(List files) { + final var nextAvailable = new java.util.ArrayList<>(availableFiles); + nextAvailable.addAll(Objects.requireNonNull(files, "files")); + return new AssetDetailsBankCompositionDraft(nextAvailable, selectedFiles); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionFamilySupport.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionFamilySupport.java new file mode 100644 index 00000000..507b396f --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionFamilySupport.java @@ -0,0 +1,31 @@ +package p.studio.workspaces.assets.details.bank; + +import p.packer.messages.assets.AssetFamilyCatalog; +import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails; +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +import java.util.List; + +interface AssetDetailsBankCompositionFamilySupport { + boolean supports(AssetFamilyCatalog assetFamily); + + AssetDetailsBankCompositionDraft createDraft(AssetWorkspaceAssetDetails details); + + AssetDetailsBankCompositionDraft moveToSelected( + AssetDetailsBankCompositionDraft draft, + List files); + + AssetDetailsBankCompositionDraft moveToAvailable( + AssetDetailsBankCompositionDraft draft, + List files); + + AssetDetailsBankCompositionDraft moveUp( + AssetDetailsBankCompositionDraft draft, + int selectedIndex); + + AssetDetailsBankCompositionDraft moveDown( + AssetDetailsBankCompositionDraft draft, + int selectedIndex); + + AssetDetailsBankCompositionCapacityState evaluate(AssetDetailsBankCompositionDraft draft); +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionViewModel.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionViewModel.java new file mode 100644 index 00000000..9e9fa075 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionViewModel.java @@ -0,0 +1,20 @@ +package p.studio.workspaces.assets.details.bank; + +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +import java.util.List; +import java.util.Objects; + +public record AssetDetailsBankCompositionViewModel( + boolean editing, + boolean dirty, + List availableFiles, + List selectedFiles, + AssetDetailsBankCompositionCapacityState capacityState) { + + public AssetDetailsBankCompositionViewModel { + availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles")); + selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles")); + capacityState = Objects.requireNonNull(capacityState, "capacityState"); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsFallbackBankCompositionFamilySupport.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsFallbackBankCompositionFamilySupport.java new file mode 100644 index 00000000..df06b817 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsFallbackBankCompositionFamilySupport.java @@ -0,0 +1,32 @@ +package p.studio.workspaces.assets.details.bank; + +import p.packer.messages.assets.AssetFamilyCatalog; +import p.studio.controls.banks.StudioAssetCapacitySeverity; +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +import java.util.List; + +final class AssetDetailsFallbackBankCompositionFamilySupport extends AssetDetailsAbstractBankCompositionFamilySupport { + @Override + public boolean supports(AssetFamilyCatalog assetFamily) { + return true; + } + + @Override + protected boolean canAdd( + AssetDetailsBankCompositionDraft currentDraft, + List accepted, + AssetWorkspaceBankCompositionFile candidate) { + return true; + } + + @Override + public AssetDetailsBankCompositionCapacityState evaluate(AssetDetailsBankCompositionDraft draft) { + return new AssetDetailsBankCompositionCapacityState( + 0.0d, + StudioAssetCapacitySeverity.GREEN, + false, + draft.selectedFiles().size() + " selected", + ""); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsSoundBankCompositionFamilySupport.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsSoundBankCompositionFamilySupport.java new file mode 100644 index 00000000..474efe85 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsSoundBankCompositionFamilySupport.java @@ -0,0 +1,52 @@ +package p.studio.workspaces.assets.details.bank; + +import p.packer.messages.assets.AssetFamilyCatalog; +import p.studio.controls.banks.StudioAssetCapacitySeverity; +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +import java.util.List; + +final class AssetDetailsSoundBankCompositionFamilySupport extends AssetDetailsAbstractBankCompositionFamilySupport { + private static final long MAX_BYTES = 1024L * 1024L; + + @Override + public boolean supports(AssetFamilyCatalog assetFamily) { + return assetFamily == AssetFamilyCatalog.SOUND_BANK; + } + + @Override + protected boolean canAdd( + AssetDetailsBankCompositionDraft currentDraft, + List accepted, + AssetWorkspaceBankCompositionFile candidate) { + final long usedBytes = selectedBytes(currentDraft.selectedFiles()) + selectedBytes(accepted); + return usedBytes + candidate.size() <= MAX_BYTES; + } + + @Override + public AssetDetailsBankCompositionCapacityState evaluate(AssetDetailsBankCompositionDraft draft) { + final long usedBytes = selectedBytes(draft.selectedFiles()); + final double progress = (double) usedBytes / (double) MAX_BYTES; + final boolean blocked = usedBytes >= MAX_BYTES; + return new AssetDetailsBankCompositionCapacityState( + progress, + severityFor(progress), + blocked, + usedBytes + " / " + MAX_BYTES + " bytes", + blocked ? "Sound bank byte capacity reached." : ""); + } + + private long selectedBytes(List files) { + return files.stream().mapToLong(AssetWorkspaceBankCompositionFile::size).sum(); + } + + private StudioAssetCapacitySeverity severityFor(double progress) { + if (progress >= 0.95d) { + return StudioAssetCapacitySeverity.RED; + } + if (progress >= 0.65d) { + return StudioAssetCapacitySeverity.ORANGE; + } + return StudioAssetCapacitySeverity.GREEN; + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsTileBankCompositionFamilySupport.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsTileBankCompositionFamilySupport.java new file mode 100644 index 00000000..08babd93 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsTileBankCompositionFamilySupport.java @@ -0,0 +1,68 @@ +package p.studio.workspaces.assets.details.bank; + +import p.packer.messages.assets.AssetFamilyCatalog; +import p.studio.controls.banks.StudioAssetCapacitySeverity; +import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails; +import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; + +import java.util.List; + +final class AssetDetailsTileBankCompositionFamilySupport extends AssetDetailsAbstractBankCompositionFamilySupport { + private int maxSlots = 64; + + @Override + public boolean supports(AssetFamilyCatalog assetFamily) { + return assetFamily == AssetFamilyCatalog.TILE_BANK; + } + + @Override + public AssetDetailsBankCompositionDraft createDraft(AssetWorkspaceAssetDetails details) { + maxSlots = computeMaxSlots(details); + return super.createDraft(details); + } + + @Override + protected boolean canAdd( + AssetDetailsBankCompositionDraft currentDraft, + List accepted, + AssetWorkspaceBankCompositionFile candidate) { + return currentDraft.selectedFiles().size() + accepted.size() < maxSlots; + } + + @Override + public AssetDetailsBankCompositionCapacityState evaluate(AssetDetailsBankCompositionDraft draft) { + final int used = draft.selectedFiles().size(); + final double progress = maxSlots <= 0 ? 0.0d : (double) used / (double) maxSlots; + final boolean blocked = used >= maxSlots; + return new AssetDetailsBankCompositionCapacityState( + progress, + severityFor(progress), + blocked, + used + " / " + maxSlots, + blocked ? "Tile bank slot capacity reached." : ""); + } + + private int computeMaxSlots(AssetWorkspaceAssetDetails details) { + final String tileSizeValue = details.metadataFields().stream() + .filter(field -> field.key().equals("tile_size")) + .map(field -> field.value()) + .findFirst() + .orElse("16x16"); + final int tileSize = switch (tileSizeValue) { + case "8x8" -> 8; + case "32x32" -> 32; + default -> 16; + }; + return Math.max(1, (256 / tileSize) * (256 / tileSize)); + } + + private StudioAssetCapacitySeverity severityFor(double progress) { + if (progress >= 0.95d) { + return StudioAssetCapacitySeverity.RED; + } + if (progress >= 0.65d) { + return StudioAssetCapacitySeverity.ORANGE; + } + return StudioAssetCapacitySeverity.GREEN; + } +} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinatorTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinatorTest.java new file mode 100644 index 00000000..6084fdff --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinatorTest.java @@ -0,0 +1,121 @@ +package p.studio.workspaces.assets.details.bank; + +import org.junit.jupiter.api.Test; +import p.packer.dtos.PackerCodecConfigurationFieldDTO; +import p.packer.messages.AssetReference; +import p.packer.messages.assets.AssetFamilyCatalog; +import p.packer.messages.assets.OutputCodecCatalog; +import p.packer.messages.assets.OutputFormatCatalog; +import p.packer.messages.assets.PackerCodecConfigurationFieldType; +import p.studio.workspaces.assets.messages.*; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +final class AssetDetailsBankCompositionCoordinatorTest { + @Test + void tileFamilyBlocksSelectionAfterSlotCapacity() { + final AssetDetailsBankCompositionCoordinator coordinator = new AssetDetailsBankCompositionCoordinator(); + coordinator.replaceDetails(tileDetails("32x32", 65)); + coordinator.beginEdit(); + + coordinator.moveToSelected(coordinator.viewModel().availableFiles()); + + assertEquals(64, coordinator.viewModel().selectedFiles().size()); + assertTrue(coordinator.viewModel().capacityState().blocked()); + } + + @Test + void soundFamilyBlocksSelectionAfterByteCapacity() { + final AssetDetailsBankCompositionCoordinator coordinator = new AssetDetailsBankCompositionCoordinator(); + coordinator.replaceDetails(soundDetails(600_000L, 600_000L)); + coordinator.beginEdit(); + + coordinator.moveToSelected(coordinator.viewModel().availableFiles()); + + assertEquals(1, coordinator.viewModel().selectedFiles().size()); + assertTrue(coordinator.viewModel().capacityState().progress() > 0.5d); + } + + @Test + void resetAndCancelFollowStudioFormSessionLifecycle() { + final AssetDetailsBankCompositionCoordinator coordinator = new AssetDetailsBankCompositionCoordinator(); + coordinator.replaceDetails(tileDetails("16x16", 2)); + coordinator.beginEdit(); + + final var firstFile = coordinator.viewModel().availableFiles().subList(0, 1); + coordinator.moveToSelected(firstFile); + assertTrue(coordinator.viewModel().dirty()); + + coordinator.reset(); + assertFalse(coordinator.viewModel().dirty()); + + coordinator.moveToSelected(firstFile); + assertTrue(coordinator.viewModel().dirty()); + coordinator.cancel(); + assertFalse(coordinator.viewModel().editing()); + assertFalse(coordinator.viewModel().dirty()); + } + + private AssetWorkspaceAssetDetails tileDetails(String tileSize, int fileCount) { + return new AssetWorkspaceAssetDetails( + summary(AssetFamilyCatalog.TILE_BANK), + List.of(), + OutputFormatCatalog.TILES_INDEXED_V1, + OutputCodecCatalog.NONE, + List.of(OutputCodecCatalog.NONE), + Map.of(OutputCodecCatalog.NONE, List.of()), + List.of(new PackerCodecConfigurationFieldDTO("tile_size", "Tile Size", PackerCodecConfigurationFieldType.TEXT, tileSize, true, List.of())), + new AssetWorkspaceBankCompositionDetails( + files("tile", fileCount, 1024L), + List.of(), + 0L), + Map.of()); + } + + private AssetWorkspaceAssetDetails soundDetails(long firstSize, long secondSize) { + return new AssetWorkspaceAssetDetails( + summary(AssetFamilyCatalog.SOUND_BANK), + List.of(), + OutputFormatCatalog.SOUND_V1, + OutputCodecCatalog.NONE, + List.of(OutputCodecCatalog.NONE), + Map.of(OutputCodecCatalog.NONE, List.of()), + List.of(), + new AssetWorkspaceBankCompositionDetails( + List.of( + new AssetWorkspaceBankCompositionFile("a.wav", "a.wav", firstSize, 1L, null, Map.of()), + new AssetWorkspaceBankCompositionFile("b.wav", "b.wav", secondSize, 1L, null, Map.of())), + List.of(), + 0L), + Map.of()); + } + + private List files(String prefix, int count, long size) { + return java.util.stream.IntStream.range(0, count) + .mapToObj(index -> new AssetWorkspaceBankCompositionFile( + prefix + "-" + index + ".png", + prefix + "-" + index + ".png", + size, + 1L, + null, + Map.of())) + .toList(); + } + + private AssetWorkspaceAssetSummary summary(AssetFamilyCatalog family) { + return new AssetWorkspaceAssetSummary( + AssetReference.forAssetId(1), + "bank", + AssetWorkspaceAssetState.REGISTERED, + AssetWorkspaceBuildParticipation.INCLUDED, + 1, + family, + Path.of("/tmp/bank"), + false, + false); + } +}