|
24 | 24 | from mhfu_studio.stage import collision, mesh, textures |
25 | 25 | from mhfu_studio.stage import ops as O |
26 | 26 |
|
27 | | -from .scene import CLIMB_MAX_UP, Array, Key, MapScene, Point |
| 27 | +from .scene import CLASS_NAMES, CLIMB_MATERIALS, Array, Key, MapScene, Point, group_name |
28 | 28 |
|
29 | 29 | GROUP, OBJECT, FACE, COLLISION = "group", "object", "face", "collision" |
30 | 30 | KINDS = (GROUP, OBJECT, FACE, COLLISION) |
|
34 | 34 | SHORT_WALL = 100.0 |
35 | 35 | """A climbable triangle shorter than this is worth a warning.""" |
36 | 36 | KEEPS = ("did-not-fit",) |
| 37 | +#: a collision chunk in words: 1 holds the floor, 0 the walls and ceilings |
| 38 | +CHUNK_WORDS = {0: "wall", 1: "floor"} |
37 | 39 | """Error codes a commit keeps (a partial pack still draws), shown as warnings.""" |
38 | 40 |
|
39 | 41 |
|
@@ -241,15 +243,16 @@ def refresh(self, scene: MapScene) -> Selection: |
241 | 243 | out.parts[k] = [int(f) for f in np.nonzero(np.isin(g.triangles, ids).all(1))[0]] |
242 | 244 | return out |
243 | 245 |
|
244 | | - def describe(self, scene: MapScene) -> str: |
| 246 | + def describe(self) -> str: |
| 247 | + """In words: "2 objects in group 8", "group 8", "3 triangles in 2 groups".""" |
245 | 248 | if self.empty: |
246 | 249 | return "nothing selected" |
247 | | - bits = [] |
248 | | - for k, ids in self.vertices.items(): |
249 | | - n = len(self.parts.get(k, [])) |
250 | | - what = {GROUP: "the whole group", OBJECT: f"{n} object(s)", FACE: f"{n} face(s)"} |
251 | | - bits.append(f"{scene.group(*k).label}: {what[self.kind]}, {len(ids)} vertices") |
252 | | - return "; ".join(bits) |
| 250 | + ks = list(self.vertices) |
| 251 | + where = group_name(ks[0]) if len(ks) == 1 else count(len(ks), "group") |
| 252 | + if self.kind == GROUP: |
| 253 | + return where |
| 254 | + n = sum(len(self.parts.get(k, [])) for k in ks) |
| 255 | + return f"{count(n, 'object' if self.kind == OBJECT else 'triangle')} in {where}" |
253 | 256 |
|
254 | 257 |
|
255 | 258 | @dataclass |
@@ -293,10 +296,26 @@ def centroid(self, scene: MapScene) -> Array: |
293 | 296 | return out |
294 | 297 |
|
295 | 298 | def describe(self, scene: MapScene) -> str: |
| 299 | + """In words, with the kinds: "3 collision triangles: 2 wall, 1 climbable".""" |
296 | 300 | if self.empty: |
297 | 301 | return "no collision triangle selected" |
298 | | - by = sorted(self.by_chunk().items()) |
299 | | - return "collision: " + ", ".join(f"chunk {c} x{len(t)}" for c, t in by) |
| 302 | + kinds: dict[str, int] = {} |
| 303 | + for c, t in self.tris: |
| 304 | + k = CLASS_NAMES[str(scene.chunk(c).klass[t])] |
| 305 | + kinds[k] = kinds.get(k, 0) + 1 |
| 306 | + what = ", ".join(f"{n} {k}" for k, n in kinds.items()) |
| 307 | + if len(kinds) == 1: |
| 308 | + what = next(iter(kinds)) |
| 309 | + return f"{count(len(self), 'collision triangle')}: {what}" |
| 310 | + |
| 311 | + |
| 312 | +def count(n: int, noun: str) -> str: |
| 313 | + return f"{n} {noun}{'' if n == 1 else 's'}" |
| 314 | + |
| 315 | + |
| 316 | +def triangle_name(chunk: int, tri: int) -> str: |
| 317 | + """`wall triangle 12`: a collision triangle as a person points at it.""" |
| 318 | + return f"{CHUNK_WORDS.get(chunk, f'chunk {chunk}')} triangle {tri}" |
300 | 319 |
|
301 | 320 |
|
302 | 321 | def by_chunk(tris: Iterable[Pair]) -> dict[int, list[int]]: |
@@ -340,55 +359,68 @@ def op_key(op: Mapping[str, Any]) -> Key | None: |
340 | 359 |
|
341 | 360 |
|
342 | 361 | def describe_op(op: Mapping[str, Any]) -> str: |
343 | | - """One line per op, in the user's terms.""" |
| 362 | + """One line per op, in plain words: "group 8: moved by 0, 100, 0".""" |
344 | 363 | k = str(op.get("op")) |
345 | 364 | key = op_key(op) |
346 | | - where = f"sub{key[0]}.g{key[1]}" if key else "collision" |
347 | | - if k == "transform": |
348 | | - by = [round(v) for v in op.get("by", (0, 0, 0))] |
349 | | - rot = [round(v) for v in op.get("rotate", (0, 0, 0))] |
350 | | - sc = [round(v, 2) for v in op.get("scale", (1, 1, 1))] |
351 | | - n = len(op.get("vertices", [])) |
352 | | - return f"{k:<9} {where:<9} {n:4d} v by {by} rot {rot} scale {sc}" |
353 | | - if k == "move": |
354 | | - return f"{k:<9} {where:<9} by {op.get('by')}" |
| 365 | + where = group_name(key) if key else "collision" |
| 366 | + if k in ("transform", "move"): |
| 367 | + by, rot = op.get("by", (0, 0, 0)), op.get("rotate", (0, 0, 0)) |
| 368 | + sc = op.get("scale", (1, 1, 1)) |
| 369 | + bits = [f"moved by {_xyz(by)}"] if any(round(v) for v in by) else [] |
| 370 | + if any(round(v) for v in rot): |
| 371 | + bits.append(f"turned {_xyz(rot)}\N{DEGREE SIGN}") |
| 372 | + if any(round(v, 2) != 1 for v in sc): |
| 373 | + bits.append(f"scaled \N{MULTIPLICATION SIGN}{_xyz(sc, 2)}") |
| 374 | + return f"{where}: {'; '.join(bits) or 'no change'}" |
355 | 375 | if k == "pack": |
356 | | - solid = " +collider" if op.get("solid") else "" |
357 | | - n = len(op.get("prims") or []) |
358 | | - return f"{k:<9} {where:<9} {Path(str(op.get('obj', '?'))).name} into {n} prims{solid}" |
| 376 | + name = Path(str(op.get("obj", "?"))).name |
| 377 | + return f"added {name} into {where}" + (", solid" if op.get("solid") else "") |
359 | 378 | if k == "clear": |
360 | | - what = f"{len(op['prims'])} prims" if op.get("prims") else "whole group" |
361 | | - return f"{k:<9} {where:<9} {what}" |
| 379 | + n = len(op.get("prims") or []) |
| 380 | + return f"removed from {where} ({count(n, 'drawing slot')})" if n else f"removed {where}" |
362 | 381 | if k == "material": |
363 | | - bits = [f"texture {op['texture']}"] if "texture" in op else [] |
| 382 | + bits = [f"texture slot {op['texture']}"] if "texture" in op else [] |
364 | 383 | if "rgba" in op: |
365 | | - bits.append(f"rgba {tuple(op['rgba'])}") |
366 | | - return f"{k:<9} {where:<9} {', '.join(bits) or '?'}" |
| 384 | + bits.append(f"colour {_xyz(op['rgba'])}") |
| 385 | + return f"{where}: {', '.join(bits) or 'material'}" |
367 | 386 | if k == "texture": |
368 | 387 | if "png" in op: |
369 | | - src = str(op["png"]) |
| 388 | + src = Path(str(op["png"])).name |
370 | 389 | elif "from" in op: |
371 | | - src = f"st{op['from']['stage']:03d} slot {op['from']['slot']}" |
| 390 | + src = f"slot {op['from']['slot']} of st{op['from']['stage']:03d}" |
372 | 391 | else: |
373 | | - src = f"rgb {tuple(op.get('rgb', ()))}" |
374 | | - return f"{k:<9} slot {op.get('slot')!s:<4} <- {src}" |
| 392 | + src = f"flat colour {_xyz(op.get('rgb', ()))}" |
| 393 | + return f"texture slot {op.get('slot')}: {src}" |
375 | 394 | if k == "collision": |
376 | 395 | if "solid_box" in op: |
377 | | - return "collision box collider" |
| 396 | + return "added a box collider" |
378 | 397 | if op.get("add"): |
379 | | - chunk = "auto" if op.get("chunk") is None else op["chunk"] |
380 | | - return f"collision chunk {chunk} +{len(op['add'])} triangle(s){_flags_text(op)}" |
| 398 | + return f"added {count(len(op['add']), 'collision triangle')}{_flags_text(op)}" |
381 | 399 | n = ("tri" in op) + len(op.get("tris", [])) |
382 | | - what = "delete" if op.get("delete") else ("move" if "verts" in op else "flags") |
383 | | - return f"collision chunk {op.get('chunk', 1)} {what} x{n}{_flags_text(op)}" |
384 | | - return f"{k:<9} {where}" |
| 400 | + tris = count(n, f"{CHUNK_WORDS.get(op.get('chunk', 1), 'collision')} triangle") |
| 401 | + if op.get("delete"): |
| 402 | + return f"deleted {tris}" |
| 403 | + if "verts" in op: |
| 404 | + return f"moved {tris}" |
| 405 | + return f"changed {tris}{_flags_text(op)}" |
| 406 | + return f"{k} {where}" |
| 407 | + |
| 408 | + |
| 409 | +def _xyz(v: Sequence[float], digits: int = 0) -> str: |
| 410 | + return ", ".join(f"{round(float(x), digits):g}" for x in v) |
385 | 411 |
|
386 | 412 |
|
387 | 413 | def _flags_text(op: Mapping[str, Any]) -> str: |
| 414 | + """The flags an op sets, climbing named.""" |
388 | 415 | fl = op.get("flags") or {} |
389 | 416 | if not isinstance(fl, Mapping) or not fl: |
390 | 417 | return "" |
391 | | - return " " + " ".join(f"{k}={v}" for k, v in fl.items()) |
| 418 | + if set(fl) == {"material"}: |
| 419 | + if fl["material"] in CLIMB_MATERIALS: |
| 420 | + return ": climbable" |
| 421 | + if fl["material"] == 0: |
| 422 | + return ": not climbable" |
| 423 | + return ": " + ", ".join(f"{k} {v}" for k, v in fl.items()) |
392 | 424 |
|
393 | 425 |
|
394 | 426 | class EditSession: |
@@ -817,17 +849,13 @@ def climb_warnings(self, tris: Iterable[Pair]) -> list[str]: |
817 | 849 | for c, t in tris: |
818 | 850 | ch = self.scene.chunk(c) |
819 | 851 | if not ch.vertical(t): |
820 | | - ny = abs(float(ch.normals[t][1])) |
821 | | - out.append( |
822 | | - f"chunk {c} tri {t} is not near-vertical (|n.y| = {ny:.2f} >=" |
823 | | - f" {CLIMB_MAX_UP:.2f}): the material will not make it climbable" |
824 | | - ) |
| 852 | + out.append(f"{triangle_name(c, t)} is too flat to climb") |
825 | 853 | h = float(ch.verts[t][:, 1].max() - ch.verts[t][:, 1].min()) |
826 | 854 | if h < SHORT_WALL: |
827 | | - out.append(f"chunk {c} tri {t} is only {h:.0f} units tall") |
| 855 | + out.append(f"{triangle_name(c, t)} is only {h:.0f} units tall: too short to climb") |
828 | 856 | mats.add(int(ch.material[t])) |
829 | 857 | if len(mats) > 1: |
830 | | - out.append(f"the patch mixes materials {sorted(mats)}") |
| 858 | + out.append(f"the selected triangles mix materials {sorted(mats)}") |
831 | 859 | return out |
832 | 860 |
|
833 | 861 | # checks |
|
0 commit comments