asset details (WIP)

This commit is contained in:
bQUARKz 2026-03-19 15:27:17 +00:00
parent 236fa04120
commit 64df57a774
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
21 changed files with 3194 additions and 1968 deletions

View File

@ -28,6 +28,16 @@ public class PackerTileBankWalker extends PackerAbstractBankWalker<PackerTileBan
return SUPPORTED_MIME_TYPES;
}
@Override
protected boolean canReuseMetadata(
final PackerFileProbe fileProbe,
final PackerFileCacheEntry cachedEntry) {
if (!super.canReuseMetadata(fileProbe, cachedEntry)) {
return false;
}
return hasReusableTileMetadata(cachedEntry.metadata());
}
@Override
protected PackerProbeResult processFileProbe(
final PackerFileProbe fileProbe,
@ -130,6 +140,26 @@ public class PackerTileBankWalker extends PackerAbstractBankWalker<PackerTileBan
return new PackerProbeResult(fileProbe, Map.of("tile", tile, "palette", palette), diagnostics);
}
@SuppressWarnings("unchecked")
private boolean hasReusableTileMetadata(final Map<String, Object> metadata) {
final Object tileValue = metadata.get("tile");
if (!(tileValue instanceof Map<?, ?> tile)) {
return false;
}
final Object paletteValue = metadata.get("palette");
if (!(paletteValue instanceof Map<?, ?> palette)) {
return false;
}
final Object width = tile.get("width");
final Object height = tile.get("height");
final Object indices = tile.get("paletteIndices");
final Object colors = palette.get("originalArgb8888");
return width instanceof Number
&& height instanceof Number
&& indices instanceof List<?>
&& colors instanceof List<?>;
}
/*
private void serializeProbeArtifacts(
final PackerFileProbe fileProbe,

View File

@ -0,0 +1,64 @@
package p.packer.repositories;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.models.PackerAssetCacheEntry;
import p.packer.models.PackerFileCacheEntry;
import p.packer.models.PackerTileIndexedV1;
import p.packer.models.PackerTileBankRequirements;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
final class PackerTileBankWalkerTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
@TempDir
Path tempDir;
@Test
void invalidatesCachedTileMetadataWhenPaletteIndicesAreMissing() throws Exception {
final Path assetRoot = Files.createDirectories(tempDir.resolve("asset"));
final Path filePath = assetRoot.resolve("tile.png");
writeTile(filePath);
final PackerTileBankWalker walker = new PackerTileBankWalker(MAPPER);
final PackerAssetCacheEntry priorCache = new PackerAssetCacheEntry(
1,
"contract",
List.of(new PackerFileCacheEntry(
"tile.png",
"image/png",
Files.size(filePath),
Files.getLastModifiedTime(filePath).toMillis(),
null,
Map.of(
"tile", Map.of("width", 16, "height", 16),
"palette", Map.of("originalArgb8888", List.of(0xFFFFFFFF))),
List.of())));
final var result = walker.walk(assetRoot, new PackerTileBankRequirements(16), Optional.of(priorCache));
final PackerTileIndexedV1 tile = (PackerTileIndexedV1) result.probeResults().getFirst().metadata().get("tile");
assertNotNull(tile);
assertTrue(tile.paletteIndices().length > 0);
}
private void writeTile(Path path) throws Exception {
final BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
image.setRGB(x, y, (x + y) % 2 == 0 ? 0xFFFF0000 : 0xFF00FF00);
}
}
ImageIO.write(image, "png", path.toFile());
}
}

View File

@ -33,6 +33,8 @@ public abstract class StudioDualListView<T> extends HBox {
private Consumer<List<T>> onMoveToRight = ignored -> { };
private Consumer<List<T>> onMoveToLeft = ignored -> { };
private Consumer<T> onLeftSelectionChanged = ignored -> { };
private Consumer<T> onRightSelectionChanged = ignored -> { };
private IntConsumer onMoveUp = ignored -> { };
private IntConsumer onMoveDown = ignored -> { };
private boolean interactionEnabled = true;
@ -95,6 +97,8 @@ public abstract class StudioDualListView<T> extends HBox {
updateActionState();
leftListView.getSelectionModel().getSelectedItems().addListener((javafx.collections.ListChangeListener<? super T>) ignored -> updateActionState());
rightListView.getSelectionModel().getSelectedItems().addListener((javafx.collections.ListChangeListener<? super T>) ignored -> updateActionState());
leftListView.getSelectionModel().selectedItemProperty().addListener((ignored, oldValue, newValue) -> onLeftSelectionChanged.accept(newValue));
rightListView.getSelectionModel().selectedItemProperty().addListener((ignored, oldValue, newValue) -> onRightSelectionChanged.accept(newValue));
}
public void setLeftItems(List<T> items) {
@ -133,6 +137,14 @@ public abstract class StudioDualListView<T> extends HBox {
this.onMoveToLeft = Objects.requireNonNull(onMoveToLeft, "onMoveToLeft");
}
public void setOnLeftSelectionChanged(Consumer<T> onLeftSelectionChanged) {
this.onLeftSelectionChanged = Objects.requireNonNull(onLeftSelectionChanged, "onLeftSelectionChanged");
}
public void setOnRightSelectionChanged(Consumer<T> onRightSelectionChanged) {
this.onRightSelectionChanged = Objects.requireNonNull(onRightSelectionChanged, "onRightSelectionChanged");
}
public void setOnMoveUp(IntConsumer onMoveUp) {
this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp");
}
@ -149,6 +161,14 @@ public abstract class StudioDualListView<T> extends HBox {
rightTitleLabel.setText(Objects.requireNonNull(title, "title"));
}
public void selectLeftItem(T item) {
selectItem(leftListView, leftItems, item);
}
public void selectRightItem(T item) {
selectItem(rightListView, rightItems, item);
}
protected abstract String itemText(T item);
protected Node itemGraphic(T item) {
@ -222,6 +242,22 @@ public abstract class StudioDualListView<T> extends HBox {
}
}
private void selectItem(ListView<T> listView, List<T> items, T item) {
final var selectionModel = listView.getSelectionModel();
if (item == null) {
selectionModel.clearSelection();
return;
}
final int index = items.indexOf(item);
if (index < 0) {
selectionModel.clearSelection();
return;
}
selectionModel.clearSelection();
selectionModel.select(index);
listView.scrollTo(index);
}
private Node createIndexedGraphic(Node graphic, int index) {
final Label chip = new Label((index + 1) + ".");
chip.getStyleClass().add("studio-dual-list-index-chip");

View File

@ -97,6 +97,7 @@ public enum I18n {
ASSETS_SECTION_SUMMARY("assets.section.summary"),
ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"),
ASSETS_SECTION_BANK_COMPOSITION("assets.section.bankComposition"),
ASSETS_SECTION_PALETTE_OVERHAULING("assets.section.paletteOverhauling"),
ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"),
ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"),
ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"),
@ -187,6 +188,11 @@ public enum I18n {
ASSETS_DETAILS_BANK_COMPOSITION_SELECTED("assets.details.bankComposition.selected"),
ASSETS_DETAILS_BANK_COMPOSITION_READONLY_HINT("assets.details.bankComposition.readonlyHint"),
ASSETS_DETAILS_BANK_COMPOSITION_EDITING_HINT("assets.details.bankComposition.editingHint"),
ASSETS_PALETTE_OVERHAULING_AVAILABLE("assets.paletteOverhauling.available"),
ASSETS_PALETTE_OVERHAULING_SELECTED("assets.paletteOverhauling.selected"),
ASSETS_PALETTE_OVERHAULING_TILE_SELECTOR("assets.paletteOverhauling.tileSelector"),
ASSETS_PALETTE_OVERHAULING_APPLIED_PALETTE("assets.paletteOverhauling.appliedPalette"),
ASSETS_PALETTE_OVERHAULING_PREVIEW_EMPTY("assets.paletteOverhauling.previewEmpty"),
ASSETS_DETAILS_CODEC_CONFIGURATION_EMPTY("assets.details.codecConfiguration.empty"),
ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"),
ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"),

View File

@ -21,6 +21,7 @@ import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.dialogs.AssetDiagnosticsDialog;
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl;
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
import p.studio.workspaces.assets.details.palette.AssetDetailsPaletteOverhaulingControl;
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
@ -52,6 +53,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
private final AssetDetailsSummaryControl summaryControl;
private final AssetDetailsContractControl contractControl;
private final AssetDetailsBankCompositionControl bankCompositionControl;
private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
private final VBox actionsContent = new VBox(10);
private final ScrollPane actionsScroll = new ScrollPane();
private final VBox actionsSection;
@ -71,6 +73,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
this.paletteOverhaulingControl = new AssetDetailsPaletteOverhaulingControl(workspaceBus);
this.actionsSection = createActionsSection();
getStyleClass().add("assets-workspace-pane");
@ -254,7 +257,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
renderActions();
if (!readyMounted) {
readyMounted = true;
detailsContent.getChildren().setAll(primarySectionsRow, contractControl, bankCompositionControl);
detailsContent.getChildren().setAll(primarySectionsRow, contractControl, bankCompositionControl, paletteOverhaulingControl);
}
syncActionsSectionHeight(summaryControl.getHeight());
}

View File

@ -0,0 +1,183 @@
package p.studio.workspaces.assets.details.palette;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import p.studio.Container;
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
import p.studio.controls.forms.StudioFormMode;
import p.studio.controls.forms.StudioFormSection;
import p.studio.events.StudioWorkspaceEventBus;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
import p.studio.workspaces.framework.StudioSubscriptionBag;
import java.util.Objects;
public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSection {
private static final String SECTION_ID = "asset-details.palette-overhauling";
private final StudioWorkspaceEventBus workspaceBus;
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
private final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
private final AssetDetailsPaletteOverhaulingDualListView dualListView = new AssetDetailsPaletteOverhaulingDualListView();
private final AssetDetailsPaletteOverhaulingPreviewPane previewPane = new AssetDetailsPaletteOverhaulingPreviewPane();
private final HBox body = new HBox(16, dualListView, previewPane);
private final VBox content = new VBox(12, body);
private AssetWorkspaceDetailsViewState viewState;
public AssetDetailsPaletteOverhaulingControl(StudioWorkspaceEventBus workspaceBus) {
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
body.getStyleClass().add("assets-details-palette-overhauling-body");
content.setFillWidth(true);
HBox.setHgrow(dualListView, Priority.ALWAYS);
HBox.setHgrow(previewPane, Priority.ALWAYS);
dualListView.setMaxWidth(Double.MAX_VALUE);
previewPane.setMaxWidth(Double.MAX_VALUE);
}
@Override
public void subscribe() {
subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> {
viewState = event.viewState();
coordinator.replaceDetails(viewState == null ? null : viewState.selectedAssetDetails());
if (viewState == null || viewState.selectedAssetReference() == null) {
publishEditScope(workspaceBus, currentScopeKey(), null);
}
renderSection();
}));
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
event -> handleEditScopeChanged(event, currentScopeKey())));
}
@Override
public void unsubscribe() {
subscriptions.clear();
}
@Override
protected void renderSection() {
if (viewState == null || viewState.selectedAssetDetails() == null || !coordinator.ready()) {
clearRenderedSection();
return;
}
final AssetDetailsPaletteOverhaulingViewModel viewModel = coordinator.viewModel();
dualListView.setLeftItems(viewModel.availableFiles());
dualListView.setRightItems(viewModel.selectedFiles());
dualListView.setInteractionEnabled(viewModel.editing());
dualListView.setMoveToRightAllowed(viewModel.selectedFiles().size() < 64);
dualListView.setOnMoveToRight(items -> {
coordinator.moveToSelected(items);
rerenderPreservingScrollPosition();
});
dualListView.setOnMoveToLeft(items -> {
coordinator.moveToAvailable(items);
rerenderPreservingScrollPosition();
});
dualListView.setOnMoveUp(index -> { });
dualListView.setOnMoveDown(index -> { });
dualListView.setOnLeftSelectionChanged(item -> {
if (item != null) {
coordinator.selectPreviewPalette(item);
previewPane.render(
coordinator.viewModel().previewTileFiles(),
coordinator.viewModel().previewTileFile(),
coordinator.viewModel().previewPaletteFile(),
coordinator.viewModel().renderableFileCount());
}
});
dualListView.setOnRightSelectionChanged(item -> {
if (item != null) {
coordinator.selectPreviewPalette(item);
previewPane.render(
coordinator.viewModel().previewTileFiles(),
coordinator.viewModel().previewTileFile(),
coordinator.viewModel().previewPaletteFile(),
coordinator.viewModel().renderableFileCount());
}
});
if (viewModel.previewPaletteFile() != null) {
if (viewModel.selectedFiles().contains(viewModel.previewPaletteFile())) {
dualListView.selectRightItem(viewModel.previewPaletteFile());
} else {
dualListView.selectLeftItem(viewModel.previewPaletteFile());
}
}
previewPane.setOnPreviewTileChanged(file -> {
if (file != null) {
coordinator.selectPreviewTile(file);
previewPane.render(
coordinator.viewModel().previewTileFiles(),
coordinator.viewModel().previewTileFile(),
coordinator.viewModel().previewPaletteFile(),
coordinator.viewModel().renderableFileCount());
}
});
previewPane.render(
viewModel.previewTileFiles(),
viewModel.previewTileFile(),
viewModel.previewPaletteFile(),
viewModel.renderableFileCount());
renderFormSection(content);
}
@Override
protected String sectionTitle() {
return Container.i18n().text(I18n.ASSETS_SECTION_PALETTE_OVERHAULING);
}
@Override
protected StudioFormMode formMode() {
return coordinator.ready() && coordinator.viewModel().editing() ? StudioFormMode.EDITING : StudioFormMode.READ_ONLY;
}
@Override
protected boolean isDirty() {
return coordinator.ready() && coordinator.viewModel().dirty();
}
@Override
protected void beginEdit() {
coordinator.beginEdit();
publishEditScope(workspaceBus, currentScopeKey(), SECTION_ID);
}
@Override
protected void apply() {
coordinator.apply();
publishEditScope(workspaceBus, currentScopeKey(), null);
renderSection();
}
@Override
protected void reset() {
coordinator.reset();
}
@Override
protected void cancel() {
coordinator.cancel();
publishEditScope(workspaceBus, currentScopeKey(), null);
}
@Override
protected String sectionStyleClass() {
return "assets-details-palette-overhauling-section";
}
@Override
protected String formSectionId() {
return SECTION_ID;
}
private String currentScopeKey() {
return viewState == null || viewState.selectedAssetReference() == null
? null
: "asset-details:" + viewState.selectedAssetReference();
}
}

View File

@ -0,0 +1,209 @@
package p.studio.workspaces.assets.details.palette;
import p.packer.messages.assets.AssetFamilyCatalog;
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.nio.ByteBuffer;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public final class AssetDetailsPaletteOverhaulingCoordinator {
private StudioFormSession<AssetDetailsPaletteOverhaulingDraft> formSession;
public void replaceDetails(AssetWorkspaceAssetDetails details) {
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.TILE_BANK) {
formSession = null;
return;
}
final List<AssetWorkspaceBankCompositionFile> allFiles = paletteFiles(details);
final List<AssetWorkspaceBankCompositionFile> previewTileFiles = details.bankComposition().selectedFiles().stream()
.filter(AssetDetailsPaletteOverhaulingCoordinator::supportsPalettePreview)
.toList();
final Map<String, AssetWorkspaceBankCompositionFile> filesByPath = allFiles.stream()
.collect(java.util.stream.Collectors.toMap(
AssetWorkspaceBankCompositionFile::path,
file -> file,
(left, right) -> left,
LinkedHashMap::new));
final List<AssetWorkspaceBankCompositionFile> initialSelected = new ArrayList<>();
final List<AssetWorkspaceBankCompositionFile> available = new ArrayList<>(allFiles);
available.removeAll(initialSelected);
final AssetDetailsPaletteOverhaulingDraft source = new AssetDetailsPaletteOverhaulingDraft(
previewTileFiles,
allFiles,
available,
initialSelected,
previewTileFiles.isEmpty() ? null : previewTileFiles.getFirst().path(),
null);
if (formSession == null) {
formSession = new StudioFormSession<>(source);
return;
}
if (!Objects.equals(formSession.source(), source)) {
formSession.replaceSource(source);
}
}
public boolean ready() {
return formSession != 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<AssetWorkspaceBankCompositionFile> files) {
if (ready() && formSession.mode() == StudioFormMode.EDITING) {
formSession.updateDraft(current -> current.moveToSelected(files));
}
}
public void moveToAvailable(List<AssetWorkspaceBankCompositionFile> files) {
if (ready() && formSession.mode() == StudioFormMode.EDITING) {
formSession.updateDraft(current -> current.moveToAvailable(files));
}
}
public void selectPreviewTile(AssetWorkspaceBankCompositionFile file) {
if (ready() && file != null) {
formSession.updateDraft(current -> current.selectPreviewTile(file.path()));
}
}
public void selectPreviewPalette(AssetWorkspaceBankCompositionFile file) {
if (ready() && file != null) {
formSession.updateDraft(current -> current.selectPreviewPalette(file.path()));
}
}
public AssetDetailsPaletteOverhaulingViewModel viewModel() {
if (!ready()) {
return new AssetDetailsPaletteOverhaulingViewModel(false, false, List.of(), List.of(), List.of(), List.of(), null, null, 0);
}
final AssetDetailsPaletteOverhaulingDraft draft = formSession.draft();
return new AssetDetailsPaletteOverhaulingViewModel(
formSession.mode() == StudioFormMode.EDITING,
formSession.isDirty(),
draft.previewTileFiles(),
draft.allFiles(),
draft.availableFiles(),
draft.selectedFiles(),
fileByPath(draft.previewTileFiles(), draft.previewTilePath()),
fileByPath(draft.allFiles(), draft.previewPalettePath()),
draft.allFiles().size());
}
static boolean supportsPalettePreview(AssetWorkspaceBankCompositionFile file) {
if (file == null) {
return false;
}
return nestedMap(file.metadata(), "tile") != null && nestedMap(file.metadata(), "palette") != null;
}
@SuppressWarnings("unchecked")
private static Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
final Object value = metadata.get(key);
if (value instanceof Map<?, ?> nested) {
return (Map<String, Object>) nested;
}
return null;
}
private static List<AssetWorkspaceBankCompositionFile> paletteFiles(AssetWorkspaceAssetDetails details) {
final Map<String, AssetWorkspaceBankCompositionFile> ordered = new LinkedHashMap<>();
// `bankComposition` files are already projected by the packer read model from the current cache/walk state.
for (AssetWorkspaceBankCompositionFile file : details.bankComposition().availableFiles()) {
if (supportsPalettePreview(file)) {
ordered.putIfAbsent(paletteHash(file), canonicalPaletteFile(file));
}
}
for (AssetWorkspaceBankCompositionFile file : details.bankComposition().selectedFiles()) {
if (supportsPalettePreview(file)) {
ordered.putIfAbsent(paletteHash(file), canonicalPaletteFile(file));
}
}
return List.copyOf(ordered.values());
}
@SuppressWarnings("unchecked")
private static AssetWorkspaceBankCompositionFile canonicalPaletteFile(AssetWorkspaceBankCompositionFile file) {
final String hash = paletteHash(file);
final Map<String, Object> metadata = new LinkedHashMap<>(file.metadata());
metadata.put("paletteHash", hash);
return new AssetWorkspaceBankCompositionFile(
hash,
hash,
file.size(),
file.lastModified(),
file.fingerprint(),
metadata);
}
@SuppressWarnings("unchecked")
private static String paletteHash(AssetWorkspaceBankCompositionFile file) {
final Map<String, Object> palette = nestedMap(file.metadata(), "palette");
final Object convertedValue = palette == null ? null : palette.get("convertedRgb565");
final List<Integer> colors = convertedValue instanceof List<?> converted
? (List<Integer>) converted
: List.of();
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (int index = 0; index < 15; index++) {
final int color = index < colors.size() && colors.get(index) != null ? colors.get(index) : 0;
digest.update(ByteBuffer.allocate(4).putInt(color).array());
}
long value = 0L;
final byte[] bytes = digest.digest();
for (int index = 0; index < 8; index++) {
value = (value << 8) | Byte.toUnsignedLong(bytes[index]);
}
value &= 0x0FFFFFFFFFFFFFFFL;
final String base36 = Long.toUnsignedString(value, 36).toUpperCase();
return base36.length() >= 10
? base36.substring(0, 10)
: "0".repeat(10 - base36.length()) + base36;
} catch (Exception exception) {
throw new IllegalStateException("Unable to hash palette colors", exception);
}
}
private static AssetWorkspaceBankCompositionFile fileByPath(List<AssetWorkspaceBankCompositionFile> files, String path) {
if (path == null) {
return null;
}
return files.stream()
.filter(file -> path.equals(file.path()))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,95 @@
package p.studio.workspaces.assets.details.palette;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public record AssetDetailsPaletteOverhaulingDraft(
List<AssetWorkspaceBankCompositionFile> previewTileFiles,
List<AssetWorkspaceBankCompositionFile> allFiles,
List<AssetWorkspaceBankCompositionFile> availableFiles,
List<AssetWorkspaceBankCompositionFile> selectedFiles,
String previewTilePath,
String previewPalettePath) {
private static final int MIN_SELECTED = 1;
private static final int MAX_SELECTED = 64;
public AssetDetailsPaletteOverhaulingDraft {
previewTileFiles = List.copyOf(Objects.requireNonNull(previewTileFiles, "previewTileFiles"));
allFiles = List.copyOf(Objects.requireNonNull(allFiles, "allFiles"));
availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles"));
selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles"));
previewTilePath = normalize(previewTilePath);
previewPalettePath = normalize(previewPalettePath);
}
public AssetDetailsPaletteOverhaulingDraft moveToSelected(List<AssetWorkspaceBankCompositionFile> files) {
final List<AssetWorkspaceBankCompositionFile> accepted = new ArrayList<>();
for (AssetWorkspaceBankCompositionFile file : files) {
if (file == null || !availableFiles.contains(file) || selectedFiles.contains(file)) {
continue;
}
if (selectedFiles.size() + accepted.size() >= MAX_SELECTED) {
break;
}
accepted.add(file);
}
if (accepted.isEmpty()) {
return this;
}
final List<AssetWorkspaceBankCompositionFile> nextAvailable = new ArrayList<>(availableFiles);
nextAvailable.removeAll(accepted);
final List<AssetWorkspaceBankCompositionFile> nextSelected = new ArrayList<>(selectedFiles);
nextSelected.addAll(accepted);
final String nextPreviewPalettePath = previewPalettePath == null ? accepted.getFirst().path() : previewPalettePath;
return new AssetDetailsPaletteOverhaulingDraft(previewTileFiles, allFiles, nextAvailable, nextSelected, previewTilePath, nextPreviewPalettePath);
}
public AssetDetailsPaletteOverhaulingDraft moveToAvailable(List<AssetWorkspaceBankCompositionFile> files) {
final List<AssetWorkspaceBankCompositionFile> removed = new ArrayList<>();
for (AssetWorkspaceBankCompositionFile file : files) {
if (file != null && selectedFiles.contains(file)) {
removed.add(file);
}
}
if (removed.isEmpty() || selectedFiles.size() - removed.size() < MIN_SELECTED) {
return this;
}
final List<AssetWorkspaceBankCompositionFile> nextSelected = new ArrayList<>(selectedFiles);
nextSelected.removeAll(removed);
final List<AssetWorkspaceBankCompositionFile> nextAvailable = new ArrayList<>(availableFiles);
nextAvailable.addAll(removed);
final String nextPreviewPalettePath = removed.stream().anyMatch(file -> file.path().equals(previewPalettePath))
? nextSelected.getFirst().path()
: previewPalettePath;
return new AssetDetailsPaletteOverhaulingDraft(previewTileFiles, allFiles, nextAvailable, nextSelected, previewTilePath, nextPreviewPalettePath);
}
public AssetDetailsPaletteOverhaulingDraft selectPreviewTile(String path) {
final String normalized = normalize(path);
if (normalized == null || previewTileFiles.stream().noneMatch(file -> file.path().equals(normalized))) {
return this;
}
return new AssetDetailsPaletteOverhaulingDraft(previewTileFiles, allFiles, availableFiles, selectedFiles, normalized, previewPalettePath);
}
public AssetDetailsPaletteOverhaulingDraft selectPreviewPalette(String path) {
final String normalized = normalize(path);
if (normalized == null || allFiles.stream().noneMatch(file -> file.path().equals(normalized))) {
return this;
}
return new AssetDetailsPaletteOverhaulingDraft(previewTileFiles, allFiles, availableFiles, selectedFiles, previewTilePath, normalized);
}
private static String normalize(String path) {
if (path == null) {
return null;
}
final String trimmed = path.trim();
return trimmed.isBlank() ? null : trimmed;
}
}

View File

@ -0,0 +1,96 @@
package p.studio.workspaces.assets.details.palette;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import p.studio.Container;
import p.studio.controls.banks.StudioDualListView;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
import java.util.List;
import java.util.Map;
public final class AssetDetailsPaletteOverhaulingDualListView extends StudioDualListView<AssetWorkspaceBankCompositionFile> {
public AssetDetailsPaletteOverhaulingDualListView() {
setLeftTitle(Container.i18n().text(I18n.ASSETS_PALETTE_OVERHAULING_AVAILABLE));
setRightTitle(Container.i18n().text(I18n.ASSETS_PALETTE_OVERHAULING_SELECTED));
}
@Override
protected String itemText(AssetWorkspaceBankCompositionFile item) {
return "";
}
@Override
protected Node itemGraphic(AssetWorkspaceBankCompositionFile item) {
final Label hashLabel = new Label(item.displayName());
hashLabel.getStyleClass().add("assets-details-palette-hash");
final HBox strip = createPaletteStrip(item);
final HBox root = new HBox(8, hashLabel, strip);
root.getStyleClass().add("assets-details-palette-item");
return root;
}
private HBox createPaletteStrip(AssetWorkspaceBankCompositionFile item) {
final HBox strip = new HBox(0);
strip.getStyleClass().add("assets-details-palette-strip");
final List<Integer> colors = paletteDisplayColors(item.metadata());
for (int index = 0; index < 15; index++) {
final Region swatch = new Region();
swatch.getStyleClass().add("assets-details-palette-swatch");
swatch.setMinSize(12.0d, 18.0d);
swatch.setPrefSize(12.0d, 18.0d);
swatch.setMaxSize(12.0d, 18.0d);
swatch.setStyle("-fx-background-color: " + cssColor(colorAt(colors, index)) + ";");
strip.getChildren().add(swatch);
}
return strip;
}
@SuppressWarnings("unchecked")
private List<Integer> paletteDisplayColors(Map<String, Object> metadata) {
final Object paletteValue = metadata.get("palette");
if (!(paletteValue instanceof Map<?, ?> palette)) {
return List.of();
}
final Object convertedValue = palette.get("convertedRgb565");
if (convertedValue instanceof List<?> converted) {
return ((List<Integer>) converted).stream()
.map(this::rgb565ToArgb8888)
.toList();
}
return List.of();
}
private int colorAt(List<Integer> colors, int index) {
if (index < 0 || index >= colors.size()) {
return 0x00000000;
}
final Integer color = colors.get(index);
return color == null ? 0x00000000 : color;
}
private String cssColor(int argb) {
final int alpha = (argb >>> 24) & 0xFF;
if (alpha == 0) {
return "transparent";
}
final int red = (argb >>> 16) & 0xFF;
final int green = (argb >>> 8) & 0xFF;
final int blue = argb & 0xFF;
return String.format("#%02X%02X%02X", red, green, blue);
}
private int rgb565ToArgb8888(Integer rgb565) {
if (rgb565 == null) {
return 0x00000000;
}
final int value = rgb565 & 0xFFFF;
final int red = ((value >> 11) & 0x1F) * 255 / 31;
final int green = ((value >> 5) & 0x3F) * 255 / 63;
final int blue = (value & 0x1F) * 255 / 31;
return 0xFF000000 | (red << 16) | (green << 8) | blue;
}
}

View File

@ -0,0 +1,263 @@
package p.studio.workspaces.assets.details.palette;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import p.studio.Container;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
private static final double PREVIEW_SIZE = 192.0d;
private static final double PREVIEW_FRAME_SIZE = PREVIEW_SIZE + 16.0d;
private final Label selectorLabel = new Label(Container.i18n().text(I18n.ASSETS_PALETTE_OVERHAULING_TILE_SELECTOR));
private final ComboBox<AssetWorkspaceBankCompositionFile> tileSelector = new ComboBox<>();
private final Canvas tileCanvas = new Canvas(PREVIEW_SIZE, PREVIEW_SIZE);
private final Label emptyLabel = new Label(Container.i18n().text(I18n.ASSETS_PALETTE_OVERHAULING_PREVIEW_EMPTY));
private final Label paletteLabel = new Label(Container.i18n().text(I18n.ASSETS_PALETTE_OVERHAULING_APPLIED_PALETTE));
private final HBox paletteStrip = new HBox(0);
private Consumer<AssetWorkspaceBankCompositionFile> onPreviewTileChanged = ignored -> { };
private boolean suppressSelectorEvents;
public AssetDetailsPaletteOverhaulingPreviewPane() {
getStyleClass().add("assets-details-palette-preview");
setSpacing(10);
setAlignment(Pos.TOP_CENTER);
setMinWidth(PREVIEW_FRAME_SIZE);
setPrefWidth(PREVIEW_FRAME_SIZE);
setMaxWidth(PREVIEW_FRAME_SIZE);
selectorLabel.getStyleClass().add("assets-details-palette-preview-label");
paletteLabel.getStyleClass().add("assets-details-palette-preview-label");
selectorLabel.setMaxWidth(Double.MAX_VALUE);
selectorLabel.setAlignment(Pos.CENTER_LEFT);
paletteLabel.setMaxWidth(Double.MAX_VALUE);
paletteLabel.setAlignment(Pos.CENTER);
tileSelector.setMinWidth(PREVIEW_FRAME_SIZE);
tileSelector.setPrefWidth(PREVIEW_FRAME_SIZE);
tileSelector.setMaxWidth(PREVIEW_FRAME_SIZE);
tileSelector.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
@Override
protected void updateItem(AssetWorkspaceBankCompositionFile item, boolean empty) {
super.updateItem(item, empty);
setText(empty || item == null ? null : item.displayName());
}
});
tileSelector.setButtonCell(new javafx.scene.control.ListCell<>() {
@Override
protected void updateItem(AssetWorkspaceBankCompositionFile item, boolean empty) {
super.updateItem(item, empty);
setText(empty || item == null ? null : item.displayName());
}
});
tileSelector.valueProperty().addListener((ignored, oldValue, newValue) -> {
if (!suppressSelectorEvents && newValue != null) {
onPreviewTileChanged.accept(newValue);
}
});
final VBox previewSurface = new VBox(emptyLabel, tileCanvas);
previewSurface.getStyleClass().add("assets-details-palette-preview-surface");
previewSurface.setAlignment(Pos.CENTER);
previewSurface.setMinWidth(PREVIEW_FRAME_SIZE);
previewSurface.setPrefWidth(PREVIEW_FRAME_SIZE);
previewSurface.setMaxWidth(PREVIEW_FRAME_SIZE);
previewSurface.setMinHeight(PREVIEW_FRAME_SIZE);
previewSurface.setPrefHeight(PREVIEW_FRAME_SIZE);
previewSurface.setMaxHeight(PREVIEW_FRAME_SIZE);
VBox.setVgrow(previewSurface, Priority.ALWAYS);
emptyLabel.getStyleClass().add("assets-details-section-message");
paletteStrip.getStyleClass().add("assets-details-palette-preview-strip");
paletteStrip.setAlignment(Pos.CENTER);
paletteStrip.setMinWidth(PREVIEW_FRAME_SIZE);
paletteStrip.setPrefWidth(PREVIEW_FRAME_SIZE);
paletteStrip.setMaxWidth(PREVIEW_FRAME_SIZE);
for (int index = 0; index < 15; index++) {
final Region swatch = new Region();
swatch.getStyleClass().add("assets-details-palette-preview-swatch");
swatch.setMinSize(16.0d, 26.0d);
swatch.setPrefSize(16.0d, 26.0d);
swatch.setMaxSize(16.0d, 26.0d);
paletteStrip.getChildren().add(swatch);
}
getChildren().addAll(selectorLabel, tileSelector, previewSurface, paletteLabel, paletteStrip);
}
public void setOnPreviewTileChanged(Consumer<AssetWorkspaceBankCompositionFile> onPreviewTileChanged) {
this.onPreviewTileChanged = Objects.requireNonNull(onPreviewTileChanged, "onPreviewTileChanged");
}
public void render(
List<AssetWorkspaceBankCompositionFile> previewFiles,
AssetWorkspaceBankCompositionFile previewTileFile,
AssetWorkspaceBankCompositionFile previewPaletteFile,
int renderableFileCount) {
suppressSelectorEvents = true;
tileSelector.setItems(FXCollections.observableArrayList(previewFiles));
tileSelector.setValue(previewTileFile);
suppressSelectorEvents = false;
final boolean renderable = previewTileFile != null && previewPaletteFile != null;
tileCanvas.setVisible(renderable);
tileCanvas.setManaged(renderable);
emptyLabel.setVisible(!renderable);
emptyLabel.setManaged(!renderable);
updatePaletteStrip(previewPaletteFile);
if (renderable) {
renderTile(tileCanvas.getGraphicsContext2D(), previewTileFile, previewPaletteFile);
} else {
clearCanvas(tileCanvas.getGraphicsContext2D());
}
}
private void updatePaletteStrip(AssetWorkspaceBankCompositionFile previewPaletteFile) {
final List<Integer> colors = previewPaletteFile == null ? List.of() : paletteDisplayColors(previewPaletteFile.metadata());
for (int index = 0; index < paletteStrip.getChildren().size(); index++) {
final Region swatch = (Region) paletteStrip.getChildren().get(index);
final int argb = colorAt(colors, index);
swatch.setStyle("-fx-background-color: " + cssColor(argb) + ";");
}
}
private void renderTile(GraphicsContext graphics, AssetWorkspaceBankCompositionFile tileFile, AssetWorkspaceBankCompositionFile paletteFile) {
clearCanvas(graphics);
final Map<String, Object> tile = nestedMap(tileFile.metadata(), "tile");
if (tile == null) {
return;
}
final Integer width = asInteger(tile.get("width"));
final Integer height = asInteger(tile.get("height"));
final List<Integer> indices = paletteIndices(tile.get("paletteIndices"));
final List<Integer> colors = paletteDisplayColors(paletteFile.metadata());
if (width == null || height == null || width <= 0 || height <= 0 || indices.isEmpty()) {
return;
}
final double pixelWidth = PREVIEW_SIZE / width;
final double pixelHeight = PREVIEW_SIZE / height;
for (int index = 0; index < indices.size(); index++) {
final int paletteIndex = indices.get(index);
final int x = index % width;
final int y = index / width;
graphics.setFill(toFxColor(paletteIndexToArgb(colors, paletteIndex)));
graphics.fillRect(x * pixelWidth, y * pixelHeight, pixelWidth, pixelHeight);
}
}
private void clearCanvas(GraphicsContext graphics) {
final double cell = 12.0d;
for (int y = 0; y < PREVIEW_SIZE / cell; y++) {
for (int x = 0; x < PREVIEW_SIZE / cell; x++) {
final boolean even = ((x + y) % 2) == 0;
graphics.setFill(even
? javafx.scene.paint.Color.rgb(20, 28, 38)
: javafx.scene.paint.Color.rgb(28, 36, 48));
graphics.fillRect(x * cell, y * cell, cell, cell);
}
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
final Object value = metadata.get(key);
if (value instanceof Map<?, ?> nested) {
return (Map<String, Object>) nested;
}
return null;
}
@SuppressWarnings("unchecked")
private List<Integer> paletteIndices(Object value) {
if (value instanceof List<?> list) {
return (List<Integer>) list;
}
return List.of();
}
@SuppressWarnings("unchecked")
private List<Integer> paletteDisplayColors(Map<String, Object> metadata) {
final Map<String, Object> palette = nestedMap(metadata, "palette");
if (palette == null) {
return List.of();
}
final Object converted = palette.get("convertedRgb565");
if (converted instanceof List<?> list) {
return ((List<Integer>) list).stream()
.map(this::rgb565ToArgb8888)
.toList();
}
return List.of();
}
private Integer asInteger(Object value) {
if (value instanceof Number number) {
return number.intValue();
}
return null;
}
private int paletteIndexToArgb(List<Integer> colors, int paletteIndex) {
if (paletteIndex <= 0) {
return 0x00000000;
}
return colorAt(colors, paletteIndex - 1);
}
private int colorAt(List<Integer> colors, int index) {
if (index < 0 || index >= colors.size()) {
return 0x00000000;
}
final Integer color = colors.get(index);
return color == null ? 0x00000000 : color;
}
private String cssColor(int argb) {
final int alpha = (argb >>> 24) & 0xFF;
if (alpha == 0) {
return "transparent";
}
final int red = (argb >>> 16) & 0xFF;
final int green = (argb >>> 8) & 0xFF;
final int blue = argb & 0xFF;
return String.format("#%02X%02X%02X", red, green, blue);
}
private javafx.scene.paint.Color toFxColor(int argb) {
final int alpha = (argb >>> 24) & 0xFF;
if (alpha == 0) {
return javafx.scene.paint.Color.TRANSPARENT;
}
final int red = (argb >>> 16) & 0xFF;
final int green = (argb >>> 8) & 0xFF;
final int blue = argb & 0xFF;
return javafx.scene.paint.Color.rgb(red, green, blue, alpha / 255.0d);
}
private int rgb565ToArgb8888(Integer rgb565) {
if (rgb565 == null) {
return 0x00000000;
}
final int value = rgb565 & 0xFFFF;
final int red = ((value >> 11) & 0x1F) * 255 / 31;
final int green = ((value >> 5) & 0x3F) * 255 / 63;
final int blue = (value & 0x1F) * 255 / 31;
return 0xFF000000 | (red << 16) | (green << 8) | blue;
}
}

View File

@ -0,0 +1,25 @@
package p.studio.workspaces.assets.details.palette;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
import java.util.List;
import java.util.Objects;
public record AssetDetailsPaletteOverhaulingViewModel(
boolean editing,
boolean dirty,
List<AssetWorkspaceBankCompositionFile> previewTileFiles,
List<AssetWorkspaceBankCompositionFile> allFiles,
List<AssetWorkspaceBankCompositionFile> availableFiles,
List<AssetWorkspaceBankCompositionFile> selectedFiles,
AssetWorkspaceBankCompositionFile previewTileFile,
AssetWorkspaceBankCompositionFile previewPaletteFile,
int renderableFileCount) {
public AssetDetailsPaletteOverhaulingViewModel {
previewTileFiles = List.copyOf(Objects.requireNonNull(previewTileFiles, "previewTileFiles"));
allFiles = List.copyOf(Objects.requireNonNull(allFiles, "allFiles"));
availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles"));
selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles"));
}
}

View File

@ -87,6 +87,7 @@ assets.badge.diagnostics=Diagnostics
assets.section.summary=Summary
assets.section.runtimeContract=Runtime Contract
assets.section.bankComposition=Bank Composition
assets.section.paletteOverhauling=Palette Overhauling
assets.subsection.codecConfiguration=Codec Configuration
assets.subsection.metadata=Metadata
assets.section.inputsPreview=Inputs / Preview
@ -178,6 +179,11 @@ assets.details.bankComposition.available=Available
assets.details.bankComposition.selected=Selected
assets.details.bankComposition.readonlyHint=Current bank composition state is shown read-only until editing begins.
assets.details.bankComposition.editingHint=Bank composition editing shell is active. Behavior wiring lands in the next slice.
assets.paletteOverhauling.available=Candidate Palettes
assets.paletteOverhauling.selected=Bank Palettes
assets.paletteOverhauling.tileSelector=Preview Tile
assets.paletteOverhauling.appliedPalette=Applied Palette
assets.paletteOverhauling.previewEmpty=Select at least one palette to inspect it on a tile.
assets.details.codecConfiguration.empty=This codec does not expose configuration fields yet.
assets.details.metadata.empty=This asset does not expose metadata fields yet.
assets.addWizard.title=Add Asset

View File

@ -706,9 +706,9 @@
}
.assets-details-contract-section {
-fx-min-height: 372;
-fx-pref-height: 372;
-fx-max-height: 372;
-fx-min-height: 302;
-fx-pref-height: 302;
-fx-max-height: 302;
}
.assets-details-bank-composition-section {
@ -717,10 +717,20 @@
-fx-max-height: 372;
}
.assets-details-palette-overhauling-section {
-fx-min-height: 446;
-fx-pref-height: 446;
-fx-max-height: 446;
}
.assets-details-bank-composition-body {
-fx-alignment: top-left;
}
.assets-details-palette-overhauling-body {
-fx-alignment: top-left;
}
.assets-details-bank-file-cell {
-fx-alignment: center-left;
}
@ -752,6 +762,64 @@
-fx-font-size: 10px;
}
.assets-details-palette-item {
-fx-alignment: center-left;
}
.assets-details-palette-hash {
-fx-text-fill: #9fc3e7;
-fx-font-size: 10px;
-fx-font-family: "Monaco", "Menlo", monospace;
-fx-min-width: 76;
}
.assets-details-palette-strip,
.assets-details-palette-preview-strip {
-fx-alignment: center-left;
}
.assets-details-palette-swatch,
.assets-details-palette-preview-swatch {
-fx-min-width: 12;
-fx-pref-width: 12;
-fx-max-width: 12;
-fx-min-height: 18;
-fx-pref-height: 18;
-fx-max-height: 18;
-fx-background-insets: 0;
-fx-border-width: 0;
-fx-padding: 0;
}
.assets-details-palette-preview {
-fx-background-color: #0f1318;
-fx-background-radius: 10;
-fx-border-color: #2f3a47;
-fx-border-radius: 10;
-fx-padding: 10;
-fx-spacing: 10;
}
.assets-details-palette-preview-label {
-fx-text-fill: #9fc3e7;
-fx-font-size: 11px;
-fx-font-weight: bold;
}
.assets-details-palette-preview-surface {
-fx-background-color: #111820;
-fx-background-radius: 8;
-fx-border-color: #314154;
-fx-border-radius: 8;
-fx-padding: 8;
-fx-min-width: 208;
-fx-pref-width: 208;
-fx-max-width: 208;
-fx-min-height: 208;
-fx-pref-height: 208;
-fx-max-height: 208;
}
.assets-details-contract-column {
-fx-spacing: 8;
-fx-min-width: 0;
@ -766,9 +834,9 @@
-fx-background-radius: 10;
-fx-border-color: #2f3a47;
-fx-border-radius: 10;
-fx-pref-height: 198;
-fx-min-height: 198;
-fx-max-height: 198;
-fx-pref-height: 160;
-fx-min-height: 160;
-fx-max-height: 160;
}
.assets-details-contract-metadata-scroll > .viewport {
@ -971,6 +1039,11 @@
-fx-padding: 2 7 2 7;
}
.assets-details-palette-overhauling-section .studio-dual-list-actions > .button:nth-child(3),
.assets-details-palette-overhauling-section .studio-dual-list-actions > .button:nth-child(4) {
-fx-opacity: 0.45;
}
.studio-asset-capacity-meter-track {
-fx-background-color: #10161d;
-fx-background-radius: 0;

View File

@ -112,7 +112,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
files("tile", fileCount, 1024L),
List.of(),
0L),
Map.of());
List.of());
}
private AssetWorkspaceAssetDetails soundDetails(long firstSize, long secondSize) {
@ -130,7 +130,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
new AssetWorkspaceBankCompositionFile("b.wav", "b.wav", secondSize, 1L, null, Map.of())),
List.of(),
0L),
Map.of());
List.of());
}
private List<AssetWorkspaceBankCompositionFile> files(String prefix, int count, long size) {

View File

@ -0,0 +1,134 @@
package p.studio.workspaces.assets.details.palette;
import org.junit.jupiter.api.Test;
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.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 AssetDetailsPaletteOverhaulingCoordinatorTest {
@Test
void defaultsToFirstTilePaletteWhenNoSelectedFilesExist() {
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
coordinator.replaceDetails(tileDetails(List.of(), files("tile", 3)));
assertTrue(coordinator.ready());
assertEquals(0, coordinator.viewModel().selectedFiles().size());
assertNull(coordinator.viewModel().previewTileFile());
assertNull(coordinator.viewModel().previewPaletteFile());
}
@Test
void clickingSelectedPaletteChangesOnlyPreviewPalette() {
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
coordinator.replaceDetails(tileDetails(List.of(files("tile", 3).get(0), files("tile", 3).get(1)), List.of(files("tile", 3).get(2))));
coordinator.beginEdit();
final AssetWorkspaceBankCompositionFile first = coordinator.viewModel().availableFiles().get(0);
coordinator.moveToSelected(List.of(first));
coordinator.selectPreviewPalette(first);
assertEquals(first.path(), coordinator.viewModel().previewPaletteFile().path());
assertEquals(1, coordinator.viewModel().selectedFiles().size());
}
@Test
void removingPreviewedPaletteFallsBackToFirstRemainingSelectedPalette() {
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
final List<AssetWorkspaceBankCompositionFile> generated = files("tile", 3);
coordinator.replaceDetails(tileDetails(List.of(generated.get(0), generated.get(1)), List.of(generated.get(2))));
coordinator.beginEdit();
final AssetWorkspaceBankCompositionFile first = coordinator.viewModel().availableFiles().get(0);
coordinator.moveToSelected(List.of(first));
coordinator.selectPreviewPalette(first);
coordinator.moveToAvailable(List.of(first));
assertEquals(1, coordinator.viewModel().selectedFiles().size());
assertEquals(first.path(), coordinator.viewModel().previewPaletteFile().path());
}
@Test
void deduplicatesPalettesByColorHash() {
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
final AssetWorkspaceBankCompositionFile first = file("tile-a.png", List.of(0xFFFF0000, 0xFF00FF00));
final AssetWorkspaceBankCompositionFile duplicate = file("tile-b.png", List.of(0xFFFF0000, 0xFF00FF00));
final AssetWorkspaceBankCompositionFile unique = file("tile-c.png", List.of(0xFF0000FF));
coordinator.replaceDetails(tileDetails(List.of(first), List.of(duplicate, unique)));
assertEquals(2, coordinator.viewModel().allFiles().size());
assertEquals(10, coordinator.viewModel().allFiles().getFirst().displayName().length());
assertEquals(coordinator.viewModel().allFiles().stream().map(AssetWorkspaceBankCompositionFile::displayName).distinct().count(),
coordinator.viewModel().allFiles().size());
}
private AssetWorkspaceAssetDetails tileDetails(
List<AssetWorkspaceBankCompositionFile> selectedFiles,
List<AssetWorkspaceBankCompositionFile> availableFiles) {
return new AssetWorkspaceAssetDetails(
new AssetWorkspaceAssetSummary(
AssetReference.forAssetId(1),
"bank",
AssetWorkspaceAssetState.REGISTERED,
AssetWorkspaceBuildParticipation.INCLUDED,
1,
AssetFamilyCatalog.TILE_BANK,
Path.of("/tmp/bank"),
false,
false),
List.of(),
OutputFormatCatalog.TILES_INDEXED_V1,
OutputCodecCatalog.NONE,
List.of(OutputCodecCatalog.NONE),
Map.of(OutputCodecCatalog.NONE, List.of()),
List.of(),
new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L),
List.of());
}
private List<AssetWorkspaceBankCompositionFile> files(String prefix, int count) {
return java.util.stream.IntStream.range(0, count)
.mapToObj(index -> file(
prefix + "-" + index + ".png",
List.of(
0xFF000000 | ((index + 1) << 16),
0xFF000000 | ((index + 1) << 8),
0xFF000000 | (index + 1))))
.toList();
}
private AssetWorkspaceBankCompositionFile file(String path, List<Integer> colors) {
return new AssetWorkspaceBankCompositionFile(
path,
path,
256L,
1L,
null,
Map.of(
"tile", Map.of(
"width", 2,
"height", 2,
"paletteIndices", List.of(0, 1, 2, 0)),
"palette", Map.of(
"originalArgb8888", colors,
"convertedRgb565", colors.stream().map(this::toRgb565).toList())));
}
private int toRgb565(int argb8888) {
final int red = (argb8888 >> 16) & 0xFF;
final int green = (argb8888 >> 8) & 0xFF;
final int blue = argb8888 & 0xFF;
final int r5 = (red >> 3) & 0x1F;
final int g6 = (green >> 2) & 0x3F;
final int b5 = (blue >> 3) & 0x1F;
return (r5 << 11) | (g6 << 5) | b5;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -10,17 +10,17 @@
"asset_id" : 7,
"asset_uuid" : "62a81570-8f47-4612-9288-6060e6c9a2e2",
"root" : "ui/one-more-atlas",
"included_in_build" : true
"included_in_build" : false
}, {
"asset_id" : 8,
"asset_uuid" : "9a7386e7-6f0e-4e4c-9919-0de71e0b7031",
"root" : "ui/sound",
"included_in_build" : true
"included_in_build" : false
}, {
"asset_id" : 11,
"asset_uuid" : "64147d33-e8bf-4272-bb5c-b4c07c0276b3",
"root" : "bigode",
"included_in_build" : true
"included_in_build" : false
}, {
"asset_id" : 12,
"asset_uuid" : "b15b319f-5cab-4254-93ea-d83f4742d204",

View File

@ -12,6 +12,6 @@
}
},
"preload" : {
"enabled" : true
"enabled" : false
}
}

View File

@ -12,7 +12,10 @@
}
},
"preload" : {
"enabled" : false
"enabled" : true
},
"artifacts" : [ ]
"artifacts" : [ {
"file" : "right-palette.png",
"index" : 0
} ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

After

Width:  |  Height:  |  Size: 327 B