""" The ice coolers' model, made ready from the user's Fab library (2026-10-14, the user: "In order to maintain the ice there are 4 big machines spread over on the Ice to cool it so ice never melts use the asset from fab library called "star citizen workflow to sketchfab - decals 6" without the human"). blender -b --factory-startup --python build/fab_cooler_prepare.py -- Source: Assets/VaultCache/FabLibrary/Star_Citizen_Workflow_to_Sketchfab_-_Decals_6-b17f1be3/blender/ star-citizen-workflow-to_extracted/source/p_128_scifiEngine_005.blend (Gerardo Justel, on Fab; listing b17f1be3-6b5b-4e16-ae98-80066265e669). The source file is not changed; the result is a new .blend beside the station: * left out: the two figures and their glowing eyes (the "human"), the showroom's floor plane, the camera, and helper objects nothing refers to * left out: exact copies of a part lying on the part itself (the source has its engine twice, face for face) * left out since 2026-10-21: the camouflage cloth over the engine (the user: "remove that camo textile on top of the generators") * everything else as the author made it, modifiers and all: the platform, the engine, the decals * one collection, ICE_COOLER_COLLECTION, that the station instances four times * the packed textures written out as files beside it (textures/), one file per image * names get the prefix "COOLER " so they cannot clash with the station's """ import os import re import sys import bpy from mathutils import Vector ARGS = sys.argv[sys.argv.index("--") + 1:] OUT = os.path.abspath(ARGS[0]) TEX = os.path.join(os.path.dirname(OUT), "textures") COLLECTION = "ICE COOLER | scifiEngine_005 (Fab: Star Citizen Workflow to Sketchfab - Decals 6)" PREFIX = "COOLER " os.makedirs(TEX, exist_ok=True) bpy.data.use_autopack = False # the source packs on save, from paths on its author's disk dg = bpy.context.evaluated_depsgraph_get() was_visible = {o.name: o.visible_get() for o in bpy.data.objects} human = set(bpy.data.collections["characters"].objects) if "characters" in bpy.data.collections else set() human |= {o for o in bpy.data.objects if o.name.startswith("character_")} showroom = {o for o in bpy.data.objects if o.type == "MESH" and o.data.materials and o.data.materials[0] is not None and o.data.materials[0].name == "ground"} cloth = {o for o in bpy.data.objects if o.type == "MESH" and (o.name.startswith("Cloth") or any( m is not None and m.name.startswith("ClothCamo") for m in o.data.materials))} drop = set(human) | showroom | cloth | {o for o in bpy.data.objects if o.type in ("CAMERA", "LIGHT")} human_names = sorted(o.name for o in human) dropped_names = [] def refs(ob): """Objects that `ob` needs: its parent, and what its modifiers and constraints point at.""" out = set() if ob.parent is not None: out.add(ob.parent) for m in list(ob.modifiers) + list(ob.constraints): for p in m.bl_rna.properties: if p.type == "POINTER": v = getattr(m, p.identifier, None) if isinstance(v, bpy.types.Object): out.add(v) if getattr(m, "type", "") == "NODES": try: for k in m.keys(): v = m[k] if isinstance(v, bpy.types.Object): out.add(v) except TypeError: # Blender 5: the inputs are not ID properties any more gn = m.node_group for item in (gn.interface.items_tree if gn is not None else []): v = getattr(item, "default_value", None) if isinstance(v, bpy.types.Object): out.add(v) return out GN_USED = sorted({m.node_group.name for o in bpy.data.objects for m in o.modifiers if m.type == "NODES" and m.node_group is not None}) print("[cooler] geometry node groups in use: %s" % ", ".join(GN_USED)) # what is visible and kept, and what those need keep = {o for o in bpy.data.objects if o not in drop and (o.visible_get() or o.type != "MESH")} keep = {o for o in keep if o.type == "MESH" and o.visible_get()} need = set() todo = list(keep) while todo: o = todo.pop() for r in refs(o): if r not in keep and r not in need: need.add(r) todo.append(r) for o in need: if o in drop: print("[cooler] NOTE: %s is needed by a kept part but was to be left out; kept" % o.name) if not was_visible[o.name]: o.hide_render = o.hide_viewport = True # a helper the author kept out of sight: it stays out of sight print("[cooler] helpers kept because parts refer to them: %s" % ", ".join( "%s (%s%s)" % (o.name, o.type.lower(), "" if was_visible[o.name] else ", hidden") for o in sorted(need, key=lambda o: o.name))) keep |= need # exact copies: two parts with the same faces in the same place (evaluated, in world space) def shape(o): ev = o.evaluated_get(dg) me = ev.to_mesh() mw = o.matrix_world key = (len(me.vertices), len(me.polygons), tuple(sorted(tuple(round(c, 4) for c in (mw @ v.co)) for v in me.vertices)), tuple(sorted(tuple(p.vertices) for p in me.polygons)), tuple(m.name if m else "" for m in o.data.materials)) ev.to_mesh_clear() return key seen, copies = {}, [] for o in sorted([o for o in keep if o.type == "MESH" and o not in need], key=lambda o: (len(o.name), o.name)): k = shape(o) if k in seen: copies.append((o, seen[k])) else: seen[k] = o for o, first in copies: if any(o in refs(other) for other in keep if other is not o): print("[cooler] %s is a copy of %s but other parts refer to it: kept" % (o.name, first.name)) continue keep.discard(o) drop.add(o) print("[cooler] exact copies left out: %s" % ", ".join("%s (= %s)" % (o.name, f.name) for o, f in copies if o not in keep)) # the result's collection col = bpy.data.collections.new(COLLECTION) bpy.context.scene.collection.children.link(col) for o in keep: for c in list(o.users_collection): c.objects.unlink(o) col.objects.link(o) for o in list(bpy.data.objects): if o not in keep: dropped_names.append(o.name) bpy.data.objects.remove(o) print("[cooler] left out: %s" % ", ".join(sorted(dropped_names))) for c in list(bpy.data.collections): if c is not col and not c.objects and not c.children: bpy.data.collections.remove(c) col["fab_title"] = "Star Citizen Workflow to Sketchfab - Decals 6" col["fab_seller"] = "Gerardo Justel" col["fab_uid"] = "b17f1be3-6b5b-4e16-ae98-80066265e669" col["fab_source"] = "Assets/VaultCache/FabLibrary/Star_Citizen_Workflow_to_Sketchfab_-_Decals_6-b17f1be3/blender/star-citizen-workflow-to_extracted/source/p_128_scifiEngine_005.blend" col["anchorage_note"] = "the ice coolers' model: the Fab asset without its figures, showroom floor, camera and doubled engine" for o in keep: o.name = PREFIX + o.name for m in bpy.data.materials: if m.users and not m.name.startswith(PREFIX): m.name = PREFIX + m.name # the textures, one file each written = 0 for im in bpy.data.images: if im.packed_file is None: continue data = bytes(im.packed_file.data) ext = ".png" if data[:8] == b"\x89PNG\r\n\x1a\n" else (".jpg" if data[:3] == b"\xff\xd8\xff" else ".bin") base = re.sub(r"[^A-Za-z0-9._+-]+", "_", im.name.strip(".")).strip("_") or "image" fname = base + ("" if base.lower().endswith(ext) else ext) path = os.path.join(TEX, fname) with open(path, "wb") as fh: fh.write(data) im.filepath = path # absolute here; the save makes it relative to the new file try: im.unpack(method="REMOVE") except Exception: # noqa: BLE001 im.unpack(method="USE_ORIGINAL") im.name = PREFIX + im.name.lstrip(".") written += 1 # what is left tris = 0 lo, hi = Vector((1e9,) * 3), Vector((-1e9,) * 3) for o in col.objects: if o.type != "MESH": continue ev = o.evaluated_get(dg) me = ev.to_mesh() tris += sum(len(p.vertices) - 2 for p in me.polygons) for v in me.vertices: p = o.matrix_world @ v.co lo = Vector((min(lo.x, p.x), min(lo.y, p.y), min(lo.z, p.z))) hi = Vector((max(hi.x, p.x), max(hi.y, p.y), max(hi.z, p.z))) ev.to_mesh_clear() bpy.ops.wm.save_as_mainfile(filepath=OUT, relative_remap=True, compress=True) print("[cooler] kept %d objects (%d meshes, %d helpers), left out %d (figures: %s); %d textures written; %d triangles; " "box (%.2f %.2f %.2f)..(%.2f %.2f %.2f); saved %s" % ( len(col.objects), sum(1 for o in col.objects if o.type == "MESH"), sum(1 for o in col.objects if o.type != "MESH"), len(dropped_names), ", ".join(human_names) or "-", written, tris, lo.x, lo.y, lo.z, hi.x, hi.y, hi.z, OUT))