asset details (WIP)
This commit is contained in:
parent
236fa04120
commit
64df57a774
@ -28,6 +28,16 @@ public class PackerTileBankWalker extends PackerAbstractBankWalker<PackerTileBan
|
|||||||
return SUPPORTED_MIME_TYPES;
|
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
|
@Override
|
||||||
protected PackerProbeResult processFileProbe(
|
protected PackerProbeResult processFileProbe(
|
||||||
final PackerFileProbe fileProbe,
|
final PackerFileProbe fileProbe,
|
||||||
@ -130,6 +140,26 @@ public class PackerTileBankWalker extends PackerAbstractBankWalker<PackerTileBan
|
|||||||
return new PackerProbeResult(fileProbe, Map.of("tile", tile, "palette", palette), diagnostics);
|
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(
|
private void serializeProbeArtifacts(
|
||||||
final PackerFileProbe fileProbe,
|
final PackerFileProbe fileProbe,
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,6 +33,8 @@ public abstract class StudioDualListView<T> extends HBox {
|
|||||||
|
|
||||||
private Consumer<List<T>> onMoveToRight = ignored -> { };
|
private Consumer<List<T>> onMoveToRight = ignored -> { };
|
||||||
private Consumer<List<T>> onMoveToLeft = ignored -> { };
|
private Consumer<List<T>> onMoveToLeft = ignored -> { };
|
||||||
|
private Consumer<T> onLeftSelectionChanged = ignored -> { };
|
||||||
|
private Consumer<T> onRightSelectionChanged = ignored -> { };
|
||||||
private IntConsumer onMoveUp = ignored -> { };
|
private IntConsumer onMoveUp = ignored -> { };
|
||||||
private IntConsumer onMoveDown = ignored -> { };
|
private IntConsumer onMoveDown = ignored -> { };
|
||||||
private boolean interactionEnabled = true;
|
private boolean interactionEnabled = true;
|
||||||
@ -95,6 +97,8 @@ public abstract class StudioDualListView<T> extends HBox {
|
|||||||
updateActionState();
|
updateActionState();
|
||||||
leftListView.getSelectionModel().getSelectedItems().addListener((javafx.collections.ListChangeListener<? super T>) ignored -> updateActionState());
|
leftListView.getSelectionModel().getSelectedItems().addListener((javafx.collections.ListChangeListener<? super T>) ignored -> updateActionState());
|
||||||
rightListView.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) {
|
public void setLeftItems(List<T> items) {
|
||||||
@ -133,6 +137,14 @@ public abstract class StudioDualListView<T> extends HBox {
|
|||||||
this.onMoveToLeft = Objects.requireNonNull(onMoveToLeft, "onMoveToLeft");
|
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) {
|
public void setOnMoveUp(IntConsumer onMoveUp) {
|
||||||
this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp");
|
this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp");
|
||||||
}
|
}
|
||||||
@ -149,6 +161,14 @@ public abstract class StudioDualListView<T> extends HBox {
|
|||||||
rightTitleLabel.setText(Objects.requireNonNull(title, "title"));
|
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 abstract String itemText(T item);
|
||||||
|
|
||||||
protected Node itemGraphic(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) {
|
private Node createIndexedGraphic(Node graphic, int index) {
|
||||||
final Label chip = new Label((index + 1) + ".");
|
final Label chip = new Label((index + 1) + ".");
|
||||||
chip.getStyleClass().add("studio-dual-list-index-chip");
|
chip.getStyleClass().add("studio-dual-list-index-chip");
|
||||||
|
|||||||
@ -97,6 +97,7 @@ public enum I18n {
|
|||||||
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
||||||
ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"),
|
ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"),
|
||||||
ASSETS_SECTION_BANK_COMPOSITION("assets.section.bankComposition"),
|
ASSETS_SECTION_BANK_COMPOSITION("assets.section.bankComposition"),
|
||||||
|
ASSETS_SECTION_PALETTE_OVERHAULING("assets.section.paletteOverhauling"),
|
||||||
ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"),
|
ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"),
|
||||||
ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"),
|
ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"),
|
||||||
ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"),
|
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_SELECTED("assets.details.bankComposition.selected"),
|
||||||
ASSETS_DETAILS_BANK_COMPOSITION_READONLY_HINT("assets.details.bankComposition.readonlyHint"),
|
ASSETS_DETAILS_BANK_COMPOSITION_READONLY_HINT("assets.details.bankComposition.readonlyHint"),
|
||||||
ASSETS_DETAILS_BANK_COMPOSITION_EDITING_HINT("assets.details.bankComposition.editingHint"),
|
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_CODEC_CONFIGURATION_EMPTY("assets.details.codecConfiguration.empty"),
|
||||||
ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"),
|
ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"),
|
||||||
ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"),
|
ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"),
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import p.studio.utilities.i18n.I18n;
|
|||||||
import p.studio.workspaces.assets.dialogs.AssetDiagnosticsDialog;
|
import p.studio.workspaces.assets.dialogs.AssetDiagnosticsDialog;
|
||||||
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl;
|
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl;
|
||||||
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
|
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.details.summary.AssetDetailsSummaryControl;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
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 AssetDetailsSummaryControl summaryControl;
|
||||||
private final AssetDetailsContractControl contractControl;
|
private final AssetDetailsContractControl contractControl;
|
||||||
private final AssetDetailsBankCompositionControl bankCompositionControl;
|
private final AssetDetailsBankCompositionControl bankCompositionControl;
|
||||||
|
private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
|
||||||
private final VBox actionsContent = new VBox(10);
|
private final VBox actionsContent = new VBox(10);
|
||||||
private final ScrollPane actionsScroll = new ScrollPane();
|
private final ScrollPane actionsScroll = new ScrollPane();
|
||||||
private final VBox actionsSection;
|
private final VBox actionsSection;
|
||||||
@ -71,6 +73,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
|
this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
|
||||||
this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
|
this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
|
||||||
this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
|
this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
|
||||||
|
this.paletteOverhaulingControl = new AssetDetailsPaletteOverhaulingControl(workspaceBus);
|
||||||
this.actionsSection = createActionsSection();
|
this.actionsSection = createActionsSection();
|
||||||
|
|
||||||
getStyleClass().add("assets-workspace-pane");
|
getStyleClass().add("assets-workspace-pane");
|
||||||
@ -254,7 +257,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
renderActions();
|
renderActions();
|
||||||
if (!readyMounted) {
|
if (!readyMounted) {
|
||||||
readyMounted = true;
|
readyMounted = true;
|
||||||
detailsContent.getChildren().setAll(primarySectionsRow, contractControl, bankCompositionControl);
|
detailsContent.getChildren().setAll(primarySectionsRow, contractControl, bankCompositionControl, paletteOverhaulingControl);
|
||||||
}
|
}
|
||||||
syncActionsSectionHeight(summaryControl.getHeight());
|
syncActionsSectionHeight(summaryControl.getHeight());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -87,6 +87,7 @@ assets.badge.diagnostics=Diagnostics
|
|||||||
assets.section.summary=Summary
|
assets.section.summary=Summary
|
||||||
assets.section.runtimeContract=Runtime Contract
|
assets.section.runtimeContract=Runtime Contract
|
||||||
assets.section.bankComposition=Bank Composition
|
assets.section.bankComposition=Bank Composition
|
||||||
|
assets.section.paletteOverhauling=Palette Overhauling
|
||||||
assets.subsection.codecConfiguration=Codec Configuration
|
assets.subsection.codecConfiguration=Codec Configuration
|
||||||
assets.subsection.metadata=Metadata
|
assets.subsection.metadata=Metadata
|
||||||
assets.section.inputsPreview=Inputs / Preview
|
assets.section.inputsPreview=Inputs / Preview
|
||||||
@ -178,6 +179,11 @@ assets.details.bankComposition.available=Available
|
|||||||
assets.details.bankComposition.selected=Selected
|
assets.details.bankComposition.selected=Selected
|
||||||
assets.details.bankComposition.readonlyHint=Current bank composition state is shown read-only until editing begins.
|
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.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.codecConfiguration.empty=This codec does not expose configuration fields yet.
|
||||||
assets.details.metadata.empty=This asset does not expose metadata fields yet.
|
assets.details.metadata.empty=This asset does not expose metadata fields yet.
|
||||||
assets.addWizard.title=Add Asset
|
assets.addWizard.title=Add Asset
|
||||||
|
|||||||
@ -706,9 +706,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assets-details-contract-section {
|
.assets-details-contract-section {
|
||||||
-fx-min-height: 372;
|
-fx-min-height: 302;
|
||||||
-fx-pref-height: 372;
|
-fx-pref-height: 302;
|
||||||
-fx-max-height: 372;
|
-fx-max-height: 302;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assets-details-bank-composition-section {
|
.assets-details-bank-composition-section {
|
||||||
@ -717,10 +717,20 @@
|
|||||||
-fx-max-height: 372;
|
-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 {
|
.assets-details-bank-composition-body {
|
||||||
-fx-alignment: top-left;
|
-fx-alignment: top-left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assets-details-palette-overhauling-body {
|
||||||
|
-fx-alignment: top-left;
|
||||||
|
}
|
||||||
|
|
||||||
.assets-details-bank-file-cell {
|
.assets-details-bank-file-cell {
|
||||||
-fx-alignment: center-left;
|
-fx-alignment: center-left;
|
||||||
}
|
}
|
||||||
@ -752,6 +762,64 @@
|
|||||||
-fx-font-size: 10px;
|
-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 {
|
.assets-details-contract-column {
|
||||||
-fx-spacing: 8;
|
-fx-spacing: 8;
|
||||||
-fx-min-width: 0;
|
-fx-min-width: 0;
|
||||||
@ -766,9 +834,9 @@
|
|||||||
-fx-background-radius: 10;
|
-fx-background-radius: 10;
|
||||||
-fx-border-color: #2f3a47;
|
-fx-border-color: #2f3a47;
|
||||||
-fx-border-radius: 10;
|
-fx-border-radius: 10;
|
||||||
-fx-pref-height: 198;
|
-fx-pref-height: 160;
|
||||||
-fx-min-height: 198;
|
-fx-min-height: 160;
|
||||||
-fx-max-height: 198;
|
-fx-max-height: 160;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assets-details-contract-metadata-scroll > .viewport {
|
.assets-details-contract-metadata-scroll > .viewport {
|
||||||
@ -971,6 +1039,11 @@
|
|||||||
-fx-padding: 2 7 2 7;
|
-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 {
|
.studio-asset-capacity-meter-track {
|
||||||
-fx-background-color: #10161d;
|
-fx-background-color: #10161d;
|
||||||
-fx-background-radius: 0;
|
-fx-background-radius: 0;
|
||||||
|
|||||||
@ -112,7 +112,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
|||||||
files("tile", fileCount, 1024L),
|
files("tile", fileCount, 1024L),
|
||||||
List.of(),
|
List.of(),
|
||||||
0L),
|
0L),
|
||||||
Map.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
private AssetWorkspaceAssetDetails soundDetails(long firstSize, long secondSize) {
|
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())),
|
new AssetWorkspaceBankCompositionFile("b.wav", "b.wav", secondSize, 1L, null, Map.of())),
|
||||||
List.of(),
|
List.of(),
|
||||||
0L),
|
0L),
|
||||||
Map.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AssetWorkspaceBankCompositionFile> files(String prefix, int count, long size) {
|
private List<AssetWorkspaceBankCompositionFile> files(String prefix, int count, long size) {
|
||||||
|
|||||||
@ -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
@ -10,17 +10,17 @@
|
|||||||
"asset_id" : 7,
|
"asset_id" : 7,
|
||||||
"asset_uuid" : "62a81570-8f47-4612-9288-6060e6c9a2e2",
|
"asset_uuid" : "62a81570-8f47-4612-9288-6060e6c9a2e2",
|
||||||
"root" : "ui/one-more-atlas",
|
"root" : "ui/one-more-atlas",
|
||||||
"included_in_build" : true
|
"included_in_build" : false
|
||||||
}, {
|
}, {
|
||||||
"asset_id" : 8,
|
"asset_id" : 8,
|
||||||
"asset_uuid" : "9a7386e7-6f0e-4e4c-9919-0de71e0b7031",
|
"asset_uuid" : "9a7386e7-6f0e-4e4c-9919-0de71e0b7031",
|
||||||
"root" : "ui/sound",
|
"root" : "ui/sound",
|
||||||
"included_in_build" : true
|
"included_in_build" : false
|
||||||
}, {
|
}, {
|
||||||
"asset_id" : 11,
|
"asset_id" : 11,
|
||||||
"asset_uuid" : "64147d33-e8bf-4272-bb5c-b4c07c0276b3",
|
"asset_uuid" : "64147d33-e8bf-4272-bb5c-b4c07c0276b3",
|
||||||
"root" : "bigode",
|
"root" : "bigode",
|
||||||
"included_in_build" : true
|
"included_in_build" : false
|
||||||
}, {
|
}, {
|
||||||
"asset_id" : 12,
|
"asset_id" : 12,
|
||||||
"asset_uuid" : "b15b319f-5cab-4254-93ea-d83f4742d204",
|
"asset_uuid" : "b15b319f-5cab-4254-93ea-d83f4742d204",
|
||||||
|
|||||||
@ -12,6 +12,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preload" : {
|
"preload" : {
|
||||||
"enabled" : true
|
"enabled" : false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -12,7 +12,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preload" : {
|
"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 |
Loading…
x
Reference in New Issue
Block a user