mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Split jigsaw graph editor
- Move planar object resize geometry into JigsawStudioObjectResizer - Move owned graph JSON document transforms into JigsawStudioGraphDocuments - Move owned graph reference checks into JigsawStudioGraphInspector - Point the graph editor resize test at the relocated resizer - Add a scripted studio persistence session digest test over project and history bytes
This commit is contained in:
+305
@@ -0,0 +1,305 @@
|
||||
package art.arcane.iris.core.runtime.jigsaw;
|
||||
|
||||
import art.arcane.iris.engine.framework.structure.PlanarJigsawWorkcellResolver;
|
||||
import art.arcane.iris.engine.object.IrisJigsawMode;
|
||||
import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
final class JigsawStudioGraphDocuments {
|
||||
static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
private JigsawStudioGraphDocuments() {
|
||||
}
|
||||
|
||||
static Map<String, JigsawPlanarArchetype> expectedThemeSetSources(
|
||||
IrisStructure structure,
|
||||
Set<String> requestedStableIds
|
||||
) throws IOException {
|
||||
Map<String, JigsawPlanarArchetype> expected = new LinkedHashMap<>();
|
||||
if (structure.resolvedMode() == IrisJigsawMode.SPATIAL_JIGSAW) {
|
||||
if (requestedStableIds.isEmpty()) {
|
||||
throw new IOException("A spatial theme set requires at least one workcell source");
|
||||
}
|
||||
List<String> stableIds = new ArrayList<>(requestedStableIds);
|
||||
stableIds.sort(Comparator.naturalOrder());
|
||||
for (String stableId : stableIds) {
|
||||
if (!stableId.equals(JigsawStudioLayout.SPATIAL_WORKCELL_ID)
|
||||
&& !stableId.startsWith(JigsawStudioLayout.SPATIAL_WORKCELL_ID + "/")) {
|
||||
throw new IOException("Invalid spatial workcell source '" + stableId + "'");
|
||||
}
|
||||
expected.put(stableId, null);
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
Map<IrisJigsawWorkcellArchetype, PlanarJigsawWorkcellResolver.ResolvedWorkcell> workcells;
|
||||
try {
|
||||
workcells = PlanarJigsawWorkcellResolver.resolve(structure);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException("Planar workcell configuration is invalid: "
|
||||
+ exception.getMessage(), exception);
|
||||
}
|
||||
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
|
||||
PlanarJigsawWorkcellResolver.ResolvedWorkcell workcell = workcells.get(archetype.modelArchetype());
|
||||
if (workcell != null && workcell.enabled()) {
|
||||
expected.put(archetype.stableId(), archetype);
|
||||
}
|
||||
}
|
||||
if (expected.isEmpty()) {
|
||||
throw new IOException("A planar theme set requires at least one enabled workcell");
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
static void requireExactThemeSetSources(
|
||||
Map<String, String> requestedSources,
|
||||
Set<String> expectedStableIds
|
||||
) throws IOException {
|
||||
Set<String> missing = new LinkedHashSet<>(expectedStableIds);
|
||||
missing.removeAll(requestedSources.keySet());
|
||||
Set<String> unexpected = new LinkedHashSet<>(requestedSources.keySet());
|
||||
unexpected.removeAll(expectedStableIds);
|
||||
if (!missing.isEmpty() || !unexpected.isEmpty()) {
|
||||
throw new IOException("Theme-set sources must match enabled workcells exactly; missing="
|
||||
+ missing + ", unexpected=" + unexpected);
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] appendThemeSet(
|
||||
byte[] content,
|
||||
String themeKey,
|
||||
String structureResource
|
||||
) throws IOException {
|
||||
JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8));
|
||||
if (!parsed.isJsonObject()) {
|
||||
throw new IOException("Jigsaw structure is not a JSON object: " + structureResource);
|
||||
}
|
||||
JsonObject structure = parsed.getAsJsonObject();
|
||||
JsonArray themeSets;
|
||||
if (!structure.has("themeSets")) {
|
||||
themeSets = new JsonArray();
|
||||
structure.add("themeSets", themeSets);
|
||||
} else if (!structure.get("themeSets").isJsonArray()) {
|
||||
throw new IOException("Jigsaw structure has an invalid themeSets value: " + structureResource);
|
||||
} else {
|
||||
themeSets = structure.getAsJsonArray("themeSets");
|
||||
}
|
||||
for (JsonElement element : themeSets) {
|
||||
if (element.isJsonObject()
|
||||
&& element.getAsJsonObject().has("key")
|
||||
&& themeKey.equals(element.getAsJsonObject().get("key").getAsString())) {
|
||||
throw new IOException("Jigsaw structure already declares theme '" + themeKey + "'");
|
||||
}
|
||||
}
|
||||
JsonObject theme = new JsonObject();
|
||||
theme.addProperty("key", themeKey);
|
||||
theme.addProperty("weight", 1);
|
||||
themeSets.add(theme);
|
||||
return (GSON.toJson(structure) + "\n").getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static byte[] duplicatePieceForTheme(
|
||||
byte[] content,
|
||||
String targetPieceKey,
|
||||
String themeKey,
|
||||
String sourcePieceResource
|
||||
) throws IOException {
|
||||
JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8));
|
||||
if (!parsed.isJsonObject()) {
|
||||
throw new IOException("Jigsaw piece is not a JSON object: " + sourcePieceResource);
|
||||
}
|
||||
JsonObject piece = parsed.getAsJsonObject().deepCopy();
|
||||
piece.addProperty("object", targetPieceKey);
|
||||
JsonArray themes = new JsonArray();
|
||||
themes.add(themeKey);
|
||||
piece.add("themes", themes);
|
||||
return (GSON.toJson(piece) + "\n").getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static byte[] duplicatePieceForVariant(
|
||||
byte[] content,
|
||||
String targetPieceKey,
|
||||
String sourcePieceResource
|
||||
) throws IOException {
|
||||
JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8));
|
||||
if (!parsed.isJsonObject()) {
|
||||
throw new IOException("Jigsaw piece is not a JSON object: " + sourcePieceResource);
|
||||
}
|
||||
JsonObject piece = parsed.getAsJsonObject().deepCopy();
|
||||
piece.addProperty("object", targetPieceKey);
|
||||
return (GSON.toJson(piece) + "\n").getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static PoolMembershipDuplication duplicatePoolMemberships(
|
||||
byte[] content,
|
||||
Map<String, String> targetPieceKeysBySource,
|
||||
String poolResource
|
||||
) throws IOException {
|
||||
JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8));
|
||||
if (!parsed.isJsonObject()) {
|
||||
throw new IOException("Jigsaw pool is not a JSON object: " + poolResource);
|
||||
}
|
||||
JsonObject pool = parsed.getAsJsonObject();
|
||||
JsonArray pieces = pool.getAsJsonArray("pieces");
|
||||
if (pieces == null) {
|
||||
throw new IOException("Jigsaw pool does not declare a pieces array: " + poolResource);
|
||||
}
|
||||
JsonArray expanded = new JsonArray();
|
||||
int duplicatedEntries = 0;
|
||||
for (JsonElement element : pieces) {
|
||||
expanded.add(element.deepCopy());
|
||||
if (!element.isJsonObject() || !element.getAsJsonObject().has("piece")) {
|
||||
continue;
|
||||
}
|
||||
String sourcePieceKey = element.getAsJsonObject().get("piece").getAsString();
|
||||
String targetPieceKey = targetPieceKeysBySource.get(sourcePieceKey);
|
||||
if (targetPieceKey == null) {
|
||||
continue;
|
||||
}
|
||||
JsonObject duplicate = element.getAsJsonObject().deepCopy();
|
||||
duplicate.addProperty("piece", targetPieceKey);
|
||||
expanded.add(duplicate);
|
||||
duplicatedEntries++;
|
||||
}
|
||||
if (duplicatedEntries == 0) {
|
||||
return new PoolMembershipDuplication(content, 0);
|
||||
}
|
||||
pool.add("pieces", expanded);
|
||||
return new PoolMembershipDuplication(
|
||||
(GSON.toJson(pool) + "\n").getBytes(StandardCharsets.UTF_8),
|
||||
duplicatedEntries);
|
||||
}
|
||||
|
||||
static List<String> normalizeThemes(List<String> themes) {
|
||||
Objects.requireNonNull(themes, "Jigsaw Studio piece themes");
|
||||
Set<String> normalized = new LinkedHashSet<>();
|
||||
for (String theme : themes) {
|
||||
Objects.requireNonNull(theme, "Jigsaw Studio piece theme");
|
||||
String key = theme.trim();
|
||||
if (key.isEmpty() || !key.equals(theme)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Jigsaw Studio piece themes must be non-blank and whitespace-normalized");
|
||||
}
|
||||
if (!normalized.add(key)) {
|
||||
throw new IllegalArgumentException("Duplicate Jigsaw Studio piece theme '" + key + "'");
|
||||
}
|
||||
}
|
||||
return List.copyOf(normalized);
|
||||
}
|
||||
|
||||
static PoolEntryRemoval removePoolEntries(
|
||||
byte[] content,
|
||||
String pieceKey,
|
||||
String poolResource
|
||||
) throws IOException {
|
||||
JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8));
|
||||
if (!parsed.isJsonObject()) {
|
||||
throw new IOException("Jigsaw pool is not a JSON object: " + poolResource);
|
||||
}
|
||||
JsonObject pool = parsed.getAsJsonObject();
|
||||
JsonArray pieces = pool.getAsJsonArray("pieces");
|
||||
if (pieces == null) {
|
||||
throw new IOException("Jigsaw pool does not declare a pieces array: " + poolResource);
|
||||
}
|
||||
int removed = 0;
|
||||
for (int index = pieces.size() - 1; index >= 0; index--) {
|
||||
JsonElement entry = pieces.get(index);
|
||||
if (entry.isJsonObject()
|
||||
&& entry.getAsJsonObject().has("piece")
|
||||
&& pieceKey.equals(entry.getAsJsonObject().get("piece").getAsString())) {
|
||||
pieces.remove(index);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
if (removed == 0) {
|
||||
return new PoolEntryRemoval(content, 0);
|
||||
}
|
||||
return new PoolEntryRemoval(
|
||||
(GSON.toJson(pool) + "\n").getBytes(StandardCharsets.UTF_8),
|
||||
removed);
|
||||
}
|
||||
|
||||
static String resourceKey(String relativePath, String prefix, String suffix) {
|
||||
return relativePath.substring(prefix.length(), relativePath.length() - suffix.length());
|
||||
}
|
||||
|
||||
static byte[] addPoolEntry(
|
||||
byte[] content,
|
||||
String pieceKey,
|
||||
int weight,
|
||||
Path poolPath
|
||||
) throws IOException {
|
||||
JsonElement parsed = JsonParser.parseString(new String(content, StandardCharsets.UTF_8));
|
||||
if (!parsed.isJsonObject()) {
|
||||
throw new IOException("Jigsaw pool is not a JSON object: " + poolPath);
|
||||
}
|
||||
JsonObject pool = parsed.getAsJsonObject();
|
||||
if (!pool.has("pieces") || !pool.get("pieces").isJsonArray()) {
|
||||
throw new IOException("Jigsaw pool does not declare a pieces array: " + poolPath);
|
||||
}
|
||||
JsonArray pieces = pool.getAsJsonArray("pieces");
|
||||
for (JsonElement element : pieces) {
|
||||
if (element.isJsonObject()
|
||||
&& element.getAsJsonObject().has("piece")
|
||||
&& pieceKey.equals(element.getAsJsonObject().get("piece").getAsString())) {
|
||||
throw new IOException("Pool already references piece '" + pieceKey + "'");
|
||||
}
|
||||
}
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.addProperty("piece", pieceKey);
|
||||
entry.addProperty("weight", weight);
|
||||
pieces.add(entry);
|
||||
return (GSON.toJson(pool) + "\n").getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static String normalizeChannel(String channel) {
|
||||
String normalized = channel == null ? "" : channel.trim();
|
||||
if (normalized.equalsIgnoreCase("none")) {
|
||||
return "";
|
||||
}
|
||||
if (normalized.length() > 128) {
|
||||
throw new IllegalArgumentException("Jigsaw connector channels cannot exceed 128 characters");
|
||||
}
|
||||
for (int index = 0; index < normalized.length(); index++) {
|
||||
if (Character.isWhitespace(normalized.charAt(index))) {
|
||||
throw new IllegalArgumentException("Jigsaw connector channels cannot contain whitespace");
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
record PoolEntryRemoval(byte[] content, int removedEntries) {
|
||||
PoolEntryRemoval {
|
||||
Objects.requireNonNull(content, "Jigsaw Studio pool entry removal content");
|
||||
if (removedEntries < 0) {
|
||||
throw new IllegalArgumentException("Removed jigsaw pool entry count cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record PoolMembershipDuplication(byte[] content, int duplicatedEntries) {
|
||||
PoolMembershipDuplication {
|
||||
Objects.requireNonNull(content, "Jigsaw Studio duplicated pool content");
|
||||
if (duplicatedEntries < 0) {
|
||||
throw new IllegalArgumentException("Duplicated pool membership count cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
-977
File diff suppressed because it is too large
Load Diff
+153
@@ -0,0 +1,153 @@
|
||||
package art.arcane.iris.core.runtime.jigsaw;
|
||||
|
||||
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
|
||||
import art.arcane.iris.engine.framework.structure.PlanarJigsawWorkcellResolver;
|
||||
import art.arcane.iris.engine.object.IrisJigsawMode;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
final class JigsawStudioGraphInspector {
|
||||
private JigsawStudioGraphInspector() {
|
||||
}
|
||||
|
||||
static void requireAvailableVariantTarget(
|
||||
JigsawStudioGraphEditor.OwnedGraph graph,
|
||||
String relativePath
|
||||
) throws IOException {
|
||||
StructureResourceBundle.validateRelativePath(relativePath);
|
||||
Path target = graph.root().resolve(relativePath).normalize();
|
||||
if (!target.startsWith(graph.root())) {
|
||||
throw new IOException("Variant target escapes the pack root: " + relativePath);
|
||||
}
|
||||
if (graph.manifest().resourceHashes().containsKey(relativePath)
|
||||
|| Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Variant target already exists: " + relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
static void requireAvailableThemeSetTarget(
|
||||
JigsawStudioGraphEditor.OwnedGraph graph,
|
||||
String relativePath
|
||||
) throws IOException {
|
||||
StructureResourceBundle.validateRelativePath(relativePath);
|
||||
Path target = graph.root().resolve(relativePath).normalize();
|
||||
if (!target.startsWith(graph.root())) {
|
||||
throw new IOException("Theme-set target escapes the pack root: " + relativePath);
|
||||
}
|
||||
if (graph.manifest().resourceHashes().containsKey(relativePath)
|
||||
|| Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Theme-set target already exists: " + relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean otherOwnedPieceReferencesObject(
|
||||
JigsawStudioGraphEditor.OwnedGraph graph,
|
||||
String targetPieceKey,
|
||||
String objectKey
|
||||
) throws IOException {
|
||||
for (String relativePath : graph.manifest().resourceHashes().keySet()) {
|
||||
if (!relativePath.startsWith("jigsaw-pieces/") || !relativePath.endsWith(".json")) {
|
||||
continue;
|
||||
}
|
||||
String pieceKey = JigsawStudioGraphDocuments.resourceKey(relativePath, "jigsaw-pieces/", ".json");
|
||||
if (pieceKey.equals(targetPieceKey)) {
|
||||
continue;
|
||||
}
|
||||
IrisJigsawPiece piece = JigsawStudioGraphEditor.readPiece(JigsawStudioGraphEditor.resolveOwnedResource(graph.root(), relativePath), relativePath);
|
||||
if (objectKey.equals(piece.getObject())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void requireDeletableArchetype(
|
||||
JigsawStudioGraphEditor.OwnedGraph graph,
|
||||
String targetPieceKey,
|
||||
IrisJigsawPiece targetPiece
|
||||
) throws IOException {
|
||||
String structureResource = "structures/" + graph.manifest().structure().path() + ".json";
|
||||
if (!graph.manifest().resourceHashes().containsKey(structureResource)) {
|
||||
throw new IOException("The owned graph manifest does not include " + structureResource);
|
||||
}
|
||||
IrisStructure structure = JigsawStudioGraphEditor.readStructure(
|
||||
JigsawStudioGraphEditor.resolveOwnedResource(graph.root(), structureResource),
|
||||
structureResource);
|
||||
if (structure.resolvedMode() == IrisJigsawMode.SPATIAL_JIGSAW) {
|
||||
if (countOtherOwnedPieces(graph, targetPieceKey, null) == 0) {
|
||||
throw new IOException("Cannot delete the final spatial jigsaw variant");
|
||||
}
|
||||
return;
|
||||
}
|
||||
IrisJigsawWorkcellArchetype archetype = IrisJigsawWorkcellArchetype.fromPiece(targetPiece);
|
||||
Map<IrisJigsawWorkcellArchetype, PlanarJigsawWorkcellResolver.ResolvedWorkcell> workcells;
|
||||
try {
|
||||
workcells = PlanarJigsawWorkcellResolver.resolve(structure);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException("Planar workcell configuration is invalid: "
|
||||
+ exception.getMessage(), exception);
|
||||
}
|
||||
PlanarJigsawWorkcellResolver.ResolvedWorkcell workcell = workcells.get(archetype);
|
||||
if (workcell != null && workcell.enabled()
|
||||
&& countOtherOwnedPieces(graph, targetPieceKey, archetype) == 0) {
|
||||
throw new IOException("Cannot delete the final variant for enabled planar workcell "
|
||||
+ JigsawPlanarArchetype.fromModel(archetype).stableId());
|
||||
}
|
||||
}
|
||||
|
||||
static int countOtherOwnedPieces(
|
||||
JigsawStudioGraphEditor.OwnedGraph graph,
|
||||
String targetPieceKey,
|
||||
IrisJigsawWorkcellArchetype archetype
|
||||
) throws IOException {
|
||||
int count = 0;
|
||||
for (String relativePath : graph.manifest().resourceHashes().keySet()) {
|
||||
if (!relativePath.startsWith("jigsaw-pieces/") || !relativePath.endsWith(".json")) {
|
||||
continue;
|
||||
}
|
||||
String pieceKey = JigsawStudioGraphDocuments.resourceKey(relativePath, "jigsaw-pieces/", ".json");
|
||||
if (pieceKey.equals(targetPieceKey)) {
|
||||
continue;
|
||||
}
|
||||
if (archetype == null) {
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
IrisJigsawPiece piece = JigsawStudioGraphEditor.readPiece(JigsawStudioGraphEditor.resolveOwnedResource(graph.root(), relativePath), relativePath);
|
||||
if (IrisJigsawWorkcellArchetype.fromPiece(piece) == archetype) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void requireExclusiveObjectReference(
|
||||
JigsawStudioGraphEditor.OwnedGraph graph,
|
||||
String activePieceKey,
|
||||
String objectKey
|
||||
) throws IOException {
|
||||
for (String relativePath : graph.manifest().resourceHashes().keySet()) {
|
||||
if (!relativePath.startsWith("jigsaw-pieces/") || !relativePath.endsWith(".json")) {
|
||||
continue;
|
||||
}
|
||||
String pieceKey = relativePath.substring(
|
||||
"jigsaw-pieces/".length(),
|
||||
relativePath.length() - ".json".length());
|
||||
if (pieceKey.equals(activePieceKey)) {
|
||||
continue;
|
||||
}
|
||||
IrisJigsawPiece piece = JigsawStudioGraphEditor.readPiece(JigsawStudioGraphEditor.resolveOwnedResource(graph.root(), relativePath), relativePath);
|
||||
if (objectKey.equals(piece.getObject())) {
|
||||
throw new IOException("Object '" + objectKey + "' is shared by piece '" + pieceKey
|
||||
+ "'; duplicate the active variant before resizing its object bounds");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+548
@@ -0,0 +1,548 @@
|
||||
package art.arcane.iris.core.runtime.jigsaw;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDirection;
|
||||
import art.arcane.iris.engine.object.IrisJigsawConnector;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisJigsawWorkcellArchetype;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
final class JigsawStudioObjectResizer {
|
||||
private JigsawStudioObjectResizer() {
|
||||
}
|
||||
|
||||
static PlanarPieceObjectResize resizePlanarPieceObject(
|
||||
IrisObject source,
|
||||
IrisJigsawPiece piece,
|
||||
JigsawPlanarArchetype archetype,
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
IrisObject sourceObject = Objects.requireNonNull(source, "Planar Jigsaw Studio source object");
|
||||
IrisJigsawPiece targetPiece = Objects.requireNonNull(piece, "Planar Jigsaw Studio piece");
|
||||
JigsawPlanarArchetype targetArchetype = Objects.requireNonNull(
|
||||
archetype,
|
||||
"Planar Jigsaw Studio archetype");
|
||||
JigsawStudioCellDimensions targetDimensions = Objects.requireNonNull(
|
||||
dimensions,
|
||||
"Planar Jigsaw Studio target dimensions");
|
||||
String targetPieceKey = pieceKey == null || pieceKey.isBlank() ? "unknown" : pieceKey;
|
||||
if (targetDimensions.width() < 3 || targetDimensions.depth() < 3) {
|
||||
throw new IllegalArgumentException(
|
||||
"Planar Jigsaw Studio workcell width and depth must each be at least 3 blocks");
|
||||
}
|
||||
if (sourceObject.getW() < 1 || sourceObject.getH() < 1 || sourceObject.getD() < 1) {
|
||||
throw new IOException("Planar piece '" + targetPieceKey + "' has invalid object dimensions");
|
||||
}
|
||||
if (IrisJigsawWorkcellArchetype.fromPiece(targetPiece) != targetArchetype.modelArchetype()) {
|
||||
throw new IOException("Planar piece '" + targetPieceKey + "' does not belong to "
|
||||
+ targetArchetype.stableId());
|
||||
}
|
||||
|
||||
int quarterTurns = targetArchetype.modelArchetype().sourceToCanonicalQuarterTurns(targetPiece);
|
||||
JigsawStudioCellDimensions sourceDimensions = new JigsawStudioCellDimensions(
|
||||
sourceObject.getW(),
|
||||
sourceObject.getH(),
|
||||
sourceObject.getD());
|
||||
JigsawStudioCellDimensions canonicalDimensions = canonicalDimensions(sourceDimensions, quarterTurns);
|
||||
Map<LocalPosition, PlatformBlockState> canonicalBlocks = canonicalBlocks(
|
||||
sourceObject,
|
||||
quarterTurns,
|
||||
targetPieceKey);
|
||||
Map<LocalPosition, TileData> canonicalTiles = canonicalTiles(
|
||||
sourceObject,
|
||||
quarterTurns,
|
||||
targetPieceKey);
|
||||
List<IrisJigsawConnector> connectors = targetPiece.getConnectors();
|
||||
if (connectors == null) {
|
||||
throw new IOException("Planar piece '" + targetPieceKey + "' has no connector list");
|
||||
}
|
||||
List<ConnectorResize> connectorResizes = planConnectorResizes(
|
||||
connectors,
|
||||
sourceDimensions,
|
||||
canonicalDimensions,
|
||||
targetDimensions,
|
||||
quarterTurns,
|
||||
targetPieceKey);
|
||||
relocateConnectorPayloads(canonicalBlocks, canonicalTiles, connectorResizes, targetPieceKey);
|
||||
requireContentInsideTarget(canonicalBlocks, canonicalTiles, targetDimensions, targetPieceKey);
|
||||
|
||||
JigsawStudioCellDimensions resizedSourceDimensions = sourceDimensions(targetDimensions, quarterTurns);
|
||||
IrisObject resizedObject = rebuildSourceObject(
|
||||
canonicalBlocks,
|
||||
canonicalTiles,
|
||||
resizedSourceDimensions,
|
||||
quarterTurns,
|
||||
targetPieceKey);
|
||||
int relocatedConnectors = 0;
|
||||
for (ConnectorResize connectorResize : connectorResizes) {
|
||||
LocalPosition sourcePosition = toSource(
|
||||
connectorResize.targetCanonical(),
|
||||
resizedSourceDimensions,
|
||||
quarterTurns);
|
||||
connectorResize.connector().setPosition(sourcePosition.toIrisPosition());
|
||||
if (!connectorResize.sourceCanonical().equals(connectorResize.targetCanonical())) {
|
||||
relocatedConnectors++;
|
||||
}
|
||||
}
|
||||
return new PlanarPieceObjectResize(resizedObject, relocatedConnectors);
|
||||
}
|
||||
|
||||
private static Map<LocalPosition, PlatformBlockState> canonicalBlocks(
|
||||
IrisObject source,
|
||||
int quarterTurns,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
Map<LocalPosition, PlatformBlockState> blocks = new LinkedHashMap<>();
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : source.getBlocks()) {
|
||||
LocalPosition sourcePosition = unsignedPosition(entry.getKey(), source);
|
||||
requireInside(sourcePosition, source.getW(), source.getH(), source.getD(),
|
||||
"stored block", pieceKey);
|
||||
PlatformBlockState state = entry.getValue();
|
||||
if (state == null) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' contains a null stored block state");
|
||||
}
|
||||
LocalPosition canonicalPosition = toCanonical(
|
||||
sourcePosition,
|
||||
source.getW(),
|
||||
source.getD(),
|
||||
quarterTurns);
|
||||
if (blocks.putIfAbsent(canonicalPosition, state) != null) {
|
||||
throw new IOException("Planar piece '" + pieceKey
|
||||
+ "' maps more than one stored block to " + canonicalPosition.describe());
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static Map<LocalPosition, TileData> canonicalTiles(
|
||||
IrisObject source,
|
||||
int quarterTurns,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
Map<LocalPosition, TileData> tiles = new LinkedHashMap<>();
|
||||
for (Map.Entry<IrisBlockVector, TileData> entry : source.getStates()) {
|
||||
LocalPosition sourcePosition = unsignedPosition(entry.getKey(), source);
|
||||
requireInside(sourcePosition, source.getW(), source.getH(), source.getD(),
|
||||
"tile data", pieceKey);
|
||||
TileData tile = entry.getValue();
|
||||
if (tile == null) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' contains null tile data");
|
||||
}
|
||||
LocalPosition canonicalPosition = toCanonical(
|
||||
sourcePosition,
|
||||
source.getW(),
|
||||
source.getD(),
|
||||
quarterTurns);
|
||||
if (tiles.putIfAbsent(canonicalPosition, tile.clone()) != null) {
|
||||
throw new IOException("Planar piece '" + pieceKey
|
||||
+ "' maps more than one tile payload to " + canonicalPosition.describe());
|
||||
}
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
private static List<ConnectorResize> planConnectorResizes(
|
||||
List<IrisJigsawConnector> connectors,
|
||||
JigsawStudioCellDimensions sourceDimensions,
|
||||
JigsawStudioCellDimensions canonicalDimensions,
|
||||
JigsawStudioCellDimensions targetDimensions,
|
||||
int quarterTurns,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
List<ConnectorResize> planned = new ArrayList<>(connectors.size());
|
||||
Set<LocalPosition> sourcePositions = new LinkedHashSet<>();
|
||||
Set<LocalPosition> targetPositions = new LinkedHashSet<>();
|
||||
IrisPosition sourceSize = dimensionsPosition(sourceDimensions);
|
||||
IrisPosition canonicalSize = dimensionsPosition(canonicalDimensions);
|
||||
IrisPosition targetSize = dimensionsPosition(targetDimensions);
|
||||
for (int index = 0; index < connectors.size(); index++) {
|
||||
IrisJigsawConnector connector = connectors.get(index);
|
||||
if (connector == null || connector.getPosition() == null || connector.getDirection() == null
|
||||
|| connector.getDirection().isVertical()) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' contains invalid connector " + index);
|
||||
}
|
||||
LocalPosition sourcePosition = LocalPosition.from(connector.getPosition());
|
||||
requireInside(
|
||||
sourcePosition,
|
||||
sourceDimensions.width(),
|
||||
sourceDimensions.height(),
|
||||
sourceDimensions.depth(),
|
||||
"connector " + index,
|
||||
pieceKey);
|
||||
if (!sourcePositions.add(sourcePosition)) {
|
||||
throw new IOException("Planar piece '" + pieceKey
|
||||
+ "' has multiple connectors at " + sourcePosition.describe());
|
||||
}
|
||||
LocalPosition sourceCanonical = toCanonical(
|
||||
sourcePosition,
|
||||
sourceDimensions.width(),
|
||||
sourceDimensions.depth(),
|
||||
quarterTurns);
|
||||
IrisDirection canonicalDirection = rotateHorizontalDirection(
|
||||
connector.getDirection(),
|
||||
quarterTurns);
|
||||
LocalPosition expectedSource = LocalPosition.from(IrisJigsawConnector.canonicalPlanarPosition(
|
||||
sourceSize,
|
||||
connector.getDirection()));
|
||||
LocalPosition expectedCanonical = LocalPosition.from(IrisJigsawConnector.canonicalPlanarPosition(
|
||||
canonicalSize,
|
||||
canonicalDirection));
|
||||
boolean canonicalConnector = sourcePosition.equals(expectedSource)
|
||||
|| sourceCanonical.equals(expectedCanonical);
|
||||
LocalPosition targetCanonical = canonicalConnector
|
||||
? LocalPosition.from(IrisJigsawConnector.canonicalPlanarPosition(
|
||||
targetSize,
|
||||
canonicalDirection))
|
||||
: sourceCanonical;
|
||||
if (!inside(targetCanonical, targetDimensions)) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' connector " + index + " at "
|
||||
+ sourceCanonical.describe() + " would be cropped by the requested workcell bounds");
|
||||
}
|
||||
if (!targetPositions.add(targetCanonical)) {
|
||||
throw new IOException("Planar piece '" + pieceKey
|
||||
+ "' would place multiple connectors at " + targetCanonical.describe());
|
||||
}
|
||||
planned.add(new ConnectorResize(connector, sourceCanonical, targetCanonical));
|
||||
}
|
||||
return List.copyOf(planned);
|
||||
}
|
||||
|
||||
private static void relocateConnectorPayloads(
|
||||
Map<LocalPosition, PlatformBlockState> blocks,
|
||||
Map<LocalPosition, TileData> tiles,
|
||||
List<ConnectorResize> connectorResizes,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
Set<LocalPosition> relocatedSources = new LinkedHashSet<>();
|
||||
for (ConnectorResize connectorResize : connectorResizes) {
|
||||
if (tiles.containsKey(connectorResize.sourceCanonical())
|
||||
|| tiles.containsKey(connectorResize.targetCanonical())) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' has tile data at connector position "
|
||||
+ connectorResize.sourceCanonical().describe()
|
||||
+ "; connector tile data cannot be resized safely");
|
||||
}
|
||||
if (!connectorResize.sourceCanonical().equals(connectorResize.targetCanonical())) {
|
||||
relocatedSources.add(connectorResize.sourceCanonical());
|
||||
}
|
||||
}
|
||||
for (ConnectorResize connectorResize : connectorResizes) {
|
||||
if (connectorResize.sourceCanonical().equals(connectorResize.targetCanonical())) {
|
||||
continue;
|
||||
}
|
||||
if (blocks.containsKey(connectorResize.targetCanonical())
|
||||
&& !relocatedSources.contains(connectorResize.targetCanonical())) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' cannot relocate connector from "
|
||||
+ connectorResize.sourceCanonical().describe() + " to "
|
||||
+ connectorResize.targetCanonical().describe()
|
||||
+ " because the destination contains a stored block");
|
||||
}
|
||||
}
|
||||
List<BlockRelocation> payloads = new ArrayList<>(relocatedSources.size());
|
||||
for (ConnectorResize connectorResize : connectorResizes) {
|
||||
if (connectorResize.sourceCanonical().equals(connectorResize.targetCanonical())) {
|
||||
continue;
|
||||
}
|
||||
boolean present = blocks.containsKey(connectorResize.sourceCanonical());
|
||||
PlatformBlockState state = blocks.remove(connectorResize.sourceCanonical());
|
||||
payloads.add(new BlockRelocation(connectorResize.targetCanonical(), state, present));
|
||||
}
|
||||
for (BlockRelocation payload : payloads) {
|
||||
if (!payload.present()) {
|
||||
continue;
|
||||
}
|
||||
if (blocks.putIfAbsent(payload.target(), payload.state()) != null) {
|
||||
throw new IOException("Planar piece '" + pieceKey
|
||||
+ "' has colliding connector block payloads at " + payload.target().describe());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireContentInsideTarget(
|
||||
Map<LocalPosition, PlatformBlockState> blocks,
|
||||
Map<LocalPosition, TileData> tiles,
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
for (LocalPosition position : blocks.keySet()) {
|
||||
if (!inside(position, dimensions)) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' has a stored block, including explicit air, at "
|
||||
+ position.describe() + " that would be cropped by the requested workcell bounds");
|
||||
}
|
||||
}
|
||||
for (LocalPosition position : tiles.keySet()) {
|
||||
if (!inside(position, dimensions)) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' has tile data at "
|
||||
+ position.describe() + " that would be cropped by the requested workcell bounds");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisObject rebuildSourceObject(
|
||||
Map<LocalPosition, PlatformBlockState> canonicalBlocks,
|
||||
Map<LocalPosition, TileData> canonicalTiles,
|
||||
JigsawStudioCellDimensions sourceDimensions,
|
||||
int quarterTurns,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
IrisObject resized = new IrisObject(
|
||||
sourceDimensions.width(),
|
||||
sourceDimensions.height(),
|
||||
sourceDimensions.depth());
|
||||
Set<LocalPosition> sourcePositions = new LinkedHashSet<>();
|
||||
for (Map.Entry<LocalPosition, PlatformBlockState> entry : canonicalBlocks.entrySet()) {
|
||||
LocalPosition sourcePosition = toSource(entry.getKey(), sourceDimensions, quarterTurns);
|
||||
if (!sourcePositions.add(sourcePosition)) {
|
||||
throw new IOException("Planar piece '" + pieceKey
|
||||
+ "' maps multiple stored blocks to " + sourcePosition.describe());
|
||||
}
|
||||
resized.setUnsigned(
|
||||
sourcePosition.x(),
|
||||
sourcePosition.y(),
|
||||
sourcePosition.z(),
|
||||
entry.getValue());
|
||||
}
|
||||
sourcePositions.clear();
|
||||
for (Map.Entry<LocalPosition, TileData> entry : canonicalTiles.entrySet()) {
|
||||
LocalPosition sourcePosition = toSource(entry.getKey(), sourceDimensions, quarterTurns);
|
||||
if (!sourcePositions.add(sourcePosition)) {
|
||||
throw new IOException("Planar piece '" + pieceKey
|
||||
+ "' maps multiple tile payloads to " + sourcePosition.describe());
|
||||
}
|
||||
resized.setUnsignedTile(
|
||||
sourcePosition.x(),
|
||||
sourcePosition.y(),
|
||||
sourcePosition.z(),
|
||||
entry.getValue().clone());
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
|
||||
private static LocalPosition unsignedPosition(IrisBlockVector signed, IrisObject object) {
|
||||
return new LocalPosition(
|
||||
signed.getBlockX() + object.getCenter().getBlockX(),
|
||||
signed.getBlockY() + object.getCenter().getBlockY(),
|
||||
signed.getBlockZ() + object.getCenter().getBlockZ());
|
||||
}
|
||||
|
||||
static JigsawStudioCellDimensions canonicalDimensions(
|
||||
JigsawStudioCellDimensions source,
|
||||
int quarterTurns
|
||||
) {
|
||||
return Math.floorMod(quarterTurns, 2) == 0
|
||||
? source
|
||||
: new JigsawStudioCellDimensions(source.depth(), source.height(), source.width());
|
||||
}
|
||||
|
||||
private static JigsawStudioCellDimensions sourceDimensions(
|
||||
JigsawStudioCellDimensions canonical,
|
||||
int quarterTurns
|
||||
) {
|
||||
return Math.floorMod(quarterTurns, 2) == 0
|
||||
? canonical
|
||||
: new JigsawStudioCellDimensions(canonical.depth(), canonical.height(), canonical.width());
|
||||
}
|
||||
|
||||
private static LocalPosition toCanonical(
|
||||
LocalPosition source,
|
||||
int sourceWidth,
|
||||
int sourceDepth,
|
||||
int quarterTurns
|
||||
) {
|
||||
return switch (Math.floorMod(quarterTurns, 4)) {
|
||||
case 0 -> source;
|
||||
case 1 -> new LocalPosition(sourceDepth - 1 - source.z(), source.y(), source.x());
|
||||
case 2 -> new LocalPosition(
|
||||
sourceWidth - 1 - source.x(),
|
||||
source.y(),
|
||||
sourceDepth - 1 - source.z());
|
||||
case 3 -> new LocalPosition(source.z(), source.y(), sourceWidth - 1 - source.x());
|
||||
default -> throw new IllegalStateException("Unreachable planar object rotation");
|
||||
};
|
||||
}
|
||||
|
||||
private static LocalPosition toSource(
|
||||
LocalPosition canonical,
|
||||
JigsawStudioCellDimensions sourceDimensions,
|
||||
int quarterTurns
|
||||
) {
|
||||
return switch (Math.floorMod(quarterTurns, 4)) {
|
||||
case 0 -> canonical;
|
||||
case 1 -> new LocalPosition(
|
||||
canonical.z(),
|
||||
canonical.y(),
|
||||
sourceDimensions.depth() - 1 - canonical.x());
|
||||
case 2 -> new LocalPosition(
|
||||
sourceDimensions.width() - 1 - canonical.x(),
|
||||
canonical.y(),
|
||||
sourceDimensions.depth() - 1 - canonical.z());
|
||||
case 3 -> new LocalPosition(
|
||||
sourceDimensions.width() - 1 - canonical.z(),
|
||||
canonical.y(),
|
||||
canonical.x());
|
||||
default -> throw new IllegalStateException("Unreachable planar object rotation");
|
||||
};
|
||||
}
|
||||
|
||||
private static IrisDirection rotateHorizontalDirection(IrisDirection direction, int quarterTurns) {
|
||||
JigsawPlanarDirection planarDirection = switch (direction) {
|
||||
case NORTH_NEGATIVE_Z -> JigsawPlanarDirection.NORTH;
|
||||
case EAST_POSITIVE_X -> JigsawPlanarDirection.EAST;
|
||||
case SOUTH_POSITIVE_Z -> JigsawPlanarDirection.SOUTH;
|
||||
case WEST_NEGATIVE_X -> JigsawPlanarDirection.WEST;
|
||||
case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> throw new IllegalArgumentException(
|
||||
"Planar connector direction must be horizontal");
|
||||
};
|
||||
return planarDirection.rotateClockwise(quarterTurns).irisDirection();
|
||||
}
|
||||
|
||||
static IrisPosition dimensionsPosition(JigsawStudioCellDimensions dimensions) {
|
||||
return new IrisPosition(dimensions.width(), dimensions.height(), dimensions.depth());
|
||||
}
|
||||
|
||||
private static boolean inside(LocalPosition position, JigsawStudioCellDimensions dimensions) {
|
||||
return inside(position, dimensions.width(), dimensions.height(), dimensions.depth());
|
||||
}
|
||||
|
||||
private static boolean inside(LocalPosition position, int width, int height, int depth) {
|
||||
return position.x() >= 0 && position.x() < width
|
||||
&& position.y() >= 0 && position.y() < height
|
||||
&& position.z() >= 0 && position.z() < depth;
|
||||
}
|
||||
|
||||
private static void requireInside(
|
||||
LocalPosition position,
|
||||
int width,
|
||||
int height,
|
||||
int depth,
|
||||
String content,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
if (!inside(position, width, height, depth)) {
|
||||
throw new IOException("Planar piece '" + pieceKey + "' has " + content + " at "
|
||||
+ position.describe() + " outside its object bounds");
|
||||
}
|
||||
}
|
||||
|
||||
static IrisObject resizeObject(
|
||||
IrisObject source,
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
IrisObject object = Objects.requireNonNull(source, "Jigsaw Studio source object");
|
||||
JigsawStudioCellDimensions target = Objects.requireNonNull(
|
||||
dimensions,
|
||||
"Jigsaw Studio target object dimensions");
|
||||
String normalizedPiece = pieceKey == null || pieceKey.isBlank() ? "unknown" : pieceKey;
|
||||
IrisObject resized = new IrisObject(target.width(), target.height(), target.depth());
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : object.getBlocks()) {
|
||||
IrisBlockVector position = entry.getKey();
|
||||
LocalPosition unsigned = unsignedPosition(position, object);
|
||||
if (!inside(unsigned, target)) {
|
||||
throw new IOException("Spatial piece '" + normalizedPiece
|
||||
+ "' has a stored block, including explicit air, at " + unsigned.describe()
|
||||
+ " that would be cropped by the requested variant size");
|
||||
}
|
||||
resized.setUnsigned(
|
||||
unsigned.x(),
|
||||
unsigned.y(),
|
||||
unsigned.z(),
|
||||
entry.getValue());
|
||||
}
|
||||
for (Map.Entry<IrisBlockVector, TileData> entry : object.getStates()) {
|
||||
IrisBlockVector position = entry.getKey();
|
||||
LocalPosition unsigned = unsignedPosition(position, object);
|
||||
if (!inside(unsigned, target)) {
|
||||
throw new IOException("Spatial piece '" + normalizedPiece + "' has tile data at "
|
||||
+ unsigned.describe() + " that would be cropped by the requested variant size");
|
||||
}
|
||||
resized.setUnsignedTile(
|
||||
unsigned.x(),
|
||||
unsigned.y(),
|
||||
unsigned.z(),
|
||||
entry.getValue().clone());
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
|
||||
static void requireConnectorsInside(
|
||||
IrisJigsawPiece piece,
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
String pieceKey
|
||||
) throws IOException {
|
||||
if (piece.getConnectors() == null) {
|
||||
throw new IOException("Spatial piece '" + pieceKey + "' has no connector list");
|
||||
}
|
||||
for (int index = 0; index < piece.getConnectors().size(); index++) {
|
||||
IrisJigsawConnector connector = piece.getConnectors().get(index);
|
||||
if (connector == null || connector.getPosition() == null) {
|
||||
throw new IOException("Spatial piece '" + pieceKey + "' contains invalid connector " + index);
|
||||
}
|
||||
LocalPosition position = LocalPosition.from(connector.getPosition());
|
||||
if (!inside(position, dimensions)) {
|
||||
throw new IOException("Spatial piece '" + pieceKey + "' connector " + index + " at "
|
||||
+ position.describe() + " would be cropped by the requested variant size");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String describeDimensions(JigsawStudioCellDimensions dimensions) {
|
||||
return dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth();
|
||||
}
|
||||
|
||||
record PlanarPieceObjectResize(IrisObject object, int relocatedConnectors) {
|
||||
PlanarPieceObjectResize {
|
||||
Objects.requireNonNull(object, "Resized planar Jigsaw Studio object");
|
||||
if (relocatedConnectors < 0) {
|
||||
throw new IllegalArgumentException("Relocated connector count cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record ConnectorResize(
|
||||
IrisJigsawConnector connector,
|
||||
LocalPosition sourceCanonical,
|
||||
LocalPosition targetCanonical
|
||||
) {
|
||||
private ConnectorResize {
|
||||
Objects.requireNonNull(connector, "Planar Jigsaw Studio connector");
|
||||
Objects.requireNonNull(sourceCanonical, "Planar Jigsaw Studio source connector position");
|
||||
Objects.requireNonNull(targetCanonical, "Planar Jigsaw Studio target connector position");
|
||||
}
|
||||
}
|
||||
|
||||
private record BlockRelocation(LocalPosition target, PlatformBlockState state, boolean present) {
|
||||
private BlockRelocation {
|
||||
Objects.requireNonNull(target, "Planar Jigsaw Studio connector block target");
|
||||
if (present) {
|
||||
Objects.requireNonNull(state, "Planar Jigsaw Studio connector block state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record LocalPosition(int x, int y, int z) {
|
||||
private static LocalPosition from(IrisPosition position) {
|
||||
return new LocalPosition(position.getX(), position.getY(), position.getZ());
|
||||
}
|
||||
|
||||
private IrisPosition toIrisPosition() {
|
||||
return new IrisPosition(x, y, z);
|
||||
}
|
||||
|
||||
private String describe() {
|
||||
return x + "," + y + "," + z;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -147,8 +147,8 @@ public class JigsawStudioGraphEditorResizeTest {
|
||||
source.setUnsigned(4, 1, 1, stone);
|
||||
IrisJigsawPiece piece = endPiece(IrisDirection.EAST_POSITIVE_X, new IrisPosition(6, 1, 2));
|
||||
|
||||
JigsawStudioGraphEditor.PlanarPieceObjectResize result =
|
||||
JigsawStudioGraphEditor.resizePlanarPieceObject(
|
||||
JigsawStudioObjectResizer.PlanarPieceObjectResize result =
|
||||
JigsawStudioObjectResizer.resizePlanarPieceObject(
|
||||
source,
|
||||
piece,
|
||||
JigsawPlanarArchetype.END,
|
||||
@@ -175,7 +175,7 @@ public class JigsawStudioGraphEditorResizeTest {
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> JigsawStudioGraphEditor.resizePlanarPieceObject(
|
||||
() -> JigsawStudioObjectResizer.resizePlanarPieceObject(
|
||||
source,
|
||||
piece,
|
||||
JigsawPlanarArchetype.BLANK,
|
||||
@@ -199,7 +199,7 @@ public class JigsawStudioGraphEditorResizeTest {
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> JigsawStudioGraphEditor.resizePlanarPieceObject(
|
||||
() -> JigsawStudioObjectResizer.resizePlanarPieceObject(
|
||||
source,
|
||||
piece,
|
||||
JigsawPlanarArchetype.END,
|
||||
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user