asset details (WIP)

This commit is contained in:
bQUARKz 2026-03-19 09:16:11 +00:00
parent 91799bccd7
commit a958052bbf
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
23 changed files with 599 additions and 143 deletions

View File

@ -1,6 +1,7 @@
package p.packer.models;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -10,7 +11,8 @@ public record PackerFileCacheEntry(
long size,
long lastModified,
String fingerprint,
Map<String, Object> metadata) {
Map<String, Object> metadata,
List<PackerDiagnostic> diagnostics) {
public PackerFileCacheEntry {
relativePath = normalizeRelativePath(relativePath);
@ -23,6 +25,7 @@ public record PackerFileCacheEntry(
}
fingerprint = normalizeOptional(fingerprint);
metadata = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(metadata, "metadata")));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
private static String normalizeRelativePath(String relativePath) {

View File

@ -7,8 +7,11 @@ import p.packer.PackerWorkspacePaths;
import p.packer.exceptions.PackerRegistryException;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerAssetCacheEntry;
import p.packer.models.PackerDiagnostic;
import p.packer.models.PackerFileCacheEntry;
import p.packer.models.PackerWorkspaceCacheState;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import java.io.IOException;
import java.nio.file.Files;
@ -54,7 +57,8 @@ public final class FileSystemPackerCacheRepository {
file.size,
file.lastModified,
file.fingerprint,
file.metadata == null ? Map.of() : file.metadata));
file.metadata == null ? Map.of() : file.metadata,
loadDiagnostics(file.diagnostics)));
}
}
validateAssetId(asset.assetId);
@ -92,7 +96,8 @@ public final class FileSystemPackerCacheRepository {
file.size(),
file.lastModified(),
file.fingerprint(),
file.metadata()))
file.metadata(),
saveDiagnostics(file.diagnostics())))
.toList()))
.toList();
mapper.writerWithDefaultPrettyPrinter().writeValue(cachePath.toFile(), document);
@ -101,6 +106,39 @@ public final class FileSystemPackerCacheRepository {
}
}
private List<PackerDiagnostic> loadDiagnostics(List<CacheDiagnosticDocument> diagnostics) {
if (diagnostics == null || diagnostics.isEmpty()) {
return List.of();
}
final List<PackerDiagnostic> loaded = new ArrayList<>();
for (CacheDiagnosticDocument diagnostic : diagnostics) {
if (diagnostic == null || diagnostic.severity == null || diagnostic.category == null || diagnostic.message == null) {
continue;
}
loaded.add(new PackerDiagnostic(
diagnostic.severity,
diagnostic.category,
diagnostic.message,
diagnostic.evidencePath == null || diagnostic.evidencePath.isBlank() ? null : Path.of(diagnostic.evidencePath),
diagnostic.blocking));
}
return List.copyOf(loaded);
}
private List<CacheDiagnosticDocument> saveDiagnostics(List<PackerDiagnostic> diagnostics) {
if (diagnostics == null || diagnostics.isEmpty()) {
return List.of();
}
return diagnostics.stream()
.map(diagnostic -> new CacheDiagnosticDocument(
diagnostic.severity(),
diagnostic.category(),
diagnostic.message(),
diagnostic.evidencePath() == null ? null : diagnostic.evidencePath().toString(),
diagnostic.blocking()))
.toList();
}
private void validateDuplicateAssetIds(List<PackerAssetCacheEntry> assets) {
final Set<Integer> assetIds = new HashSet<>();
for (PackerAssetCacheEntry asset : assets) {
@ -162,6 +200,16 @@ public final class FileSystemPackerCacheRepository {
@JsonProperty("size") long size,
@JsonProperty("last_modified") long lastModified,
@JsonProperty("fingerprint") String fingerprint,
@JsonProperty("metadata") Map<String, Object> metadata) {
@JsonProperty("metadata") Map<String, Object> metadata,
@JsonProperty("diagnostics") List<CacheDiagnosticDocument> diagnostics) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record CacheDiagnosticDocument(
@JsonProperty("severity") PackerDiagnosticSeverity severity,
@JsonProperty("category") PackerDiagnosticCategory category,
@JsonProperty("message") String message,
@JsonProperty("evidence_path") String evidencePath,
@JsonProperty("blocking") boolean blocking) {
}
}

View File

@ -73,7 +73,10 @@ public abstract class PackerAbstractBankWalker<R> {
final var fileProbe = fileProbeMaybe.get();
final var cachedEntry = resolvePriorFileCache(assetRoot, fileProbe.path(), priorAssetCache);
if (cachedEntry.isPresent() && canReuseMetadata(fileProbe, cachedEntry.get())) {
probeResults.add(new PackerProbeResult(fileProbe, cachedEntry.get().metadata(), List.of()));
probeResults.add(new PackerProbeResult(
fileProbe,
cachedEntry.get().metadata(),
cachedEntry.get().diagnostics()));
continue;
}
probeResults.add(processFileProbe(fileProbe, requirements));

View File

@ -74,7 +74,8 @@ public final class PackerRuntimeAssetMaterializer {
file.size(),
file.lastModified(),
file.fingerprint(),
file.metadata()))
file.metadata(),
file.diagnostics()))
.toList());
return new PackerRuntimeAssetMaterialization(runtimeAsset, Optional.of(assetCacheEntry));
}

