mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +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:
1 parent
a4e22006e0
commit
e7c6b6e08e
6 files changed
+1050
-981
No files matched your search
+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);
|
source.setUnsigned(4, 1, 1, stone);
|
||||||
IrisJigsawPiece piece = endPiece(IrisDirection.EAST_POSITIVE_X, new IrisPosition(6, 1, 2));
|
IrisJigsawPiece piece = endPiece(IrisDirection.EAST_POSITIVE_X, new IrisPosition(6, 1, 2));
|
||||||
|
|
||||||
JigsawStudioGraphEditor.PlanarPieceObjectResize result =
|
JigsawStudioObjectResizer.PlanarPieceObjectResize result =
|
||||||
JigsawStudioGraphEditor.resizePlanarPieceObject(
|
JigsawStudioObjectResizer.resizePlanarPieceObject(
|
||||||
source,
|
source,
|
||||||
piece,
|
piece,
|
||||||
JigsawPlanarArchetype.END,
|
JigsawPlanarArchetype.END,
|
||||||
@@ -175,7 +175,7 @@ public class JigsawStudioGraphEditorResizeTest {
|
|||||||
|
|
||||||
IOException failure = assertThrows(
|
IOException failure = assertThrows(
|
||||||
IOException.class,
|
IOException.class,
|
||||||
() -> JigsawStudioGraphEditor.resizePlanarPieceObject(
|
() -> JigsawStudioObjectResizer.resizePlanarPieceObject(
|
||||||
source,
|
source,
|
||||||
piece,
|
piece,
|
||||||
JigsawPlanarArchetype.BLANK,
|
JigsawPlanarArchetype.BLANK,
|
||||||
@@ -199,7 +199,7 @@ public class JigsawStudioGraphEditorResizeTest {
|
|||||||
|
|
||||||
IOException failure = assertThrows(
|
IOException failure = assertThrows(
|
||||||
IOException.class,
|
IOException.class,
|
||||||
() -> JigsawStudioGraphEditor.resizePlanarPieceObject(
|
() -> JigsawStudioObjectResizer.resizePlanarPieceObject(
|
||||||
source,
|
source,
|
||||||
piece,
|
piece,
|
||||||
JigsawPlanarArchetype.END,
|
JigsawPlanarArchetype.END,
|
||||||
|
|||||||
BIN
Binary file not shown.
Reference in new issue
Block a user