mesh import fit ignores far-off stray pieces (with hint), project load warnings with readable names, edit projects of imported levelbuild wads load again (header runs applied to original values), -0.0 kept in saved arrays; start cameras checked in game on the test world and a village 2 base
This commit is contained in:
1 parent
ec3c619120
commit
d56f3c604f
14 files changed
+215
-20
No files matched your search
@@ -20,6 +20,7 @@ import java.util.Map;
|
||||
*/
|
||||
final class NodeCodec {
|
||||
static final int INLINE = 64;
|
||||
private static final long NEG_ZERO = Double.doubleToRawLongBits(-0.0);
|
||||
|
||||
private NodeCodec() {}
|
||||
|
||||
@@ -54,7 +55,9 @@ final class NodeCodec {
|
||||
}
|
||||
|
||||
private static Object doubles(double[] a) {
|
||||
if (a.length <= INLINE) {
|
||||
// JSON numbers lose the sign of -0.0 (imported models have it; it changes the built bytes)
|
||||
if (a.length <= INLINE
|
||||
&& java.util.Arrays.stream(a).noneMatch(x -> Double.doubleToRawLongBits(x) == NEG_ZERO)) {
|
||||
List<Object> l = new ArrayList<>(a.length);
|
||||
for (double x : a) {
|
||||
l.add(x);
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.eliasmueller.llamamod.editor.model.NodeKind;
|
||||
import com.eliasmueller.llamamod.editor.model.NodeList;
|
||||
import com.eliasmueller.llamamod.editor.model.Prop;
|
||||
import com.eliasmueller.llamamod.editor.model.PropType;
|
||||
import com.eliasmueller.llamamod.editor.model.ReadableNames;
|
||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.editor.model.TexturePage;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.Values;
|
||||
@@ -15,6 +16,7 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -154,6 +156,8 @@ final class SceneDiff {
|
||||
Map<String, Object> nodes = (Map<String, Object>) json.getOrDefault("nodes", Map.of());
|
||||
// 1. removals, 2. create added nodes (all kinds, so references resolve), 3. set values
|
||||
List<Map.Entry<Node, Map<String, Object>>> fill = new ArrayList<>();
|
||||
// per-node problems, named once the level order is final (readable names are numbered by it)
|
||||
List<Map.Entry<Node, String>> nodeWarn = new ArrayList<>();
|
||||
for (NodeKind k : Scene.LIST_KINDS) {
|
||||
Map<String, Object> d = (Map<String, Object>) nodes.get(k.prefix());
|
||||
if (d == null) {
|
||||
@@ -162,7 +166,7 @@ final class SceneDiff {
|
||||
for (Object id : (List<Object>) d.getOrDefault("removed", List.of())) {
|
||||
Node n = s.node((String) id);
|
||||
if (n == null) {
|
||||
warn.add("removed " + id + " does not exist in the base level");
|
||||
warn.add("The project removes " + aKind(k) + " that is not in the base level – ignored");
|
||||
} else {
|
||||
s.remove(n);
|
||||
}
|
||||
@@ -171,7 +175,7 @@ final class SceneDiff {
|
||||
Map<String, Object> m = (Map<String, Object>) o;
|
||||
String id = (String) m.get("id");
|
||||
if (s.node(id) != null) {
|
||||
warn.add("added node " + id + " already exists – skipped");
|
||||
nodeWarn.add(Map.entry(s.node(id), "the project adds " + aKind(k) + " in its place – skipped"));
|
||||
continue;
|
||||
}
|
||||
BrushKind bk = k == NodeKind.BRUSH ? BrushKind.valueOf((String) m.get("brushKind")) : null;
|
||||
@@ -183,7 +187,7 @@ final class SceneDiff {
|
||||
for (Map.Entry<String, Object> e : ch.entrySet()) {
|
||||
Node n = s.node(e.getKey());
|
||||
if (n == null) {
|
||||
warn.add("changed " + e.getKey() + " does not exist in the base level");
|
||||
warn.add("The project changes " + aKind(k) + " that is not in the base level – skipped");
|
||||
continue;
|
||||
}
|
||||
fill.add(Map.entry(n, (Map<String, Object>) e.getValue()));
|
||||
@@ -198,6 +202,12 @@ final class SceneDiff {
|
||||
for (Map.Entry<Node, Map<String, Object>> e : fill) {
|
||||
Node n = e.getKey();
|
||||
Set<String> known = new HashSet<>(List.of("id", "brushKind"));
|
||||
// array runs are relative to the base value: read them all before a setter clears one
|
||||
// (setting a model's vertices clears its header)
|
||||
Map<String, Object> before = new HashMap<>();
|
||||
for (Prop p : n.props()) {
|
||||
before.put(p.name(), p.get(n));
|
||||
}
|
||||
for (Prop p : n.props()) {
|
||||
known.add(p.name());
|
||||
if (!e.getValue().containsKey(p.name()) || !editable(p)) {
|
||||
@@ -211,17 +221,17 @@ final class SceneDiff {
|
||||
&& pm.containsKey("png")) {
|
||||
byte[] png = assets.get((String) pm.get("png"));
|
||||
if (png == null) {
|
||||
warn.add(n.id() + ": page image " + pm.get("png") + " missing");
|
||||
nodeWarn.add(Map.entry(n, "page image " + pm.get("png") + " missing"));
|
||||
continue;
|
||||
}
|
||||
v = Images.pagePixels(png, pg.width(), pg.height());
|
||||
} else {
|
||||
v = NodeCodec.decode(p, j, s, p.get(n));
|
||||
v = NodeCodec.decode(p, j, s, before.get(p.name()));
|
||||
}
|
||||
try {
|
||||
p.setRaw(n, v);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
warn.add(n.id() + "." + p.name() + ": " + ex.getMessage());
|
||||
nodeWarn.add(Map.entry(n, propLabel(p) + ": " + ex.getMessage()));
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, Object> f : e.getValue().entrySet()) {
|
||||
@@ -254,5 +264,20 @@ final class SceneDiff {
|
||||
for (var st : s.strats()) {
|
||||
st.setTypeName(s.catalog().nameOf(st.code()));
|
||||
}
|
||||
ReadableNames names = ReadableNames.of(s);
|
||||
for (Map.Entry<Node, String> w : nodeWarn) {
|
||||
Node n = w.getKey();
|
||||
warn.add((n.scene() == s ? names.nameOf(n) : ReadableNames.name(n)) + ": " + w.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/** "an object", "a tile model". */
|
||||
private static String aKind(NodeKind k) {
|
||||
String l = k.label().toLowerCase(Locale.ROOT);
|
||||
return ("aeiou".indexOf(l.charAt(0)) >= 0 ? "an " : "a ") + l;
|
||||
}
|
||||
|
||||
private static String propLabel(Prop p) {
|
||||
return p.label() == null || p.label().isEmpty() ? p.name() : p.label();
|
||||
}
|
||||
}
|
||||
+62
-7
@@ -52,6 +52,7 @@ final class MeshImportDialog extends JDialog {
|
||||
private final Preview preview = new Preview();
|
||||
private final JLabel info = new JLabel();
|
||||
private final JLabel warn = new JLabel();
|
||||
private final JLabel fitHint = GeoWidgets.hint(" ");
|
||||
private final JComboBox<String> up =
|
||||
new JComboBox<>(new String[] {"Y up (glTF, most OBJ)", "Z up (some CAD exports)"});
|
||||
private final JSpinner unit;
|
||||
@@ -100,6 +101,7 @@ final class MeshImportDialog extends JDialog {
|
||||
GeoWidgets.row(new JLabel("Up axis"), up),
|
||||
GeoWidgets.row(new JLabel("Unit scale (file units → cells)"), unit),
|
||||
GeoWidgets.row(new JLabel("Largest side"), fit, new JLabel("cells"), fitB),
|
||||
fitHint,
|
||||
GeoWidgets.row(new JLabel("Turn"), turn, mirror),
|
||||
GeoWidgets.row(ground)));
|
||||
form.add(GeoWidgets.section(
|
||||
@@ -196,22 +198,50 @@ final class MeshImportDialog extends JDialog {
|
||||
if (m == null) {
|
||||
return;
|
||||
}
|
||||
double[] b = m.bounds();
|
||||
double size = Math.max(b[3] - b[0], Math.max(b[4] - b[1], b[5] - b[2]));
|
||||
if (size <= 0) {
|
||||
return;
|
||||
double us = fitScale(((Number) unit.getValue()).doubleValue(), m.positions(), cells);
|
||||
if (us > 0) {
|
||||
unit.setValue(us);
|
||||
}
|
||||
double us = ((Number) unit.getValue()).doubleValue() * cells / size;
|
||||
unit.setValue(us);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unit scale that makes the largest side of the mesh's main mass ({@link Preview#focusBounds}) {@code
|
||||
* cells} long; 0 for an empty or flat-point mesh. {@code pos} is in cells at the current {@code unitScale}.
|
||||
*/
|
||||
static double fitScale(double unitScale, double[] pos, double cells) {
|
||||
double size = largestSide(Preview.focusBounds(pos));
|
||||
return size > 0 ? unitScale * cells / size : 0;
|
||||
}
|
||||
|
||||
private static double largestSide(double[] b) {
|
||||
return Math.max(b[3] - b[0], Math.max(b[4] - b[1], b[5] - b[2]));
|
||||
}
|
||||
|
||||
/** Hint shown under "Largest side" when far-off pieces are left out of Fit; empty otherwise. */
|
||||
static String fitHintText(MeshData m) {
|
||||
double[] f = Preview.focusBounds(m.positions());
|
||||
double[] b = m.bounds();
|
||||
if (largestSide(f) >= largestSide(b) - 1e-9) {
|
||||
return "";
|
||||
}
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"Fit ignores far-off stray pieces: main part %.2f × %.2f × %.2f cells.",
|
||||
f[3] - f[0],
|
||||
f[4] - f[1],
|
||||
f[5] - f[2]);
|
||||
}
|
||||
|
||||
private void update() {
|
||||
MeshData m = asset.mesh();
|
||||
if (m == null) {
|
||||
info.setText("(no mesh)");
|
||||
fitHint.setText(" ");
|
||||
return;
|
||||
}
|
||||
double[] b = m.bounds();
|
||||
String fh = fitHintText(m);
|
||||
fitHint.setText(fh.isEmpty() ? " " : fh);
|
||||
int textured = 0;
|
||||
for (MeshData.Material mt : m.materialList()) {
|
||||
if (mt.image() != null) {
|
||||
@@ -444,7 +474,8 @@ final class MeshImportDialog extends JDialog {
|
||||
|
||||
/**
|
||||
* Bounds {x0, y0, z0, x1, y1, z1} of the mesh's main mass: per axis the vertices within 2.5
|
||||
* interquartile ranges of the middle half (Tukey fences); the full bounds when they are not much
|
||||
* interquartile ranges of the middle half (Tukey fences), minus a smaller vertex group beyond a wide
|
||||
* gap (a stray triangle on a low-poly model); the full bounds when they are not much
|
||||
* larger (far-off pieces such as a stray triangle or a huge ground plane would otherwise shrink the
|
||||
* model to a dot).
|
||||
*/
|
||||
@@ -473,6 +504,30 @@ final class MeshImportDialog extends JDialog {
|
||||
while (v[hi] > q3 + fence) {
|
||||
hi--;
|
||||
}
|
||||
// low-poly models: a stray piece can hold a quarter of the vertices and pass the fences –
|
||||
// cut off the smaller side of a wide gap (fewer than half the vertices of the other side)
|
||||
for (int pass = 0; pass < 8 && hi > lo; pass++) {
|
||||
int at = lo;
|
||||
for (int i = lo; i < hi; i++) {
|
||||
if (v[i + 1] - v[i] > v[at + 1] - v[at]) {
|
||||
at = i;
|
||||
}
|
||||
}
|
||||
double gap = v[at + 1] - v[at];
|
||||
int left = at - lo + 1, right = hi - at;
|
||||
double keep = left >= right ? v[at] - v[lo] : v[hi] - v[at + 1];
|
||||
if (gap <= 0.5 * (v[hi] - v[lo])
|
||||
|| keep <= 0
|
||||
|| gap <= 2 * keep
|
||||
|| 2 * Math.min(left, right) > Math.max(left, right)) {
|
||||
break;
|
||||
}
|
||||
if (left >= right) {
|
||||
hi = at;
|
||||
} else {
|
||||
lo = at + 1;
|
||||
}
|
||||
}
|
||||
robust[axis] = v[lo];
|
||||
robust[axis + 3] = v[hi];
|
||||
far |= v[n - 1] - v[0] > 1.5 * (robust[axis + 3] - robust[axis]) + 1e-9;
|
||||
|
||||
Reference in new issue
Block a user