View File

@ -14,6 +14,7 @@ import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerContractFingerprint;
import p.packer.repositories.PackerRuntimeRegistry;
import java.io.IOException;
@ -646,7 +647,11 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
}
try {
final String previousContractFingerprint = contractFingerprint(manifest);
patchManifestContract(manifest, request);
if (!Objects.equals(previousContractFingerprint, contractFingerprint(manifest))) {
clearManifestBankComposition(manifest);
}
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
currentSnapshot,
@ -770,6 +775,35 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
}
}
private String contractFingerprint(ObjectNode manifest) {
final JsonNode outputNode = manifest.path("output");
if (!(outputNode instanceof ObjectNode outputObject)) {
return PackerContractFingerprint.metadataFingerprint(Map.of());
}
final JsonNode metadataNode = outputObject.path("metadata");
if (!(metadataNode instanceof ObjectNode metadataObject)) {
return PackerContractFingerprint.metadataFingerprint(Map.of());
}
final Map<String, String> metadata = new LinkedHashMap<>();
metadataObject.fields().forEachRemaining(entry -> {
if (entry.getKey() == null || entry.getKey().isBlank()) {
return;
}
final JsonNode valueNode = entry.getValue();
if (valueNode == null || valueNode.isContainerNode()) {
return;
}
metadata.put(entry.getKey().trim(), valueNode.isNull() ? "" : valueNode.asText(""));
});
return PackerContractFingerprint.metadataFingerprint(metadata);
}
private void clearManifestBankComposition(ObjectNode manifest) {
final ObjectNode inputsNode = mutableObject(manifest, "inputs");
inputsNode.removeAll();
manifest.putArray("artifacts");
}
private boolean isTrustedRelativePath(String value) {
final Path path = Path.of(value).normalize();
return !path.isAbsolute() && !path.startsWith("..");

View File

@ -44,6 +44,7 @@ public final class PackerAssetDetailsService {
final var parsed = runtimeAsset.parsedDeclaration();
diagnostics.addAll(parsed.diagnostics());
diagnostics.addAll(runtimeAsset.walkDiagnostics());
appendWalkFileDiagnostics(runtimeAsset, diagnostics);
if (!parsed.valid()) {
return failureResult(project, request.assetReference(), resolved, diagnostics);
}
@ -253,6 +254,18 @@ public final class PackerAssetDetailsService {
true));
}
private void appendWalkFileDiagnostics(
PackerRuntimeAsset runtimeAsset,
List<PackerDiagnostic> diagnostics) {
for (PackerRuntimeWalkFile file : runtimeAsset.walkProjection().buildCandidateFiles()) {
for (PackerDiagnostic diagnostic : file.diagnostics()) {
if (!diagnostics.contains(diagnostic)) {
diagnostics.add(diagnostic);
}
}
}
}
private AssetReference canonicalReference(
final PackerProjectContext project,
final Path assetRoot,

View File

@ -7,8 +7,11 @@ import p.packer.PackerWorkspacePaths;
import p.packer.exceptions.PackerRegistryException;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerAssetCacheEntry;
import p.packer.models.PackerDiagnostic;
import p.packer.models.PackerFileCacheEntry;
import p.packer.models.PackerWorkspaceCacheState;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import java.nio.file.Files;
import java.nio.file.Path;
@ -46,14 +49,21 @@ final class FileSystemPackerCacheRepositoryTest {
128L,
42L,
"abc123",
Map.of("tile_count", 4)),
Map.of("tile_count", 4),
List.of(new PackerDiagnostic(
PackerDiagnosticSeverity.WARNING,
PackerDiagnosticCategory.HYGIENE,
"cached warning",
Path.of("/tmp/ui.png"),
false))),
new PackerFileCacheEntry(
"tiles/ui-2.png",
"image/png",
256L,
84L,
"def456",
Map.of()))),
Map.of(),
List.of()))),
new PackerAssetCacheEntry(1, "contract-1", List.of(
new PackerFileCacheEntry(
"audio/click.wav",
@ -61,7 +71,8 @@ final class FileSystemPackerCacheRepositoryTest {
512L,
21L,
null,
Map.of("sample_count", 8))))));
Map.of("sample_count", 8),
List.of())))));
repository.save(project, state);
final var loaded = repository.load(project);
@ -70,6 +81,8 @@ final class FileSystemPackerCacheRepositoryTest {
assertEquals("contract-3", loaded.findAsset(3).orElseThrow().contractFingerprint());
assertTrue(loaded.findAsset(3).flatMap(asset -> asset.findFile("tiles/ui.png")).isPresent());
assertEquals(128L, loaded.findAsset(3).orElseThrow().findFile("tiles/ui.png").orElseThrow().size());
assertEquals(1, loaded.findAsset(3).orElseThrow().findFile("tiles/ui.png").orElseThrow().diagnostics().size());
assertEquals("cached warning", loaded.findAsset(3).orElseThrow().findFile("tiles/ui.png").orElseThrow().diagnostics().getFirst().message());
}
@Test
@ -114,7 +127,7 @@ final class FileSystemPackerCacheRepositoryTest {
}
@Test
void doesNotSerializeDiagnostics() throws Exception {
void serializesDiagnosticsWhenPresent() throws Exception {
final var repository = new FileSystemPackerCacheRepository(MAPPER);
final var project = project(tempDir.resolve("project"));
final var state = new PackerWorkspaceCacheState(
@ -126,13 +139,19 @@ final class FileSystemPackerCacheRepositoryTest {
128L,
42L,
"abc123",
Map.of("warning_count", 2))))));
Map.of("warning_count", 2),
List.of(new PackerDiagnostic(
PackerDiagnosticSeverity.WARNING,
PackerDiagnosticCategory.HYGIENE,
"cached diagnostic",
Path.of("/tmp/ui.png"),
false)))))));
repository.save(project, state);
final String json = Files.readString(PackerWorkspacePaths.cachePath(project));
assertFalse(json.contains("diagnostic"));
assertFalse(json.contains("diagnostics"));
assertTrue(json.contains("\"diagnostics\""));
assertTrue(json.contains("\"cached diagnostic\""));
assertTrue(json.contains("\"asset_id\""));
assertTrue(json.contains("\"contract_fingerprint\""));
assertTrue(json.contains("\"metadata\""));

