The bind called bpy.path.relpath after setting aetheria.asset_kind and aetheria.id, so a .cc on another drive than the .blend raised and left the collection half-bound, and an unsaved .blend stored a path relative to the working directory. The stored path is now computed first: absolute for an unsaved .blend, '//'-relative when relpath succeeds, absolute when it raises ValueError. Every reader already resolves both through bpy.path.abspath. smoke_package.py gains the bind cases: unsaved stores absolute and survives two Save As moves, a failing bind leaves no aetheria.* property, same-drive stores '//'-relative, and with SMOKE_OTHER_DRIVE the final bind crosses drives before the package passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
502 lines
22 KiB
Python
502 lines
22 KiB
Python
"""Aetheria ship authoring controls in Brokkr's Blender sidebar."""
|
|
|
|
bl_info = {
|
|
"name": "Aetheria Ships for Brokkr",
|
|
"author": "GameCult",
|
|
"version": (0, 1, 0),
|
|
"blender": (4, 3, 0),
|
|
"location": "View3D > Sidebar > Brokkr > Aetheria Ship",
|
|
"description": "Author a typed Aetheria ship package: layout, lines, anchors and its GLB",
|
|
"category": "Object",
|
|
}
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
from .ship_cc import (HARDPOINT_TYPE_NAMES, ROTATION_NAMES, capture_grease_pencil, decode_hardpoint,
|
|
encode_hardpoint, read, read_layout, replace_layout, replace_lines, replace_visual)
|
|
|
|
MODEL_ASSET = "ship.glb"
|
|
AETHERDB_TIMEOUT = 600 # seconds; the first run builds AetherDb
|
|
|
|
|
|
HARDPOINT_TYPES = tuple((str(i), name, name) for i, name in enumerate(HARDPOINT_TYPE_NAMES))
|
|
ROTATIONS = tuple((str(i), name, name) for i, name in enumerate(ROTATION_NAMES))
|
|
|
|
|
|
class AETHERIA_PG_cell(bpy.types.PropertyGroup):
|
|
occupied: bpy.props.BoolProperty(name="Occupied")
|
|
|
|
|
|
class AETHERIA_PG_hardpoint(bpy.types.PropertyGroup):
|
|
mount_id: bpy.props.StringProperty(name="Mount ID")
|
|
kind: bpy.props.EnumProperty(name="Type", items=HARDPOINT_TYPES)
|
|
x: bpy.props.IntProperty(name="X", min=0, max=31)
|
|
y: bpy.props.IntProperty(name="Y", min=0, max=31)
|
|
footprint: bpy.props.StringProperty(
|
|
name="Footprint", default="#", description="Top-to-bottom rows of # and ., separated by /"
|
|
)
|
|
rotation: bpy.props.EnumProperty(name="Rotation", items=ROTATIONS)
|
|
armor: bpy.props.FloatProperty(name="Armor", min=0)
|
|
firing_arc: bpy.props.FloatProperty(name="Firing Arc", min=0, max=360)
|
|
|
|
|
|
class AETHERIA_PG_layout(bpy.types.PropertyGroup):
|
|
ship_id: bpy.props.StringProperty()
|
|
ship_cc: bpy.props.StringProperty()
|
|
revision: bpy.props.StringProperty()
|
|
width: bpy.props.IntProperty(default=1)
|
|
height: bpy.props.IntProperty(default=1)
|
|
new_width: bpy.props.IntProperty(name="Width", default=1, min=1, max=32)
|
|
new_height: bpy.props.IntProperty(name="Height", default=1, min=1, max=32)
|
|
cells: bpy.props.CollectionProperty(type=AETHERIA_PG_cell)
|
|
hardpoints: bpy.props.CollectionProperty(type=AETHERIA_PG_hardpoint)
|
|
|
|
|
|
def _footprint(raw):
|
|
width, height, cells = raw[0]
|
|
return "/".join("".join("#" if cells[x * height + y] else "." for x in range(width))
|
|
for y in range(height - 1, -1, -1))
|
|
|
|
|
|
def _parse_footprint(text):
|
|
rows = text.split("/")
|
|
width = len(rows[0])
|
|
height = len(rows)
|
|
if not (1 <= width <= 32 and 1 <= height <= 32 and
|
|
all(len(row) == width and set(row) <= {"#", "."} for row in rows)):
|
|
raise ValueError("Footprint must use equal-width #/. rows separated by /")
|
|
return [[width, height, [rows[height - 1 - y][x] == "#"
|
|
for x in range(width) for y in range(height)]]]
|
|
|
|
|
|
def _layout_path(context):
|
|
collection = _ship_collection(context)
|
|
if not collection.get("aetheria.ship_cc"):
|
|
raise ValueError("Ship collection has no bound .cc path")
|
|
return collection, bpy.path.abspath(collection["aetheria.ship_cc"])
|
|
|
|
|
|
class AETHERIA_OT_load_layout(bpy.types.Operator):
|
|
bl_idname = "aetheria.load_ship_layout"
|
|
bl_label = "Load Ship Layout"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
try:
|
|
collection, path = _layout_path(context)
|
|
shape, hardpoints, revision = read_layout(path, _brokkr_cultlib(context))
|
|
width, height, cells = shape
|
|
state = context.scene.aetheria_layout
|
|
state.ship_id = collection["aetheria.id"]
|
|
state.ship_cc = path
|
|
state.revision = revision
|
|
state.width = state.new_width = width
|
|
state.height = state.new_height = height
|
|
state.cells.clear()
|
|
for cell in cells:
|
|
state.cells.add().occupied = bool(cell)
|
|
state.hardpoints.clear()
|
|
for raw in hardpoints:
|
|
fields = decode_hardpoint(raw)
|
|
hp = state.hardpoints.add()
|
|
hp.kind = str(fields["Type"])
|
|
hp.x, hp.y = fields["Position"]
|
|
hp.footprint = _footprint(fields["Shape"])
|
|
hp.mount_id = fields["Transform"] or ""
|
|
hp.rotation = str(fields["Rotation"])
|
|
hp.armor = fields["Armor"]
|
|
hp.firing_arc = fields["FiringArc"]
|
|
self.report({"INFO"}, f"Loaded {width}x{height} layout and {len(hardpoints)} hardpoints")
|
|
return {"FINISHED"}
|
|
except (OSError, ValueError, RuntimeError, ImportError, KeyError) as exc:
|
|
self.report({"ERROR"}, str(exc))
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class AETHERIA_OT_resize_layout(bpy.types.Operator):
|
|
bl_idname = "aetheria.resize_ship_layout"
|
|
bl_label = "Resize Grid"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
state = context.scene.aetheria_layout
|
|
old = [cell.occupied for cell in state.cells]
|
|
width, height = state.width, state.height
|
|
expanded = [old[x * height + y] if x < width and y < height else False
|
|
for x in range(state.new_width) for y in range(state.new_height)]
|
|
state.cells.clear()
|
|
for cell in expanded:
|
|
state.cells.add().occupied = cell
|
|
state.width, state.height = state.new_width, state.new_height
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AETHERIA_OT_add_hardpoint(bpy.types.Operator):
|
|
bl_idname = "aetheria.add_ship_hardpoint"
|
|
bl_label = "Add Hardpoint"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
context.scene.aetheria_layout.hardpoints.add()
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AETHERIA_OT_remove_hardpoint(bpy.types.Operator):
|
|
bl_idname = "aetheria.remove_ship_hardpoint"
|
|
bl_label = "Remove Hardpoint"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
index: bpy.props.IntProperty()
|
|
|
|
def execute(self, context):
|
|
state = context.scene.aetheria_layout
|
|
if 0 <= self.index < len(state.hardpoints):
|
|
state.hardpoints.remove(self.index)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AETHERIA_OT_save_layout(bpy.types.Operator):
|
|
bl_idname = "aetheria.save_ship_layout"
|
|
bl_label = "Save Layout to .cc"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
try:
|
|
collection, path = _layout_path(context)
|
|
state = context.scene.aetheria_layout
|
|
if state.ship_id != collection["aetheria.id"] or state.ship_cc != path:
|
|
raise ValueError("Load this collection's layout before saving")
|
|
cells = [cell.occupied for cell in state.cells]
|
|
hardpoints = [encode_hardpoint(
|
|
Type=int(hp.kind), Position=[hp.x, hp.y], Shape=_parse_footprint(hp.footprint),
|
|
Transform=hp.mount_id, Rotation=int(hp.rotation), Armor=hp.armor, FiringArc=hp.firing_arc)
|
|
for hp in state.hardpoints]
|
|
state.revision = replace_layout(
|
|
path, _brokkr_cultlib(context), state.ship_id, state.revision,
|
|
[state.width, state.height, cells], hardpoints
|
|
)
|
|
self.report({"INFO"}, f"Saved {len(hardpoints)} hardpoints to {path}")
|
|
return {"FINISHED"}
|
|
except (OSError, ValueError, RuntimeError, ImportError, KeyError) as exc:
|
|
self.report({"ERROR"}, str(exc))
|
|
return {"CANCELLED"}
|
|
|
|
|
|
def _brokkr_cultlib(context):
|
|
brokkr = context.preferences.addons.get("brokkr_bridge")
|
|
if brokkr is None:
|
|
raise RuntimeError("Enable Brokkr before using Aetheria Ships")
|
|
return brokkr.preferences.cultlib_py_src
|
|
|
|
|
|
def _ship_collection(context):
|
|
obj = context.active_object
|
|
if obj is None:
|
|
raise ValueError("Select an object in the ship collection")
|
|
matches = [collection for collection in bpy.data.collections
|
|
if collection.get("aetheria.asset_kind") == "ship" and obj.name in collection.all_objects]
|
|
if len(matches) != 1:
|
|
raise ValueError("Selected object must belong to exactly one bound ship collection")
|
|
return matches[0]
|
|
|
|
|
|
class AETHERIA_OT_bind_ship_collection(bpy.types.Operator):
|
|
bl_idname = "aetheria.bind_ship_collection"
|
|
bl_label = "Bind Ship Collection"
|
|
bl_description = "Bind the active collection to one typed ship .cc record"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
try:
|
|
if not context.scene.aetheria_ship_cc_path:
|
|
raise ValueError("Choose an existing ship authoring .cc file")
|
|
obj = context.active_object
|
|
if obj is None:
|
|
raise ValueError("Select an object in the ship collection")
|
|
direct = list(obj.users_collection)
|
|
marked = [collection for collection in bpy.data.collections
|
|
if collection.get("aetheria.asset_kind") == "ship" and obj.name in collection.all_objects]
|
|
if len(marked) == 1:
|
|
collection = marked[0]
|
|
elif len(marked) == 0 and len(direct) == 1:
|
|
collection = direct[0]
|
|
else:
|
|
raise ValueError("Select an object in exactly one ship collection")
|
|
path = bpy.path.abspath(context.scene.aetheria_ship_cc_path)
|
|
ship_id = read(path, _brokkr_cultlib(context)).ship.body[0]
|
|
if collection.get("aetheria.asset_kind") not in (None, "ship"):
|
|
raise ValueError(f"{collection.name} has another aetheria.asset_kind")
|
|
if collection.get("aetheria.id") not in (None, ship_id):
|
|
raise ValueError(f"{collection.name} is bound to another ship ID")
|
|
stored = path
|
|
if bpy.data.filepath:
|
|
try:
|
|
stored = bpy.path.relpath(path)
|
|
except ValueError:
|
|
pass
|
|
collection["aetheria.asset_kind"] = "ship"
|
|
collection["aetheria.id"] = ship_id
|
|
collection["aetheria.ship_cc"] = stored
|
|
self.report({"INFO"}, f"Bound {collection.name} to {ship_id}")
|
|
return {"FINISHED"}
|
|
except (OSError, ValueError, RuntimeError, ImportError) as exc:
|
|
self.report({"ERROR"}, str(exc))
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class AETHERIA_OT_capture_ship_lines(bpy.types.Operator):
|
|
bl_idname = "aetheria.capture_ship_lines"
|
|
bl_label = "Capture Ship Lines"
|
|
bl_description = "Replace only the line slot in the selected typed ship .cc record"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
try:
|
|
obj = context.active_object
|
|
collection = _ship_collection(context)
|
|
if not collection.get("aetheria.ship_cc"):
|
|
raise ValueError(f"{collection.name} has no bound ship .cc path")
|
|
path = bpy.path.abspath(collection["aetheria.ship_cc"])
|
|
cultlib = _brokkr_cultlib(context)
|
|
if read(path, cultlib).ship.body[0] != collection["aetheria.id"]:
|
|
raise ValueError("The bound collection ID does not match its .cc ship ID")
|
|
lines = capture_grease_pencil(
|
|
obj, context.evaluated_depsgraph_get(), context.scene.frame_current,
|
|
evaluated=context.scene.aetheria_capture_evaluated_lines,
|
|
)
|
|
count = replace_lines(path, cultlib, lines)
|
|
self.report({"INFO"}, f"Captured {count} lines into {path}")
|
|
return {"FINISHED"}
|
|
except (OSError, ValueError, RuntimeError, ImportError) as exc:
|
|
self.report({"ERROR"}, str(exc))
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class AETHERIA_AP_preferences(bpy.types.AddonPreferences):
|
|
bl_idname = __package__
|
|
|
|
aetheria_repo: bpy.props.StringProperty(
|
|
name="Aetheria repo", subtype="DIR_PATH",
|
|
description="Aetheria checkout whose tools/AetherDb validates packages. "
|
|
"Empty: the nearest folder above the bound .cc that holds tools/AetherDb")
|
|
|
|
def draw(self, context):
|
|
self.layout.prop(self, "aetheria_repo")
|
|
|
|
|
|
def _aetheria_repo(context, ship_cc):
|
|
addon = context.preferences.addons.get(__package__)
|
|
configured = addon.preferences.aetheria_repo if addon and addon.preferences else ""
|
|
if configured:
|
|
repo = Path(bpy.path.abspath(configured))
|
|
if not (repo / "tools" / "AetherDb").is_dir():
|
|
raise ValueError(f"The Aetheria repo preference {repo} has no tools/AetherDb")
|
|
return repo
|
|
for folder in Path(ship_cc).resolve().parents:
|
|
if (folder / "tools" / "AetherDb").is_dir():
|
|
return folder
|
|
raise ValueError("No Aetheria repo above the ship .cc; set it in the add-on's preferences")
|
|
|
|
|
|
def _aetherdb(context, ship_cc, *args):
|
|
"""Runs one AetherDb ship-authoring command; returns its exit code and its output, stdout then stderr. The compiler
|
|
warnings 'dotnet run' prints when it rebuilds AetherDb are left out; errors are kept."""
|
|
repo = _aetheria_repo(context, ship_cc)
|
|
result = subprocess.run(
|
|
["dotnet", "run", "--project", str(repo / "tools" / "AetherDb"), "--", "ship-authoring", *args],
|
|
cwd=repo, capture_output=True, text=True, timeout=AETHERDB_TIMEOUT)
|
|
lines = (result.stdout + "\n" + result.stderr).splitlines()
|
|
return result.returncode, "\n".join(line for line in lines if line.strip() and ": warning " not in line)
|
|
|
|
|
|
def _layer_collection(layer, collection):
|
|
if layer.collection == collection:
|
|
return layer
|
|
for child in layer.children:
|
|
found = _layer_collection(child, collection)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
|
|
def _anchors(collection):
|
|
"""One ShipAnchor row per object tagged aetheria.role. Its aetheria.id is both the anchor id and the GLB node id."""
|
|
rows = []
|
|
for obj in sorted(collection.all_objects, key=lambda candidate: candidate.name):
|
|
role = obj.get("aetheria.role")
|
|
if role is None:
|
|
continue
|
|
anchor_id = obj.get("aetheria.id")
|
|
if not isinstance(anchor_id, str) or not anchor_id.strip():
|
|
raise ValueError(f"{obj.name} has aetheria.role {role} but no aetheria.id")
|
|
parent = None
|
|
if role == "weapon-muzzle":
|
|
parent = obj.parent.get("aetheria.id") if obj.parent else None
|
|
if not parent:
|
|
raise ValueError(f"Muzzle {obj.name} must be parented to its weapon mount object")
|
|
rows.append([anchor_id, str(role), anchor_id, parent, int(obj.get("aetheria.order", 0))])
|
|
return rows
|
|
|
|
|
|
def _export_glb(context, collection, filepath):
|
|
"""Exports the collection's objects, Grease Pencil excluded, with custom properties as node extras."""
|
|
view_layer = context.view_layer
|
|
layer = _layer_collection(view_layer.layer_collection, collection)
|
|
if layer is None:
|
|
raise ValueError(f"{collection.name} is not in the current view layer")
|
|
previous_layer = view_layer.active_layer_collection
|
|
previous_selection = [obj for obj in view_layer.objects if obj.select_get()]
|
|
previous_active = view_layer.objects.active
|
|
try:
|
|
view_layer.active_layer_collection = layer
|
|
for obj in previous_selection:
|
|
obj.select_set(False)
|
|
for obj in collection.all_objects:
|
|
if obj.type != "GREASEPENCIL" and obj.name in view_layer.objects:
|
|
obj.select_set(True)
|
|
bpy.ops.export_scene.gltf(
|
|
filepath=filepath, export_format="GLB", use_active_collection=True, use_selection=True,
|
|
export_extras=True, export_yup=True, export_apply=True)
|
|
finally:
|
|
for obj in view_layer.objects:
|
|
obj.select_set(obj in previous_selection)
|
|
view_layer.objects.active = previous_active
|
|
view_layer.active_layer_collection = previous_layer
|
|
|
|
|
|
class AETHERIA_OT_package_ship(bpy.types.Operator):
|
|
bl_idname = "aetheria.package_ship"
|
|
bl_label = "Package Ship"
|
|
bl_description = ("Write the anchors and ship.glb from the bound collection, capture its LineArt, "
|
|
"and run AetherDb's validator")
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
try:
|
|
collection, path = _layout_path(context)
|
|
cultlib = _brokkr_cultlib(context)
|
|
ship_id = collection["aetheria.id"]
|
|
if read(path, cultlib).ship.body[0] != ship_id:
|
|
raise ValueError("The bound collection ID does not match its .cc ship ID")
|
|
anchors = _anchors(collection)
|
|
_export_glb(context, collection, str(Path(path).with_name(MODEL_ASSET)))
|
|
replace_visual(path, cultlib, ship_id, MODEL_ASSET, anchors)
|
|
pencils = [obj for obj in collection.all_objects if obj.type == "GREASEPENCIL"]
|
|
if len(pencils) == 1:
|
|
replace_lines(path, cultlib, capture_grease_pencil(
|
|
pencils[0], context.evaluated_depsgraph_get(), scene.frame_current,
|
|
evaluated=scene.aetheria_capture_evaluated_lines))
|
|
code, message = _aetherdb(context, path, "validate", path)
|
|
except (OSError, ValueError, RuntimeError, ImportError, KeyError, subprocess.SubprocessError) as exc:
|
|
scene.aetheria_package_report = f"Package failed: {exc}"
|
|
self.report({"ERROR"}, scene.aetheria_package_report)
|
|
return {"CANCELLED"}
|
|
scene.aetheria_package_report = message or f"validate exited {code}"
|
|
if code != 0:
|
|
self.report({"ERROR"}, f"Packaged {len(anchors)} anchors; the validator refused it: {scene.aetheria_package_report}")
|
|
return {"CANCELLED"}
|
|
self.report({"INFO"}, scene.aetheria_package_report)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AETHERIA_PT_ship(bpy.types.Panel):
|
|
bl_label = "Aetheria Ship"
|
|
bl_idname = "AETHERIA_PT_ship"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Brokkr"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.prop(context.scene, "aetheria_ship_cc_path")
|
|
layout.operator("aetheria.bind_ship_collection", icon="LINKED")
|
|
layout.prop(context.scene, "aetheria_capture_evaluated_lines")
|
|
obj = context.active_object
|
|
layout.label(text=f"Source: {obj.name if obj else '(none)'}")
|
|
try:
|
|
collection = _ship_collection(context)
|
|
layout.label(text=f"Ship: {collection['aetheria.id']}")
|
|
except (ValueError, KeyError):
|
|
layout.label(text="No bound ship collection", icon="ERROR")
|
|
layout.operator("aetheria.capture_ship_lines", icon="GREASEPENCIL")
|
|
|
|
layout.separator()
|
|
layout.operator("aetheria.load_ship_layout", icon="IMPORT")
|
|
state = context.scene.aetheria_layout
|
|
if not state.ship_id:
|
|
self._draw_package(context, layout)
|
|
return
|
|
layout.label(text=f"Layout: {state.ship_id}")
|
|
dims = layout.row(align=True)
|
|
dims.prop(state, "new_width")
|
|
dims.prop(state, "new_height")
|
|
dims.operator("aetheria.resize_ship_layout", text="Resize")
|
|
grid = layout.box()
|
|
grid.label(text="Hull cells (top row first)")
|
|
if len(state.cells) == state.width * state.height:
|
|
for y in range(state.height - 1, -1, -1):
|
|
row = grid.row(align=True)
|
|
row.label(text=str(y))
|
|
for x in range(state.width):
|
|
row.prop(state.cells[x * state.height + y], "occupied", text="")
|
|
layout.label(text="Hardpoints")
|
|
for index, hp in enumerate(state.hardpoints):
|
|
box = layout.box()
|
|
header = box.row(align=True)
|
|
header.prop(hp, "mount_id")
|
|
header.operator("aetheria.remove_ship_hardpoint", text="", icon="X").index = index
|
|
box.prop(hp, "kind")
|
|
position = box.row(align=True)
|
|
position.prop(hp, "x")
|
|
position.prop(hp, "y")
|
|
box.prop(hp, "footprint")
|
|
box.prop(hp, "rotation")
|
|
box.prop(hp, "armor")
|
|
box.prop(hp, "firing_arc")
|
|
layout.operator("aetheria.add_ship_hardpoint", icon="ADD")
|
|
layout.operator("aetheria.save_ship_layout", icon="FILE_TICK")
|
|
self._draw_package(context, layout)
|
|
|
|
@staticmethod
|
|
def _draw_package(context, layout):
|
|
layout.separator()
|
|
layout.operator("aetheria.package_ship", icon="PACKAGE")
|
|
report = context.scene.aetheria_package_report
|
|
if report:
|
|
box = layout.box()
|
|
for line in report.splitlines():
|
|
box.label(text=line)
|
|
|
|
|
|
classes = (AETHERIA_PG_cell, AETHERIA_PG_hardpoint, AETHERIA_PG_layout,
|
|
AETHERIA_OT_load_layout, AETHERIA_OT_resize_layout,
|
|
AETHERIA_OT_add_hardpoint, AETHERIA_OT_remove_hardpoint,
|
|
AETHERIA_OT_save_layout, AETHERIA_OT_bind_ship_collection,
|
|
AETHERIA_OT_capture_ship_lines, AETHERIA_OT_package_ship, AETHERIA_AP_preferences, AETHERIA_PT_ship)
|
|
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.Scene.aetheria_layout = bpy.props.PointerProperty(type=AETHERIA_PG_layout)
|
|
bpy.types.Scene.aetheria_ship_cc_path = bpy.props.StringProperty(
|
|
name="Ship .cc", subtype="FILE_PATH", description="Existing typed ship authoring record"
|
|
)
|
|
bpy.types.Scene.aetheria_package_report = bpy.props.StringProperty(
|
|
name="Package report", description="AetherDb's verdict on the last Package Ship")
|
|
bpy.types.Scene.aetheria_capture_evaluated_lines = bpy.props.BoolProperty(
|
|
name="Evaluated LineArt", default=True,
|
|
description="Capture the visible modifier result at the current frame",
|
|
)
|
|
|
|
|
|
def unregister():
|
|
del bpy.types.Scene.aetheria_layout
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
del bpy.types.Scene.aetheria_capture_evaluated_lines
|
|
del bpy.types.Scene.aetheria_package_report
|
|
del bpy.types.Scene.aetheria_ship_cc_path
|