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
@@ -40,7 +40,7 @@ cells.
|
||||
|---|---|
|
||||
| **Up axis** | **Y up (glTF, most OBJ)** or **Z up (some CAD exports)** |
|
||||
| **Unit scale (file units → cells)** | how many cells one unit of the file is |
|
||||
| **Largest side** … **cells** + **Fit** | sets the unit scale so the largest side becomes this many cells (default 4) |
|
||||
| **Largest side** … **cells** + **Fit** | sets the unit scale so the largest side of the main part becomes this many cells (default 4); far-off stray pieces are left out, and a hint below says so |
|
||||
| **Turn** | 0°, 90°, 180° or 270° about the vertical axis |
|
||||
| **Mirror (left ↔ right)** | mirrors the model |
|
||||
|
||||
|
||||
@@ -55,10 +55,14 @@ deterministic; brushes use the stored `seed`).
|
||||
Node kinds, ids and properties: `notes/editor-api.md` (every persistent, settable `Prop`).
|
||||
Values by `PropType`: numbers (INT/FIXED/ANGLE/FLAGS/COLOR as integers, DOUBLE), booleans,
|
||||
strings, REF = node id (or `null`). Arrays: up to 64 elements as JSON lists, longer ones as
|
||||
`{"i32": base64}` (int32 LE), `{"f64": base64}` (float64 LE) or `{"bytes": base64}`. A *changed*
|
||||
`{"i32": base64}` (int32 LE), `{"f64": base64}` (float64 LE) or `{"bytes": base64}`; a short double
|
||||
array containing -0.0 is written as `f64` too (JSON numbers drop the sign; since 2026-11-07). A *changed*
|
||||
array of unchanged length is stored as runs of the changed elements only:
|
||||
`{"runs": [[start, values], …]}` (values encoded as above) – e.g. editing a few vertices or
|
||||
pixels of an original page stores just those values.
|
||||
pixels of an original page stores just those values. Runs apply to the reference value read before any
|
||||
property of the node is set (setting a model's vertices clears its header; until 2026-11-07 header runs
|
||||
after a vertex change failed with "project file is damaged: NullPointerException", e.g. on
|
||||
`llamamod --editor-import` of a levelbuild WAD).
|
||||
|
||||
Loading: reference scene → removals → create all added nodes (all kinds, so references across
|
||||
kinds resolve) → set values of added/changed nodes and settings → apply `order`. Problems
|
||||
|
||||
@@ -91,7 +91,10 @@ Invisible collision volumes (Invisible wall/block, colliders) are drawn transluc
|
||||
### Mesh import (File → Import mesh)
|
||||
File chooser, then a dialog: rotatable preview (mouse wheel zooms; framed on the main mass –
|
||||
`Preview.focusBounds`: per axis the vertices within 2.5 IQR of the middle half (Tukey fences), the full
|
||||
bounds unless they are over 1.5× larger; `geo_import_focus.jpg`: house with a triangle 400 cells away, `geo_import_zoomed_out.jpg`) (shown as in game; flat shading with a small z-buffer at
|
||||
bounds unless they are over 1.5× larger; since 2026-11-07 also minus the smaller vertex group beyond a gap
|
||||
wider than half the extent and twice the kept side, holding under half the other side's vertices – a stray triangle on a
|
||||
low-poly box is 3 of 11 vertices and passes the fences; **Fit** uses the same bounds, and a hint under
|
||||
"Largest side" names the main part's size when pieces are ignored, `geo_import_fit_stray.jpg`; `geo_import_focus.jpg`: house with a triangle 400 cells away, `geo_import_zoomed_out.jpg`) (shown as in game; flat shading with a small z-buffer at
|
||||
twice the panel size, `PreviewRaster` – before 2026-11-07 the triangles were sorted by centroid and hidden
|
||||
faces showed as slivers) with a 1-cell grid and a llama-sized marker, source
|
||||
info, up axis, unit scale and "fit largest side to n cells", turn, mirror, stand on the ground,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
+3
-1
@@ -138,7 +138,9 @@ stairs, platforms register as shadow casters (`solids`). 1.0 = texture colour
|
||||
rest on the llama): on village 2 bases (node 2 has unk2 ≠ 0) that put the camera into the llama's
|
||||
head. Waypoint lists take an optional 5th value unk2: `[(x,z,y,deg,unk2)]`.
|
||||
In game (test world, apptest 2026-11-07): the start camera is behind the llama at the default
|
||||
distance (`notes/img/editor/testworld_start_camera.jpg`).
|
||||
distance (`notes/img/editor/testworld_start_camera.jpg`). Village 2 base (levelbuild floor + player on
|
||||
t1l1m002, `--editor-import` as an edit project, boot-to-level 1/2): camera behind the llama, not in its
|
||||
head (`notes/img/editor/village2_levelbuild_start_camera.jpg`).
|
||||
* **LevelDoor** (exit) – solved, see section "Exit (LevelDoor)" below.
|
||||
* Sizes that work: TRAK 0.6–1.0 MB, up to 1100 models, 5200 tiles, WAD up to 6.9 MB
|
||||
(originals: TRAK ≤ 1 MB, ≤ 5800 tiles, WAD ≤ 6.85 MB). Test world: 628 models,
|
||||
|
||||
@@ -24,7 +24,9 @@ The editor's play-test (`PlaytestRunner.install`) should write the bound form
|
||||
**Start camera along** (`StartOverride.apply`): the llama's chain moves and turns with the start when it
|
||||
is a levelbuild/editor start camera – one node within 2.5 cells (`PlayerStart`, levelbuild since
|
||||
2026-11-07) or the older "node 8 behind + nodes on the llama" chain; original chains stay. Test:
|
||||
`BootToLevelModTest.singleCameraNodeMovesWithTheStart`.
|
||||
`BootToLevelModTest.singleCameraNodeMovesWithTheStart`. In game (apptest 2026-11-07, test world,
|
||||
`start=21, 0.1, 15, 90`, i.e. moved and turned from the plaza start 18/14.6 yaw 0): camera behind the llama,
|
||||
facing the stairs (`img/editor/bootstart_testworld_camera.jpg`).
|
||||
|
||||
Patches: `jmp` at 0x549C50 (main-menu frame handler) → once: read slots (FUN_0041c0e0), load
|
||||
slot (FUN_0041cc70), set tribe/world/chapter/map 0x584EB4…, music shutdown, state 0x5846E4 = 3,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -150,4 +150,23 @@ class ProjectRoundTripTest {
|
||||
assertEquals(0, q.scene().tiles().size());
|
||||
assertTrue(q.scene().listOf(NodeKind.STRAT).isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* An imported levelbuild WAD as an edit project (llamamod --editor-import): model headers change by
|
||||
* runs while the vertices change too – the vertex setter clears the header, the runs still apply.
|
||||
*/
|
||||
@Test
|
||||
void importedLevelbuildWadLoadsBack() throws IOException {
|
||||
Bases cd = cd();
|
||||
com.eliasmueller.llamamod.levelbuild.LevelBuilder lb = new com.eliasmueller.llamamod.levelbuild.LevelBuilder(
|
||||
cd.wad("t1l1m002"), rel -> null, "Start", 1, Map.of());
|
||||
lb.floor(6, 6, 30, 30, 1.0, "grass");
|
||||
lb.player(16, 12, 0);
|
||||
Project p = new Project(
|
||||
Templates.original(cd, "t1l1m002"), WadImporter.importWad(lb.build(), "t1l1m002", cd::fileOrNull));
|
||||
byte[] wad = WadBuilder.build(p.scene());
|
||||
Project q = ProjectIO.read(ProjectIO.write(p, cd), cd);
|
||||
assertTrue(q.warnings().isEmpty(), q.warnings().toString());
|
||||
assertArrayEquals(wad, WadBuilder.build(q.scene()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.eliasmueller.llamamod.editor.io;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
import com.eliasmueller.llamamod.editor.model.NodeKind;
|
||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SceneDiffWarningsTest {
|
||||
@Test
|
||||
void loadWarningsUseReadableNamesNotIds() {
|
||||
Scene s = new Scene();
|
||||
s.add(Scene.createWithId(NodeKind.LIGHT, "light:1", null), -1);
|
||||
s.add(Scene.createWithId(NodeKind.LIGHT, "light:2", null), -1);
|
||||
Map<String, Object> json = Map.of(
|
||||
"nodes",
|
||||
Map.of(
|
||||
"light",
|
||||
Map.of(
|
||||
"removed", List.of("light:99"),
|
||||
"added", List.of(Map.of("id", "light:2")),
|
||||
"changed", Map.of("light:77", Map.of()))));
|
||||
List<String> warn = new ArrayList<>();
|
||||
SceneDiff.apply(s, json, Map.of(), new SceneDiff.Unknown(), warn);
|
||||
assertEquals(
|
||||
List.of(
|
||||
"The project removes a light that is not in the base level – ignored",
|
||||
"The project changes a light that is not in the base level – skipped",
|
||||
"Light 2: the project adds a light in its place – skipped"),
|
||||
warn);
|
||||
for (String w : warn) {
|
||||
assertFalse(w.contains("light:"), w);
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -53,4 +53,47 @@ class PaletteAndPreviewTest {
|
||||
double[] g = MeshImportDialog.Preview.focusBounds(small);
|
||||
assertArrayEquals(new double[] {0, 0, 0, 2, 1, 2}, g, 1e-9);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fitUsesTheMainMassAndHintsAtStrayPieces() {
|
||||
// a 1-cell cube of 200 vertices plus one stray vertex 500 cells away
|
||||
double[] pos = new double[3 * 201];
|
||||
for (int i = 0; i < 200; i++) {
|
||||
pos[3 * i] = (i % 2);
|
||||
pos[3 * i + 1] = (i / 2 % 2);
|
||||
pos[3 * i + 2] = (i / 4 % 2);
|
||||
}
|
||||
pos[600] = 500;
|
||||
// largest side of the main part is 1 cell: Fit to 4 cells scales by 4, not by 4/500
|
||||
assertEquals(4 * 0.5, MeshImportDialog.fitScale(0.5, pos, 4), 1e-9);
|
||||
int[] tris = new int[] {0, 1, 2};
|
||||
var stray = new com.eliasmueller.llamamod.editor.model.MeshData(
|
||||
pos, tris, new double[0], new int[1], new int[0], java.util.List.of());
|
||||
assertTrue(MeshImportDialog.fitHintText(stray).startsWith("Fit ignores far-off stray pieces"));
|
||||
double[] cube = java.util.Arrays.copyOf(pos, 600);
|
||||
assertEquals(2, MeshImportDialog.fitScale(1, cube, 2), 1e-9);
|
||||
var clean = new com.eliasmueller.llamamod.editor.model.MeshData(
|
||||
cube, tris, new double[0], new int[1], new int[0], java.util.List.of());
|
||||
assertEquals("", MeshImportDialog.fitHintText(clean));
|
||||
assertEquals(0, MeshImportDialog.fitScale(1, new double[0], 2));
|
||||
// low-poly: a 2×1×3 box (8 vertices) and a stray triangle 400 cells away (3 of 11 vertices)
|
||||
double[] low = new double[3 * 11];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
low[3 * i] = i % 2 * 2;
|
||||
low[3 * i + 1] = i / 2 % 2;
|
||||
low[3 * i + 2] = i / 4 * 3;
|
||||
}
|
||||
low[24] = 400;
|
||||
low[27] = 401;
|
||||
low[30] = 400;
|
||||
low[31] = 1;
|
||||
assertArrayEquals(new double[] {0, 0, 0, 2, 1, 3}, MeshImportDialog.Preview.focusBounds(low), 1e-9);
|
||||
assertEquals(4.0 / 3, MeshImportDialog.fitScale(1, low, 4), 1e-9);
|
||||
// a wedge (6 vertices on x = 0, 2 on x = 1) is not cut
|
||||
double[] wedge = {0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 2, 0, 0, 2, 1, 1, 0, 0, 1, 0, 1};
|
||||
assertArrayEquals(new double[] {0, 0, 0, 1, 2, 1}, MeshImportDialog.Preview.focusBounds(wedge), 1e-9);
|
||||
// two equal towers far apart stay together
|
||||
double[] towers = {0, 0, 0, 1, 5, 1, 0, 5, 0, 1, 0, 1, 20, 0, 0, 21, 5, 1, 20, 5, 0, 21, 0, 1};
|
||||
assertArrayEquals(new double[] {0, 0, 0, 21, 5, 1}, MeshImportDialog.Preview.focusBounds(towers), 1e-9);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user