View File

@ -29,7 +29,7 @@ final class PackerAbstractBankWalkerTest {
final var filePath = writeText(assetRoot.resolve("entry.txt"), "hello world");
final long lastModified = Files.getLastModifiedTime(filePath).toMillis();
final var priorCache = new PackerAssetCacheEntry(1, "contract", List.of(
new PackerFileCacheEntry("entry.txt", "text/plain", 1L, lastModified, "cached", Map.of("source", "cache"))));
new PackerFileCacheEntry("entry.txt", "text/plain", 1L, lastModified, "cached", Map.of("source", "cache"), List.of())));
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));
@ -51,7 +51,8 @@ final class PackerAbstractBankWalkerTest {
Files.size(filePath),
currentLastModified - 1L,
"cached",
Map.of("source", "cache"))));
Map.of("source", "cache"),
List.of())));
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));
@ -74,13 +75,21 @@ final class PackerAbstractBankWalkerTest {
Files.size(filePath),
lastModified,
fingerprint,
Map.of("source", "cache"))));
Map.of("source", "cache"),
List.of(new PackerDiagnostic(
p.packer.messages.diagnostics.PackerDiagnosticSeverity.WARNING,
p.packer.messages.diagnostics.PackerDiagnosticCategory.HYGIENE,
"cached file-scoped diagnostic",
filePath,
false)))));
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));
assertEquals(0, walker.processCount());
assertEquals(1, walker.fingerprintCount());
assertEquals("cache", result.probeResults().getFirst().metadata().get("source"));
assertEquals(1, result.probeResults().getFirst().diagnostics().size());
assertTrue(result.probeResults().getFirst().diagnostics().getFirst().message().contains("cached"));
}
@Test
@ -96,7 +105,8 @@ final class PackerAbstractBankWalkerTest {
Files.size(filePath),
lastModified,
null,
Map.of("source", "cache"))));
Map.of("source", "cache"),
List.of())));
final var result = walker.walk(assetRoot, "requirements", java.util.Optional.of(priorCache));

