The 0.1.2 blockout regeneration ran without --origin, so Blosm centred on the extract instead of fitting the project frame. The frontage shifted ~119 m in X and ~2.4 m in Y and the hero fell outside the 6902/6838 gap - the verified placement was silently undone, while every render still looked like a plausible block. - Blockout regenerated with --origin 34.101545,-118.338718 (affine residual 0.0 cm); frontage and north buildings re-exported and re-imported. - Verified spans restored: 6922 -24276..-17700, 6902 -17749..-14591, hero -14644..-12028 between them, theatre -12082..-8210, TCL -22889..-18442, Tussaud's -27071..-22799. - ue_apply_facade_kit now uses get_actor_bounds() (world space). It had used the static mesh bounding box, which is building-line-local, so every kit piece was placed ~12 km east of its building in empty ground. - ue_import_north_buildings.py was missing 'import sys' (NameError, exit 255, import silently skipped) - the same trap as the hero importer. - Kit re-applied: 625 pieces across eight buildings; packaged QA still PASS (walk, transect 0 holes, wall probe 0/35, facade probe 0/9).
216 lines
8.2 KiB
Python
216 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Assemble the façade kit onto the block buildings.
|
|
|
|
Takes the imported kit from `/Game/Hollywood/Kit` and lays it out over a frontage
|
|
building's street face: shopfront bays on the ground floor, window bays above,
|
|
pilasters on the bay boundaries, a cornice under the parapet and a coping over it.
|
|
|
|
Layout is driven by the building's own bounding box, so it follows whatever the
|
|
blockout gives: the bay count is `round(width / BAY)` and the storey count comes
|
|
from the height above the ground floor. Nothing is hand-placed per building.
|
|
|
|
UnrealEditor-Cmd <uproject> -run=pythonscript \
|
|
-script=Tools/ue_apply_facade_kit.py -nullrhi -unattended -nopause -stdout
|
|
|
|
# or, as a step inside the demo builder:
|
|
from ue_apply_facade_kit import apply_to_actor
|
|
|
|
Result file: /tmp/gigline/kit-apply.txt
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
import unreal
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import ue_common # noqa: E402
|
|
|
|
KIT = "/Game/Hollywood/Kit"
|
|
RESULT = "/tmp/gigline/kit-apply.txt"
|
|
|
|
BAY_CM = 496.0 # the kit module
|
|
STOREY_CM = 310.0
|
|
SHOP_FLOOR_CM = 310.0 # ground floor is one storey tall
|
|
KIT_PREFIX = "Kit_"
|
|
|
|
lines: list[str] = []
|
|
|
|
|
|
def log(message: str) -> None:
|
|
lines.append(message)
|
|
print(message)
|
|
|
|
|
|
def load_kit() -> dict:
|
|
"""Load every kit mesh once, keyed by its short name."""
|
|
kit = {}
|
|
registry = unreal.AssetRegistryHelpers.get_asset_registry()
|
|
filt = unreal.ARFilter(package_paths=[KIT], recursive_paths=False,
|
|
class_paths=[unreal.TopLevelAssetPath("/Script/Engine", "StaticMesh")])
|
|
for data in registry.get_assets(filt):
|
|
mesh = unreal.EditorAssetLibrary.load_asset(str(data.package_name))
|
|
if mesh:
|
|
kit[mesh.get_name()] = mesh
|
|
return kit
|
|
|
|
|
|
def _bounds(mesh):
|
|
box = mesh.get_bounding_box()
|
|
return box.min, box.max
|
|
|
|
|
|
def _spawn(mesh, location, label, material=None, parent_collection=None):
|
|
actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
|
|
unreal.StaticMeshActor, location)
|
|
if actor is None:
|
|
log(f"could not spawn {label}")
|
|
return None
|
|
actor.set_actor_label(label)
|
|
comp = actor.static_mesh_component
|
|
comp.set_static_mesh(mesh)
|
|
comp.set_mobility(unreal.ComponentMobility.STATIC)
|
|
comp.set_collision_enabled(unreal.CollisionEnabled.QUERY_AND_PHYSICS)
|
|
comp.set_collision_profile_name("BlockAll")
|
|
if material:
|
|
comp.set_material(0, material)
|
|
return actor
|
|
|
|
|
|
def apply_to_actor(actor, kit, materials=None, prefix=None):
|
|
"""Lay the kit out over one building actor's street face.
|
|
|
|
Returns the number of pieces placed. The building's own mesh stays in place
|
|
as the wall the pieces sit on.
|
|
"""
|
|
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
|
if not comp or not comp.static_mesh:
|
|
return 0
|
|
# Labels must be unique per building, otherwise two buildings' pieces share a
|
|
# label and the outliner (and any label-based lookup) becomes ambiguous.
|
|
slug = actor.get_actor_label().replace("Frontage_SM_", "").replace("North_", "").lower()
|
|
prefix = prefix or f"Kit_{slug}_"
|
|
# WORLD-space bounds. The imported meshes carry a building-line-local offset in
|
|
# their own vertices and the demo builder then positions the actor, so the
|
|
# asset's bounding box is NOT world space: using it put every kit piece about
|
|
# 12 km east of its building, floating in empty ground where nothing could see
|
|
# it. get_actor_bounds() includes the actor transform.
|
|
origin_w, extent_w = actor.get_actor_bounds(False)
|
|
mn = unreal.Vector(origin_w.x - extent_w.x, origin_w.y - extent_w.y, origin_w.z - extent_w.z)
|
|
mx = unreal.Vector(origin_w.x + extent_w.x, origin_w.y + extent_w.y, origin_w.z + extent_w.z)
|
|
origin = unreal.Vector(0.0, 0.0, 0.0)
|
|
width = mx.x - mn.x
|
|
height = mx.z - mn.z
|
|
if width < BAY_CM * 0.75 or height < STOREY_CM:
|
|
log(f"{actor.get_actor_label()}: too small for the kit "
|
|
f"({width / 100:.1f} x {height / 100:.1f} m)")
|
|
return 0
|
|
|
|
bays = max(1, int(round(width / BAY_CM)))
|
|
bay_w = width / bays # actual bay width for this building
|
|
# Street face: the frontage runs along X, and the street is at smaller Y.
|
|
face_y = mn.y
|
|
base_z = mn.z
|
|
storeys = max(1, int((height - SHOP_FLOOR_CM) // STOREY_CM))
|
|
|
|
def kit_material(name):
|
|
return (materials or {}).get(name)
|
|
|
|
placed = 0
|
|
for bay in range(bays):
|
|
cx = mn.x + bay_w * (bay + 0.5)
|
|
# ground floor: shopfront
|
|
for piece, dx in (("Kit_ShopPier_L", -bay_w / 2 + 30.0),
|
|
("Kit_ShopPier_R", bay_w / 2 - 30.0)):
|
|
if piece in kit:
|
|
if _spawn(kit[piece], unreal.Vector(cx + dx, face_y, base_z),
|
|
f"{prefix}shop_pier_{bay}", kit_material(piece)):
|
|
placed += 1
|
|
if "Kit_ShopGlass" in kit:
|
|
if _spawn(kit["Kit_ShopGlass"], unreal.Vector(cx, face_y, base_z),
|
|
f"{prefix}shop_glass_{bay}", kit_material("Kit_ShopGlass")):
|
|
placed += 1
|
|
# upper storeys: window bays
|
|
for storey in range(storeys):
|
|
if "Kit_WindowBay" in kit:
|
|
z = base_z + SHOP_FLOOR_CM + STOREY_CM * storey + 65.0
|
|
if _spawn(kit["Kit_WindowBay"], unreal.Vector(cx, face_y, z),
|
|
f"{prefix}window_{bay}_{storey}", kit_material("Kit_WindowBay")):
|
|
placed += 1
|
|
# pilaster on the left boundary of every bay
|
|
if "Kit_Pilaster" in kit:
|
|
if _spawn(kit["Kit_Pilaster"],
|
|
unreal.Vector(mn.x + bay_w * bay, face_y, base_z),
|
|
f"{prefix}pilaster_{bay}", kit_material("Kit_Pilaster")):
|
|
placed += 1
|
|
|
|
# cornice under the parapet, coping on top
|
|
if "Kit_Cornice" in kit:
|
|
for bay in range(bays):
|
|
cx = mn.x + bay_w * (bay + 0.5)
|
|
if _spawn(kit["Kit_Cornice"],
|
|
unreal.Vector(cx, face_y, base_z + height - 145.0),
|
|
f"{prefix}cornice_{bay}", kit_material("Kit_Cornice")):
|
|
placed += 1
|
|
if "Kit_Parapet" in kit:
|
|
for bay in range(bays):
|
|
cx = mn.x + bay_w * (bay + 0.5)
|
|
if _spawn(kit["Kit_Parapet"],
|
|
unreal.Vector(cx, face_y, base_z + height - 90.0),
|
|
f"{prefix}parapet_{bay}", kit_material("Kit_Parapet")):
|
|
placed += 1
|
|
|
|
log(f"{actor.get_actor_label()}: {bays} bays x {storeys} storeys, "
|
|
f"{width / 100:.1f} x {height / 100:.1f} m -> {placed} pieces")
|
|
return placed
|
|
|
|
|
|
def main() -> None:
|
|
kit = load_kit()
|
|
if not kit:
|
|
log(f"no kit meshes under {KIT} — run Tools/ue_import_facade_kit.py first")
|
|
return
|
|
log(f"kit meshes: {len(kit)}")
|
|
|
|
level = "/Game/Hollywood/DemoMap"
|
|
if not unreal.EditorLevelLibrary.load_level(level):
|
|
log(f"could not load {level}")
|
|
return
|
|
# The map is LFS-tracked and checks out read-only.
|
|
content = unreal.Paths.project_content_dir()
|
|
ue_common.make_writable(os.path.join(content, "Hollywood/DemoMap.umap"))
|
|
|
|
# Remove a previous application so re-running does not stack duplicates.
|
|
removed = 0
|
|
for actor in list(unreal.EditorLevelLibrary.get_all_level_actors()):
|
|
if actor.get_actor_label().startswith("Kit_"):
|
|
unreal.EditorLevelLibrary.destroy_actor(actor)
|
|
removed += 1
|
|
log(f"cleared {removed} previous kit actors")
|
|
|
|
targets = [a for a in unreal.EditorLevelLibrary.get_all_level_actors()
|
|
if a.get_actor_label().startswith(("Frontage_", "North_"))]
|
|
log(f"frontage actors: {len(targets)}")
|
|
total = 0
|
|
for actor in targets:
|
|
total += apply_to_actor(actor, kit)
|
|
log(f"placed {total} kit pieces")
|
|
|
|
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
|
unreal.EditorLevelLibrary.get_editor_world(), level)
|
|
log(f"save_map {level}: {saved}")
|
|
|
|
|
|
try:
|
|
main()
|
|
except Exception as exc: # noqa: BLE001
|
|
import traceback
|
|
log(f"FAILED: {type(exc).__name__}: {exc}")
|
|
log(traceback.format_exc())
|
|
|
|
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
|
with open(RESULT, "w", encoding="utf-8") as handle:
|
|
handle.write("\n".join(lines) + "\n")
|