View File

@ -15,6 +15,8 @@ import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.repositories.PackerRuntimeSnapshotLoader;
import p.packer.testing.PackerFixtureLocator;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
@ -245,6 +247,41 @@ final class FileSystemPackerWorkspaceServiceTest {
assertEquals(1, loader.loadCount());
}
@Test
void updateAssetContractDropsPersistedBankCompositionWhenContractFingerprintChanges() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("update-contract-drops-bank"));
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
writeTilePng(assetRoot.resolve("confirm.png"), 16);
writeTilePng(assetRoot.resolve("cancel.png"), 16);
final FileSystemPackerWorkspaceService service = service();
final var applyResult = service.applyBankComposition(new ApplyBankCompositionRequest(
project(projectRoot),
AssetReference.forAssetId(1),
List.of("cancel.png", "confirm.png")));
assertTrue(applyResult.success());
final var updateResult = service.updateAssetContract(new UpdateAssetContractRequest(
project(projectRoot),
AssetReference.forAssetId(1),
true,
OutputCodecCatalog.NONE,
Map.of(),
Map.of("tile_size", "32x32")));
assertTrue(updateResult.success());
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
assertTrue(manifest.path("inputs").isObject());
assertTrue(manifest.path("inputs").isEmpty());
assertTrue(manifest.path("artifacts").isArray());
assertTrue(manifest.path("artifacts").isEmpty());
final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
project(projectRoot),
AssetReference.forAssetId(1)));
assertTrue(detailsResult.details().bankComposition().selectedFiles().isEmpty());
}
@Test
void applyBankCompositionWritesArtifactsAndRefreshesSnapshotWithoutWholeProjectReload() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-bank-composition"));
@ -284,6 +321,44 @@ final class FileSystemPackerWorkspaceServiceTest {
assertEquals(1, loader.loadCount());
}
@Test
void deepSyncPreservesContractDrivenBankInvalidationAfterPatchUpdate() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("deep-sync-bank-composition"));
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
writeTilePng(assetRoot.resolve("confirm.png"), 16);
writeTilePng(assetRoot.resolve("cancel.png"), 16);
final FileSystemPackerWorkspaceService service = service();
final var applyResult = service.applyBankComposition(new ApplyBankCompositionRequest(
project(projectRoot),
AssetReference.forAssetId(1),
List.of("cancel.png", "confirm.png")));
assertTrue(applyResult.success());
final var updateResult = service.updateAssetContract(new UpdateAssetContractRequest(
project(projectRoot),
AssetReference.forAssetId(1),
true,
OutputCodecCatalog.NONE,
Map.of(),
Map.of("tile_size", "32x32")));
assertTrue(updateResult.success());
final var patchedDetails = service.getAssetDetails(new GetAssetDetailsRequest(
project(projectRoot),
AssetReference.forAssetId(1)));
assertTrue(patchedDetails.details().bankComposition().availableFiles().isEmpty());
assertTrue(patchedDetails.details().bankComposition().selectedFiles().isEmpty());
service.listAssets(new ListAssetsRequest(project(projectRoot), true));
final var refreshedDetails = service.getAssetDetails(new GetAssetDetailsRequest(
project(projectRoot),
AssetReference.forAssetId(1)));
assertTrue(refreshedDetails.details().bankComposition().availableFiles().isEmpty());
assertTrue(refreshedDetails.details().bankComposition().selectedFiles().isEmpty());
}
@Test
void returnsFailureWhenAssetManifestIsMissingOnContractUpdate() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("update-contract-missing-manifest"));
@ -786,4 +861,15 @@ final class FileSystemPackerWorkspaceServiceTest {
}
return targetRoot;
}
private void writeTilePng(Path path, int tileSize) throws Exception {
Files.createDirectories(path.getParent());
final BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < tileSize; y += 1) {
for (int x = 0; x < tileSize; x += 1) {
image.setRGB(x, y, 0xFFFF0000);
}
}
ImageIO.write(image, "png", path.toFile());
}
}

View File

@ -107,6 +107,22 @@ final class PackerAssetDetailsServiceTest {
result.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList());
}
@Test
void includesFileScopedDiagnosticsInAssetDetailsDiagnostics() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-file-diagnostics"));
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
writeTilePng(assetRoot.resolve("wrong-palette.png"), 32);
final PackerAssetDetailsService service = service();
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1)));
assertEquals(PackerOperationStatus.PARTIAL, result.status());
assertTrue(result.diagnostics().stream()
.anyMatch(diagnostic -> diagnostic.message().contains("Invalid tile dimensions for wrong-palette.png")));
assertTrue(result.details().diagnostics().stream()
.anyMatch(diagnostic -> diagnostic.message().contains("Invalid tile dimensions for wrong-palette.png")));
}
@Test
void returnsUnregisteredDetailsForValidUnregisteredRootReference() throws Exception {
final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan"));
@ -224,4 +240,15 @@ final class PackerAssetDetailsServiceTest {
}
return targetRoot;
}
private void writeTilePng(Path path, int tileSize) throws Exception {
Files.createDirectories(path.getParent());
final BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < tileSize; y += 1) {
for (int x = 0; x < tileSize; x += 1) {
image.setRGB(x, y, 0xFFFF0000);
}
}
ImageIO.write(image, "png", path.toFile());
}
}

View File

@ -104,6 +104,7 @@ public enum I18n {
ASSETS_SECTION_ACTIONS("assets.section.actions"),
ASSETS_ACTIONS_EMPTY("assets.actions.empty"),
ASSETS_ACTION_REGISTER("assets.action.register"),
ASSETS_ACTION_ANALYSE("assets.action.analyse"),
ASSETS_ACTION_DELETE("assets.action.delete"),
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
@ -162,6 +163,11 @@ public enum I18n {
ASSETS_LOGS_TITLE("assets.logs.title"),
ASSETS_INPUTS_EMPTY("assets.inputs.empty"),
ASSETS_DIAGNOSTICS_EMPTY("assets.diagnostics.empty"),
ASSETS_DIAGNOSTICS_DIALOG_TITLE("assets.diagnostics.dialog.title"),
ASSETS_DIAGNOSTICS_DIALOG_SUMMARY("assets.diagnostics.dialog.summary"),
ASSETS_DIAGNOSTICS_LABEL_CATEGORY("assets.diagnostics.label.category"),
ASSETS_DIAGNOSTICS_LABEL_EVIDENCE("assets.diagnostics.label.evidence"),
ASSETS_DIAGNOSTICS_LABEL_BLOCKING("assets.diagnostics.label.blocking"),
ASSETS_PREVIEW_EMPTY("assets.preview.empty"),
ASSETS_PREVIEW_ZOOM("assets.preview.zoom"),
ASSETS_PREVIEW_TEXT_ERROR("assets.preview.textError"),

View File

@ -10,6 +10,7 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import p.packer.dtos.PackerAssetActionAvailabilityDTO;
import p.packer.dtos.PackerAssetDetailsDTO;
import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.messages.*;
import p.studio.Container;
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
@ -17,6 +18,7 @@ import p.studio.controls.forms.StudioSection;
import p.studio.events.StudioWorkspaceEventBus;
import p.studio.projects.ProjectReference;
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.summary.AssetDetailsSummaryControl;
@ -162,7 +164,10 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
final var actionsResponse = workspaceService.getAssetActions(new GetAssetActionsRequest(
projectReference.toPackerProjectContext(),
assetReference));
final AssetWorkspaceAssetDetails details = mapDetails(response.details(), actionsResponse.actions());
final AssetWorkspaceAssetDetails details = mapDetails(
response.details(),
response.diagnostics(),
actionsResponse.actions());
Platform.runLater(() -> applyLoadedDetails(generation, assetReference, details));
} catch (RuntimeException exception) {
Platform.runLater(() -> applyLoadFailure(generation, assetReference, exception));
@ -305,7 +310,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
final var visibleActions = viewState.selectedAssetDetails().actions().stream()
.filter(AssetWorkspaceAssetAction::visible)
.toList();
if (visibleActions.isEmpty()) {
if (visibleActions.isEmpty() && viewState.selectedAssetDetails().diagnostics().isEmpty()) {
nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
return nodes;
}
@ -315,6 +320,10 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
button.setOnAction(ignored -> executeAction(action));
nodes.add(button);
}
final Button analyseButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_ANALYSE));
analyseButton.setDisable(viewState.selectedAssetDetails().diagnostics().isEmpty());
analyseButton.setOnAction(ignored -> openDiagnosticsDialog());
nodes.add(analyseButton);
return nodes;
}
@ -365,6 +374,17 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
Container.backgroundTasks().submit(() -> deleteSelectedAsset(assetReference));
}
private void openDiagnosticsDialog() {
if (viewState.selectedAssetDetails() == null || getScene() == null) {
return;
}
AssetDiagnosticsDialog.showAndWait(
getScene().getWindow(),
projectReference,
viewState.selectedAssetDetails().summary().assetName(),
viewState.selectedAssetDetails().diagnostics());
}
private void registerSelectedAsset(AssetReference assetReference) {
try {
final RegisterAssetResult result = Container.packer().workspaceService().registerAsset(new RegisterAssetRequest(
@ -428,7 +448,14 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
private AssetWorkspaceAssetDetails mapDetails(
PackerAssetDetailsDTO details,
java.util.List<PackerDiagnosticDTO> diagnostics,
java.util.List<PackerAssetActionAvailabilityDTO> actions) {
final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
for (PackerDiagnosticDTO diagnostic : diagnostics) {
if (!mergedDiagnostics.contains(diagnostic)) {
mergedDiagnostics.add(diagnostic);
}
}
return new AssetWorkspaceAssetDetails(
AssetListPackerMappings.mapSummary(details.summary()),
actions.stream()
@ -444,7 +471,8 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
details.codecConfigurationFieldsByCodec(),
details.metadataFields(),
mapBankComposition(details.bankComposition()),
Map.copyOf(details.inputsByRole()));
Map.copyOf(details.inputsByRole()),
mergedDiagnostics);
}
private AssetWorkspaceBankCompositionDetails mapBankComposition(p.packer.dtos.PackerBankCompositionDetailsDTO details) {

View File

@ -0,0 +1,138 @@
package p.studio.workspaces.assets.dialogs;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.studio.Container;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
public final class AssetDiagnosticsDialog {
private final Stage stage;
private final ProjectReference projectReference;
private final String assetName;
private final List<PackerDiagnosticDTO> diagnostics;
private AssetDiagnosticsDialog(
Window owner,
ProjectReference projectReference,
String assetName,
List<PackerDiagnosticDTO> diagnostics) {
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
this.assetName = Objects.requireNonNull(assetName, "assetName");
this.diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
this.stage = new Stage();
stage.initOwner(owner);
stage.initModality(Modality.WINDOW_MODAL);
stage.setTitle(Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_DIALOG_TITLE));
stage.setScene(new Scene(buildRoot(), 720, 560));
stage.getScene().getStylesheets().add(Container.theme().getDefaultTheme());
}
public static void showAndWait(
Window owner,
ProjectReference projectReference,
String assetName,
List<PackerDiagnosticDTO> diagnostics) {
new AssetDiagnosticsDialog(owner, projectReference, assetName, diagnostics).stage.showAndWait();
}
private VBox buildRoot() {
final Label title = new Label(Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_DIALOG_TITLE));
title.getStyleClass().add("studio-launcher-section-title");
final Label subtitle = new Label(Container.i18n().format(
I18n.ASSETS_DIAGNOSTICS_DIALOG_SUMMARY,
diagnostics.size(),
assetName));
subtitle.getStyleClass().add("studio-launcher-subtitle");
subtitle.setWrapText(true);
final VBox diagnosticsContent = new VBox(10);
diagnosticsContent.getStyleClass().add("assets-diagnostics-dialog-content");
if (diagnostics.isEmpty()) {
diagnosticsContent.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_EMPTY)));
} else {
diagnostics.forEach(diagnostic -> diagnosticsContent.getChildren().add(createDiagnosticCard(diagnostic)));
}
final ScrollPane scrollPane = new ScrollPane(diagnosticsContent);
scrollPane.setFitToWidth(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.getStyleClass().add("assets-diagnostics-dialog-scroll");
final Button closeButton = new Button();
closeButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CANCEL));
closeButton.getStyleClass().addAll("studio-button", "studio-button-cancel");
closeButton.setOnAction(ignored -> stage.close());
final HBox actions = new HBox(closeButton);
actions.setAlignment(Pos.CENTER_RIGHT);
final VBox root = new VBox(16, title, subtitle, scrollPane, actions);
root.setPadding(new Insets(24));
VBox.setVgrow(scrollPane, Priority.ALWAYS);
return root;
}
private VBox createDiagnosticCard(PackerDiagnosticDTO diagnostic) {
final Label severity = new Label(severityLabel(diagnostic));
severity.getStyleClass().add("assets-details-diagnostic-severity");
final Label message = new Label(diagnostic.message());
message.getStyleClass().add("assets-details-diagnostic-message");
message.setWrapText(true);
final VBox card = new VBox(8, severity, message);
card.getStyleClass().addAll("assets-details-diagnostic-card", severityToneClass(diagnostic.severity()));
card.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_LABEL_CATEGORY),
diagnostic.category().name()));
card.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_LABEL_BLOCKING),
AssetDetailsUiSupport.booleanLabel(diagnostic.blocking())));
card.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_LABEL_EVIDENCE),
evidenceLabel(diagnostic.evidencePath())));
return card;
}
private String severityLabel(PackerDiagnosticDTO diagnostic) {
return diagnostic.blocking()
? diagnostic.severity().name() + " / BLOCKING"
: diagnostic.severity().name();
}
private String severityToneClass(PackerDiagnosticSeverity severity) {
return switch (severity) {
case ERROR -> "assets-details-diagnostic-blocker";
case WARNING -> "assets-details-diagnostic-warning";
case INFO -> "assets-details-diagnostic-hint";
};
}
private String evidenceLabel(Path evidencePath) {
if (evidencePath == null) {
return "";
}
return AssetDetailsUiSupport.projectRelativePath(projectReference, evidencePath);
}
}

View File

@ -1,6 +1,7 @@
package p.studio.workspaces.assets.messages;
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.OutputFormatCatalog;
@ -18,7 +19,8 @@ public record AssetWorkspaceAssetDetails(
Map<OutputCodecCatalog, List<PackerCodecConfigurationFieldDTO>> codecConfigurationFieldsByCodec,
List<PackerCodecConfigurationFieldDTO> metadataFields,
AssetWorkspaceBankCompositionDetails bankComposition,
Map<String, List<Path>> inputsByRole) {
Map<String, List<Path>> inputsByRole,
List<PackerDiagnosticDTO> diagnostics) {
public AssetWorkspaceAssetDetails {
Objects.requireNonNull(summary, "summary");
@ -30,5 +32,6 @@ public record AssetWorkspaceAssetDetails(
metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields"));
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
}

View File

@ -94,6 +94,7 @@ assets.section.diagnostics=Diagnostics
assets.section.actions=Actions
assets.actions.empty=No actions available for this asset.
assets.action.register=Register
assets.action.analyse=Analyse
assets.action.delete=Delete
assets.deleteDialog.title=Delete Asset
assets.deleteDialog.description=Type the asset name below to confirm deletion of {0}.
@ -153,6 +154,11 @@ assets.progress.loadingDetails=Loading selected asset details...
assets.logs.title=Logs
assets.inputs.empty=No previewable inputs are currently declared for this asset.
assets.diagnostics.empty=No diagnostics are currently attached to this asset.
assets.diagnostics.dialog.title=Asset Diagnostics
assets.diagnostics.dialog.summary={0} diagnostics for {1}
assets.diagnostics.label.category=Category
assets.diagnostics.label.evidence=Evidence
assets.diagnostics.label.blocking=Blocking
assets.preview.empty=Select an input to preview it here.
assets.preview.zoom=Zoom
assets.preview.textError=Unable to read this text-like input for preview.

View File

@ -572,6 +572,19 @@
-fx-spacing: 10;
}
.assets-diagnostics-dialog-scroll {
-fx-background-color: transparent;
-fx-fit-to-width: true;
}
.assets-diagnostics-dialog-scroll > .viewport {
-fx-background-color: transparent;
}
.assets-diagnostics-dialog-content {
-fx-spacing: 10;
}
.assets-details-action-button {
-fx-max-width: Infinity;
-fx-padding: 6 10 6 10;

View File

@ -1649,20 +1649,105 @@
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Studio",
"message" : "Project ready",
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Studio",
"message" : "Project loading started",
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Studio",
"message" : "Project opened",
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
@ -2270,49 +2355,9 @@
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
@ -2360,49 +2405,9 @@
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
@ -2448,6 +2453,11 @@
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
@ -2488,14 +2498,4 @@
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
} ]

View File

@ -19,7 +19,28 @@
"height" : 16,
"paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAEBAQEBAQEBAQEBAQAAAAABAQEBAQEBAQEBAQEAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAABAQEBAQEBAQEAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAABAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAAAAAAAAAAEBAQEBAQ=="
}
}
},
"diagnostics" : [ ]
}, {
"relative_path" : "wrong-palette.png",
"mime_type" : "image/png",
"size" : 248,
"last_modified" : 1773911050482,
"fingerprint" : "15850f68547775866b01a0fe0b0012bb0243dec303ce1f9c3e02220e05b593e6",
"metadata" : { },
"diagnostics" : [ {
"severity" : "ERROR",
"category" : "STRUCTURAL",
"message" : "Invalid tile dimensions for wrong-palette.png: expected 16x16 but got 32x32",
"evidence_path" : "/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/test-projects/main/assets/ui/atlas2/wrong-palette.png",
"blocking" : true
}, {
"severity" : "ERROR",
"category" : "STRUCTURAL",
"message" : "Tile image exceeds color limit for wrong-palette.png: expected at most 15 colors for indices 1..15",
"evidence_path" : "/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/test-projects/main/assets/ui/atlas2/wrong-palette.png",
"blocking" : true
} ]
} ]
}, {
"asset_id" : 7,
@ -35,24 +56,21 @@
"files" : [ ]
}, {
"asset_id" : 12,
"contract_fingerprint" : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"contract_fingerprint" : "d27435217b20e485d55447c55427995bec564b27481b76fd9d74798adf835005",
"files" : [ {
"relative_path" : "confirm.png",
"mime_type" : "image/png",
"size" : 137,
"last_modified" : 1773253076764,
"fingerprint" : "aa7d241deabcebe29a6096e14eaf16fdc06cf06380c11a507620b00fc7bff094",
"metadata" : {
"palette" : {
"originalArgb8888" : [ -265674 ],
"convertedRgb565" : [ -122 ]
},
"tile" : {
"width" : 16,
"height" : 16,
"paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAEBAQEBAQEBAQEBAQAAAAABAQEBAQEBAQEBAQEAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAABAQEBAQEBAQEAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAABAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAAAAAAAAAAEBAQEBAQ=="
}
}
"metadata" : { },
"diagnostics" : [ {
"severity" : "ERROR",
"category" : "STRUCTURAL",
"message" : "Invalid tile dimensions for confirm.png: expected 32x32 but got 16x16",
"evidence_path" : "/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/test-projects/main/assets/recovered/atlas2/confirm.png",
"blocking" : true
} ]
} ]
}, {
"asset_id" : 13,

View File

@ -7,13 +7,13 @@
"output" : {
"format" : "TILES/indexed_v1",
"codec" : "NONE",
"codec_configuration" : { }
"codec_configuration" : { },
"metadata" : {
"tile_size" : "32x32"
}
},
"preload" : {
"enabled" : false
},
"artifacts" : [ {
"file" : "confirm.png",
"index" : 0
} ]
"artifacts" : [ ]
}

View File

@ -1,4 +1,4 @@
{
"originalArgb8888" : [ -265674 ],
"convertedRgb565" : [ 65414 ]
"convertedRgb565" : [ 65414 ],
"originalArgb8888" : [ -265674 ]
}

View File

@ -1,5 +1,5 @@
{
"paletteIndices" : [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 ],
"height" : 16,
"width" : 16,
"height" : 16
"paletteIndices" : [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 ]
}

View File

@ -1,5 +1,5 @@
{
"width" : 16,
"height" : 16,
"paletteIndices" : [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 ]
"paletteIndices" : [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 ],
"width" : 16
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B