Re-design of submodules used in blender.git
This commit implements described in the #104573. The goal is to fix the confusion of the submodule hashes change, which are not ideal for any of the supported git-module configuration (they are either always visible causing confusion, or silently staged and committed, also causing confusion). This commit replaces submodules with a checkout of addons and addons_contrib, covered by the .gitignore, and locale and developer tools are moved to the main repository. This also changes the paths: - /release/scripts are moved to the /scripts - /source/tools are moved to the /tools - /release/datafiles/locale is moved to /locale This is done to avoid conflicts when using bisect, and also allow buildbot to automatically "recover" wgen building older or newer branches/patches. Running `make update` will initialize the local checkout to the changed repository configuration. Another aspect of the change is that the make update will support Github style of remote organization (origin remote pointing to thy fork, upstream remote pointing to the upstream blender/blender.git). Pull Request #104755
This commit is contained in:
committed by
Sergey Sharybin
parent
3e721195b0
commit
03806d0b67
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
from os.path import join
|
||||
|
||||
from autopep8_clean_config import PATHS, PATHS_EXCLUDE
|
||||
|
||||
from typing import (
|
||||
Callable,
|
||||
Generator,
|
||||
Optional,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
# Useful to disable when debugging warnings.
|
||||
USE_MULTIPROCESS = True
|
||||
|
||||
print(PATHS)
|
||||
SOURCE_EXT = (
|
||||
# Python
|
||||
".py",
|
||||
)
|
||||
|
||||
|
||||
def is_source_and_included(filename: str) -> bool:
|
||||
return (
|
||||
filename.endswith(SOURCE_EXT) and
|
||||
filename not in PATHS_EXCLUDE
|
||||
)
|
||||
|
||||
|
||||
def path_iter(
|
||||
path: str,
|
||||
filename_check: Optional[Callable[[str], bool]] = None,
|
||||
) -> Generator[str, None, None]:
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
# skip ".git"
|
||||
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
||||
|
||||
for filename in filenames:
|
||||
if filename.startswith("."):
|
||||
continue
|
||||
filepath = join(dirpath, filename)
|
||||
if filename_check is None or filename_check(filepath):
|
||||
yield filepath
|
||||
|
||||
|
||||
def path_expand(
|
||||
paths: Sequence[str],
|
||||
filename_check: Optional[Callable[[str], bool]] = None,
|
||||
) -> Generator[str, None, None]:
|
||||
for f in paths:
|
||||
if not os.path.exists(f):
|
||||
print("Missing:", f)
|
||||
elif os.path.isdir(f):
|
||||
yield from path_iter(f, filename_check)
|
||||
else:
|
||||
yield f
|
||||
|
||||
|
||||
def autopep8_format_file(f: str) -> None:
|
||||
print(f)
|
||||
subprocess.call((
|
||||
"autopep8",
|
||||
"--ignore",
|
||||
",".join((
|
||||
# Info: Use `isinstance()` instead of comparing types directly.
|
||||
# Why disable?: Changes code logic, in rare cases we want to compare exact types.
|
||||
"E721",
|
||||
# Info: Fix bare except.
|
||||
# Why disable?: Disruptive, leave our exceptions alone.
|
||||
"E722",
|
||||
# Info: Fix module level import not at top of file.
|
||||
# Why disable?: re-ordering imports is disruptive and breaks some scripts
|
||||
# that need to check if a module has already been loaded in the case of reloading.
|
||||
"E402",
|
||||
# Info: Try to make lines fit within --max-line-length characters.
|
||||
# Why disable? Causes lines to be wrapped, where long lines have the
|
||||
# trailing bracket moved to the end of the line.
|
||||
# If trailing commas were respected as they are by clang-format this might be acceptable.
|
||||
# Note that this doesn't disable all line wrapping.
|
||||
"E501",
|
||||
# Info: Fix various deprecated code (via lib2to3)
|
||||
# Why disable?: causes imports to be added/re-arranged.
|
||||
"W690",
|
||||
)),
|
||||
"--aggressive",
|
||||
"--in-place",
|
||||
"--max-line-length", "120",
|
||||
f,
|
||||
))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import sys
|
||||
|
||||
if os.path.samefile(sys.argv[-1], __file__):
|
||||
paths = path_expand(PATHS, is_source_and_included)
|
||||
else:
|
||||
paths = path_expand(sys.argv[1:], is_source_and_included)
|
||||
|
||||
if USE_MULTIPROCESS:
|
||||
import multiprocessing
|
||||
job_total = multiprocessing.cpu_count()
|
||||
pool = multiprocessing.Pool(processes=job_total * 2)
|
||||
pool.map(autopep8_format_file, paths)
|
||||
else:
|
||||
for f in paths:
|
||||
autopep8_format_file(f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,54 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import os
|
||||
|
||||
from typing import (
|
||||
Generator,
|
||||
Callable,
|
||||
Set,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
PATHS: Tuple[str, ...] = (
|
||||
"build_files",
|
||||
"doc",
|
||||
"release/datafiles",
|
||||
"release/lts",
|
||||
"release/scripts/freestyle",
|
||||
"release/scripts/modules",
|
||||
"release/scripts/presets",
|
||||
"release/scripts/startup",
|
||||
"release/scripts/templates_py",
|
||||
"source/blender",
|
||||
"tools",
|
||||
"tests",
|
||||
)
|
||||
|
||||
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", ".."))))
|
||||
|
||||
PATHS = tuple(
|
||||
os.path.join(SOURCE_DIR, p.replace("/", os.sep))
|
||||
for p in PATHS
|
||||
)
|
||||
|
||||
PATHS_EXCLUDE: Set[str] = set(
|
||||
os.path.join(SOURCE_DIR, p.replace("/", os.sep))
|
||||
for p in
|
||||
(
|
||||
"tools/svn_rev_map/sha1_to_rev.py",
|
||||
"tools/svn_rev_map/rev_to_sha1.py",
|
||||
"tools/svn_rev_map/rev_to_sha1.py",
|
||||
"release/scripts/modules/rna_manual_reference.py",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def files(path: str, test_fn: Callable[[str], bool]) -> Generator[str, None, None]:
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
# skip '.git'
|
||||
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
||||
for filename in filenames:
|
||||
if test_fn(filename):
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
yield filepath
|
||||
Executable
+421
@@ -0,0 +1,421 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright 2015 Blender Foundation - Bastien Montagne.
|
||||
|
||||
|
||||
"""
|
||||
This is a tool for generating a JSon version of a blender file (only its structure, or all its data included).
|
||||
|
||||
It can also run some simple validity checks over a .blend file.
|
||||
|
||||
WARNING! This is still WIP tool!
|
||||
|
||||
Example usage:
|
||||
|
||||
./blend2json.py foo.blend
|
||||
|
||||
To output also all 'name' fields from data:
|
||||
|
||||
./blend2json.py --filter-data="name" foo.blend
|
||||
|
||||
To output complete DNA struct info:
|
||||
|
||||
./blend2json.py --full-dna foo.blend
|
||||
|
||||
To avoid getting all 'uid' old addresses (those will change really often even when data itself does not change,
|
||||
making diff pretty noisy):
|
||||
|
||||
./blend2json.py --no-old-addresses foo.blend
|
||||
|
||||
To check a .blend file instead of outputting its JSon version (use explicit -o option to do both at the same time):
|
||||
|
||||
./blend2json.py -c foo.blend
|
||||
|
||||
"""
|
||||
|
||||
FILTER_DOC = """
|
||||
Each generic filter is made of three arguments, the include/exclude toggle ('+'/'-'), a regex to match against the name
|
||||
of the field to check (either one of the 'meta-data' generated by json exporter, or actual data field from DNA structs),
|
||||
and some regex to match against the data of this field (JSON-ified representation of the data, hence always a string).
|
||||
|
||||
Filters are evaluated in the order they are given, that is, if a block does not pass the first filter,
|
||||
it is immediately rejected and no further check is done on it.
|
||||
|
||||
You can add some recursivity to a filter (that is, if an 'include' filter is successful over a 'pointer' property,
|
||||
it will also automatically include pointed data, with a level of recursivity), by adding either
|
||||
'*' (for infinite recursion) or a number (to specify the maximum level of recursion) to the include/exclude toggle.
|
||||
Note that it only makes sense in 'include' case, and gets ignored for 'exclude' one.
|
||||
|
||||
Examples:
|
||||
|
||||
To include only MESH blocks:
|
||||
|
||||
./blend2json.py --filter-block "+" "code" "ME" foo.blend
|
||||
|
||||
To include only MESH or CURVE blocks and all data used by them:
|
||||
|
||||
./blend2json.py --filter-block "+" "code" "(ME)|(CU)" --filter-block "+*" ".*" ".*" foo.blend
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
|
||||
# Avoid maintaining multiple blendfile modules
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "modules"))
|
||||
del sys
|
||||
|
||||
import blendfile
|
||||
|
||||
|
||||
##### Utils (own json formatting) #####
|
||||
|
||||
|
||||
def json_default(o):
|
||||
if isinstance(o, bytes):
|
||||
return repr(o)[2:-1]
|
||||
elif i is ...:
|
||||
return "<...>"
|
||||
return o
|
||||
|
||||
|
||||
def json_dumps(i):
|
||||
return json.dumps(i, default=json_default)
|
||||
|
||||
|
||||
def keyval_to_json(kvs, indent, indent_step, compact_output=False):
|
||||
if compact_output:
|
||||
return ('{' + ', '.join('"%s": %s' % (k, v) for k, v in kvs) + '}')
|
||||
else:
|
||||
return ('{%s' % indent_step[:-1] +
|
||||
(',\n%s%s' % (indent, indent_step)).join(
|
||||
('"%s":\n%s%s%s' % (k, indent, indent_step, v) if (v[0] in {'[', '{'}) else
|
||||
'"%s": %s' % (k, v)) for k, v in kvs) +
|
||||
'\n%s}' % indent)
|
||||
|
||||
|
||||
def list_to_json(lst, indent, indent_step, compact_output=False):
|
||||
if compact_output:
|
||||
return ('[' + ', '.join(l for l in lst) + ']')
|
||||
else:
|
||||
return ('[%s' % indent_step[:-1] +
|
||||
((',\n%s%s' % (indent, indent_step)).join(
|
||||
('\n%s%s%s' % (indent, indent_step, l) if (i == 0 and l[0] in {'[', '{'}) else l)
|
||||
for i, l in enumerate(lst))
|
||||
) +
|
||||
'\n%s]' % indent)
|
||||
|
||||
|
||||
##### Main 'struct' writers #####
|
||||
|
||||
def gen_fake_addresses(args, blend):
|
||||
if args.use_fake_address:
|
||||
hashes = set()
|
||||
ret = {}
|
||||
for block in blend.blocks:
|
||||
if not block.addr_old:
|
||||
continue
|
||||
hsh = block.get_data_hash()
|
||||
while hsh in hashes:
|
||||
hsh += 1
|
||||
hashes.add(hsh)
|
||||
ret[block.addr_old] = hsh
|
||||
return ret
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def bheader_to_json(args, fw, blend, indent, indent_step):
|
||||
fw('%s"%s": [\n' % (indent, "HEADER"))
|
||||
indent = indent + indent_step
|
||||
|
||||
keyval = (
|
||||
("magic", json_dumps(blend.header.magic)),
|
||||
("pointer_size", json_dumps(blend.header.pointer_size)),
|
||||
("is_little_endian", json_dumps(blend.header.is_little_endian)),
|
||||
("version", json_dumps(blend.header.version)),
|
||||
)
|
||||
keyval = keyval_to_json(keyval, indent, indent_step)
|
||||
fw('%s%s' % (indent, keyval))
|
||||
|
||||
indent = indent[:-len(indent_step)]
|
||||
fw('\n%s]' % indent)
|
||||
|
||||
|
||||
def do_bblock_filter(filters, blend, block, meta_keyval, data_keyval):
|
||||
def do_bblock_filter_data_recursive(blend, block, rec_lvl, rec_iter, key=None):
|
||||
fields = (blend.structs[block.sdna_index].fields if key is None else
|
||||
[blend.structs[block.sdna_index].field_from_name.get(key[1:-1].encode())])
|
||||
for fld in fields:
|
||||
if fld is None:
|
||||
continue
|
||||
if fld.dna_name.is_pointer:
|
||||
paths = ([(fld.dna_name.name_only, i) for i in range(fld.dna_name.array_size)]
|
||||
if fld.dna_name.array_size > 1 else [fld.dna_name.name_only])
|
||||
for p in paths:
|
||||
child_block = block.get_pointer(p)
|
||||
if child_block is not None:
|
||||
child_block.user_data = max(block.user_data, rec_iter)
|
||||
if rec_lvl != 0:
|
||||
do_bblock_filter_data_recursive(blend, child_block, rec_lvl - 1, rec_iter + 1)
|
||||
|
||||
has_include = False
|
||||
do_break = False
|
||||
rec_iter = 1
|
||||
if block.user_data is None:
|
||||
block.user_data = 0
|
||||
for include, rec_lvl, key, val in filters:
|
||||
if rec_lvl < 0:
|
||||
rec_lvl = 100
|
||||
has_include = has_include or include
|
||||
# Skip exclude filters if block was already processed some way.
|
||||
if not include and block.user_data is not None:
|
||||
continue
|
||||
has_match = False
|
||||
for k, v in meta_keyval:
|
||||
if key.search(k) and val.search(v):
|
||||
has_match = True
|
||||
if include:
|
||||
block.user_data = max(block.user_data, rec_iter)
|
||||
# Note that in include cases, we have to keep checking filters, since some 'include recursive'
|
||||
# ones may still have to be processed...
|
||||
else:
|
||||
block.user_data = min(block.user_data, -rec_iter)
|
||||
do_break = True # No need to check more filters in exclude case...
|
||||
break
|
||||
for k, v in data_keyval:
|
||||
if key.search(k) and val.search(v):
|
||||
has_match = True
|
||||
if include:
|
||||
block.user_data = max(block.user_data, rec_iter)
|
||||
if rec_lvl != 0:
|
||||
do_bblock_filter_data_recursive(blend, block, rec_lvl - 1, rec_iter + 1, k)
|
||||
# Note that in include cases, we have to keep checking filters, since some 'include recursive'
|
||||
# ones may still have to be processed...
|
||||
else:
|
||||
block.user_data = min(block.user_data, -rec_iter)
|
||||
do_break = True # No need to check more filters in exclude case...
|
||||
break
|
||||
if include and not has_match: # Include check failed, implies exclusion.
|
||||
block.user_data = min(block.user_data, -rec_iter)
|
||||
do_break = True # No need to check more filters in exclude case...
|
||||
if do_break:
|
||||
break
|
||||
# Implicit 'include all' in case no include filter is specified...
|
||||
if block.user_data == 0 and not has_include:
|
||||
block.user_data = max(block.user_data, rec_iter)
|
||||
|
||||
|
||||
def bblocks_to_json(args, fw, blend, address_map, indent, indent_step):
|
||||
no_address = args.no_address
|
||||
full_data = args.full_data
|
||||
filter_data = args.filter_data
|
||||
|
||||
def gen_meta_keyval(blend, block):
|
||||
keyval = [
|
||||
("code", json_dumps(block.code)),
|
||||
("size", json_dumps(block.size)),
|
||||
]
|
||||
if not no_address:
|
||||
keyval += [("addr_old", json_dumps(address_map.get(block.addr_old, block.addr_old)))]
|
||||
keyval += [
|
||||
("dna_type_id", json_dumps(blend.structs[block.sdna_index].dna_type_id)),
|
||||
("count", json_dumps(block.count)),
|
||||
]
|
||||
return keyval
|
||||
|
||||
def gen_data_keyval(blend, block, key_filter=None):
|
||||
def _is_pointer(k):
|
||||
return blend.structs[block.sdna_index].field_from_path(blend.header, blend.handle, k).dna_name.is_pointer
|
||||
if key_filter is not None:
|
||||
return [(json_dumps(k)[1:-1], json_dumps(address_map.get(v, v) if _is_pointer(k) else v))
|
||||
for k, v in block.items_recursive_iter() if k in key_filter]
|
||||
return [(json_dumps(k)[1:-1], json_dumps(address_map.get(v, v) if _is_pointer(k) else v))
|
||||
for k, v in block.items_recursive_iter()]
|
||||
|
||||
if args.block_filters:
|
||||
for block in blend.blocks:
|
||||
meta_keyval = gen_meta_keyval(blend, block)
|
||||
data_keyval = gen_data_keyval(blend, block)
|
||||
do_bblock_filter(args.block_filters, blend, block, meta_keyval, data_keyval)
|
||||
|
||||
fw('%s"%s": [\n' % (indent, "DATA"))
|
||||
indent = indent + indent_step
|
||||
|
||||
is_first = True
|
||||
for i, block in enumerate(blend.blocks):
|
||||
if block.user_data is None or block.user_data > 0:
|
||||
meta_keyval = gen_meta_keyval(blend, block)
|
||||
if full_data:
|
||||
meta_keyval.append(("data", keyval_to_json(gen_data_keyval(blend, block),
|
||||
indent + indent_step, indent_step, args.compact_output)))
|
||||
elif filter_data:
|
||||
meta_keyval.append(("data", keyval_to_json(gen_data_keyval(blend, block, filter_data),
|
||||
indent + indent_step, indent_step, args.compact_output)))
|
||||
keyval = keyval_to_json(meta_keyval, indent, indent_step, args.compact_output)
|
||||
fw('%s%s%s' % ('' if is_first else ',\n', indent, keyval))
|
||||
is_first = False
|
||||
|
||||
indent = indent[:-len(indent_step)]
|
||||
fw('\n%s]' % indent)
|
||||
|
||||
|
||||
def bdna_to_json(args, fw, blend, indent, indent_step):
|
||||
full_dna = args.full_dna and not args.compact_output
|
||||
|
||||
def bdna_fields_to_json(blend, dna, indent, indent_step):
|
||||
lst = []
|
||||
for i, field in enumerate(dna.fields):
|
||||
keyval = (
|
||||
("dna_name", json_dumps(field.dna_name.name_only)),
|
||||
("dna_type_id", json_dumps(field.dna_type.dna_type_id)),
|
||||
("is_pointer", json_dumps(field.dna_name.is_pointer)),
|
||||
("is_method_pointer", json_dumps(field.dna_name.is_method_pointer)),
|
||||
("array_size", json_dumps(field.dna_name.array_size)),
|
||||
)
|
||||
lst.append(keyval_to_json(keyval, indent + indent_step, indent_step))
|
||||
return list_to_json(lst, indent, indent_step)
|
||||
|
||||
fw('%s"%s": [\n' % (indent, "DNA_STRUCT"))
|
||||
indent = indent + indent_step
|
||||
|
||||
is_first = True
|
||||
for dna in blend.structs:
|
||||
keyval = [
|
||||
("dna_type_id", json_dumps(dna.dna_type_id)),
|
||||
("size", json_dumps(dna.size)),
|
||||
]
|
||||
if full_dna:
|
||||
keyval += [("fields", bdna_fields_to_json(blend, dna, indent + indent_step, indent_step))]
|
||||
else:
|
||||
keyval += [("nbr_fields", json_dumps(len(dna.fields)))]
|
||||
keyval = keyval_to_json(keyval, indent, indent_step, args.compact_output)
|
||||
fw('%s%s%s' % ('' if is_first else ',\n', indent, keyval))
|
||||
is_first = False
|
||||
|
||||
indent = indent[:-len(indent_step)]
|
||||
fw('\n%s]' % indent)
|
||||
|
||||
|
||||
def blend_to_json(args, f, blend, address_map):
|
||||
fw = f.write
|
||||
fw('{\n')
|
||||
indent = indent_step = " "
|
||||
bheader_to_json(args, fw, blend, indent, indent_step)
|
||||
fw(',\n')
|
||||
bblocks_to_json(args, fw, blend, address_map, indent, indent_step)
|
||||
fw(',\n')
|
||||
bdna_to_json(args, fw, blend, indent, indent_step)
|
||||
fw('\n}\n')
|
||||
|
||||
|
||||
##### Checks #####
|
||||
|
||||
def check_file(args, blend):
|
||||
addr_old = set()
|
||||
for block in blend.blocks:
|
||||
if block.addr_old in addr_old:
|
||||
print("ERROR! Several data blocks share same 'addr_old' uuid %d, "
|
||||
"this should never happen!" % block.addr_old)
|
||||
continue
|
||||
addr_old.add(block.addr_old)
|
||||
|
||||
|
||||
##### Main #####
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
global __doc__
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
usage_text = __doc__
|
||||
|
||||
epilog = "This script is typically used to check differences between .blend files, or to check their validity."
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
parser.add_argument(
|
||||
dest="input", nargs="+", metavar='PATH',
|
||||
help="Input .blend file(s)")
|
||||
parser.add_argument(
|
||||
"-o", "--output", dest="output", action="append", metavar='PATH', required=False,
|
||||
help="Output .json file(s) (same path/name as input file(s) if not specified)")
|
||||
parser.add_argument(
|
||||
"-c", "--check-file", dest="check_file", default=False, action='store_true', required=False,
|
||||
help=("Perform some basic validation checks over the .blend file"))
|
||||
parser.add_argument(
|
||||
"--compact-output", dest="compact_output", default=False, action='store_true', required=False,
|
||||
help=("Output a very compact representation of blendfile (one line per block/DNAStruct)"))
|
||||
parser.add_argument(
|
||||
"--no-old-addresses", dest="no_address", default=False, action='store_true', required=False,
|
||||
help=("Do not output old memory address of each block of data "
|
||||
"(used as 'uuid' in .blend files, but change pretty noisily)"))
|
||||
parser.add_argument(
|
||||
"--no-fake-old-addresses", dest="use_fake_address", default=True, action='store_false',
|
||||
required=False,
|
||||
help=("Do not 'rewrite' old memory address of each block of data "
|
||||
"(they are rewritten by default to some hash of their content, "
|
||||
"to try to avoid too much diff noise between different but similar files)"))
|
||||
parser.add_argument(
|
||||
"--full-data", dest="full_data",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Also put in JSon file data itself "
|
||||
"(WARNING! will generate *huge* verbose files - and is far from complete yet)"))
|
||||
parser.add_argument(
|
||||
"--filter-data", dest="filter_data",
|
||||
default=None, required=False,
|
||||
help=("Only put in JSon file data fields which names match given comma-separated list "
|
||||
"(ignored if --full-data is set)"))
|
||||
parser.add_argument(
|
||||
"--full-dna", dest="full_dna", default=False, action='store_true', required=False,
|
||||
help=("Also put in JSon file dna properties description (ignored when --compact-output is used)"))
|
||||
|
||||
group = parser.add_argument_group("Filters", FILTER_DOC)
|
||||
group.add_argument(
|
||||
"--filter-block", dest="block_filters", nargs=3, action='append',
|
||||
help=("Filter to apply to BLOCKS (a.k.a. data itself)"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
if not args.output:
|
||||
if args.check_file:
|
||||
args.output = [None] * len(args.input)
|
||||
else:
|
||||
args.output = [os.path.splitext(infile)[0] + ".json" for infile in args.input]
|
||||
|
||||
if args.block_filters:
|
||||
args.block_filters = [(True if m[0] == "+" else False,
|
||||
0 if len(m) == 1 else (-1 if m[1] == "*" else int(m[1:])),
|
||||
re.compile(f), re.compile(d))
|
||||
for m, f, d in args.block_filters]
|
||||
|
||||
if args.filter_data:
|
||||
if args.full_data:
|
||||
args.filter_data = None
|
||||
else:
|
||||
args.filter_data = {n.encode() for n in args.filter_data.split(',')}
|
||||
|
||||
for infile, outfile in zip(args.input, args.output):
|
||||
with blendfile.open_blend(infile) as blend:
|
||||
address_map = gen_fake_addresses(args, blend)
|
||||
|
||||
if args.check_file:
|
||||
check_file(args, blend)
|
||||
|
||||
if outfile:
|
||||
with open(outfile, 'w', encoding="ascii", errors='xmlcharrefreplace') as f:
|
||||
blend_to_json(args, f, blend, address_map)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
r"""
|
||||
This script exports permutations of key-maps with different settings modified.
|
||||
|
||||
Useful for checking changes intended for one configuration don't impact others accidentally.
|
||||
|
||||
./blender.bin -b --factory-startup \
|
||||
--python tools/utils/blender_keyconfig_export_permutations.py -- \
|
||||
--preset=Blender \
|
||||
--output-dir=./output \
|
||||
--keymap-prefs=select_mouse:rmb_action
|
||||
|
||||
/blender.bin -b --factory-startup \
|
||||
--python tools/utils/blender_keyconfig_export_permutations.py -- \
|
||||
--preset=Blender_27x \
|
||||
--output-dir=output \
|
||||
--keymap-prefs="select_mouse"
|
||||
|
||||
The preferences setting: ``select_mouse:rmb_action`` expands into:
|
||||
|
||||
config = [
|
||||
("select_mouse", ('LEFT', 'RIGHT')),
|
||||
("rmb_action", ('TWEAK', 'FALLBACK_TOOL')),
|
||||
]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--preset",
|
||||
dest="preset",
|
||||
default="Blender",
|
||||
metavar='PRESET', type=str,
|
||||
help="The name of the preset to export",
|
||||
required=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
dest="output_dir",
|
||||
default=".",
|
||||
metavar='OUTPUT_DIR', type=str,
|
||||
help="The directory to output to.",
|
||||
required=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--keymap-prefs",
|
||||
dest="keymap_prefs",
|
||||
default="select_mouse:rmb_action",
|
||||
metavar='KEYMAP_PREFS', type=str,
|
||||
help=(
|
||||
"Colon separated list of attributes to generate key-map configuration permutations. "
|
||||
"WARNING: as all combinations are tested, their number increases exponentially!"
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def permutations_from_attrs_impl(config, permutation, index):
|
||||
index_next = index + 1
|
||||
attr, values = config[index]
|
||||
for val in values:
|
||||
permutation[index] = (attr, val)
|
||||
if index_next == len(config):
|
||||
yield tuple(permutation)
|
||||
else:
|
||||
# Keep walking down the list of permutations.
|
||||
yield from permutations_from_attrs_impl(config, permutation, index_next)
|
||||
# Not necessary, just ensure stale values aren't used.
|
||||
permutation[index] = None
|
||||
|
||||
|
||||
def permutations_from_attrs(config):
|
||||
"""
|
||||
Take a list of attributes and possible values:
|
||||
|
||||
config = [
|
||||
("select_mouse", ('LEFT', 'RIGHT')),
|
||||
("rmb_action", ('TWEAK', 'FALLBACK_TOOL')),
|
||||
]
|
||||
|
||||
Yielding all permutations:
|
||||
|
||||
[("select_mouse", 'LEFT'), ("rmb_action", 'TWEAK')],
|
||||
[("select_mouse", 'LEFT'), ("rmb_action", 'FALLBACK_TOOL')],
|
||||
... etc ...
|
||||
"""
|
||||
if not config:
|
||||
return ()
|
||||
permutation = [None] * len(config)
|
||||
result = list(permutations_from_attrs_impl(config, permutation, 0))
|
||||
assert permutation == ([None] * len(config))
|
||||
return result
|
||||
|
||||
|
||||
def permutation_as_filename(preset, values):
|
||||
"""
|
||||
Takes a configuration, eg:
|
||||
|
||||
[("select_mouse", 'LEFT'), ("rmb_action", 'TWEAK')]
|
||||
|
||||
And returns a filename compatible path:
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
if not values:
|
||||
return quote(preset)
|
||||
|
||||
return quote(
|
||||
preset + "_" + ".".join([
|
||||
"-".join((str(key), str(val)))
|
||||
for key, val in values
|
||||
]),
|
||||
# Needed so forward slashes aren't included in the resulting name.
|
||||
safe="",
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
try:
|
||||
import bpy
|
||||
except ImportError:
|
||||
# Run outside of Blender, just show "--help".
|
||||
bpy = None
|
||||
args.insert(0, "--help")
|
||||
|
||||
args = argparse_create().parse_args(args)
|
||||
if bpy is None:
|
||||
return
|
||||
|
||||
from bpy import context
|
||||
|
||||
preset = args.preset
|
||||
output_dir = args.output_dir
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Needed for background mode.
|
||||
preset_filepath = bpy.utils.preset_find(preset, preset_path="keyconfig")
|
||||
bpy.ops.preferences.keyconfig_activate(filepath=preset_filepath)
|
||||
|
||||
# Key-map preferences..
|
||||
km_prefs = context.window_manager.keyconfigs.active.preferences
|
||||
config = []
|
||||
# Use RNA introspection:
|
||||
if args.keymap_prefs:
|
||||
for attr in args.keymap_prefs.split(":"):
|
||||
if not hasattr(km_prefs, attr):
|
||||
print(f"KeyMap preferences does not have attribute: {attr:s}")
|
||||
sys.exit(1)
|
||||
|
||||
prop_def = km_prefs.rna_type.properties.get(attr)
|
||||
match prop_def.type:
|
||||
case 'ENUM':
|
||||
value = tuple(val.identifier for val in prop_def.enum_items)
|
||||
case 'BOOLEAN':
|
||||
value = (True, False)
|
||||
case _ as prop_def_type:
|
||||
raise Exception(f"Unhandled attribute type {prop_def_type:s}")
|
||||
config.append((attr, value))
|
||||
config = tuple(config)
|
||||
|
||||
for attr_permutation in (permutations_from_attrs(config) or ((),)):
|
||||
|
||||
# Reload and set.
|
||||
if attr_permutation is not None:
|
||||
km_prefs = context.window_manager.keyconfigs.active.preferences
|
||||
for attr, value in attr_permutation:
|
||||
setattr(km_prefs, attr, value)
|
||||
# Re-activate after setting preferences, tsk, ideally this shouldn't be necessary.
|
||||
bpy.ops.preferences.keyconfig_activate(filepath=preset_filepath)
|
||||
|
||||
filepath = os.path.join(output_dir, permutation_as_filename(preset, attr_permutation) + ".py")
|
||||
|
||||
print("Writing:", filepath)
|
||||
bpy.ops.preferences.keyconfig_export(filepath=filepath, all=True)
|
||||
|
||||
sys.exit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# We unfortunately ended with three commits instead of a single one to be handled as
|
||||
# 'clang-format' commit, we are handling them as a single 'block'.
|
||||
format_commits = (
|
||||
'e12c08e8d170b7ca40f204a5b0423c23a9fbc2c1',
|
||||
'91a9cd0a94000047248598394c41ac30f893f147',
|
||||
'3076d95ba441cd32706a27d18922a30f8fd28b8a',
|
||||
)
|
||||
pre_format_commit = format_commits[0] + '~1'
|
||||
|
||||
|
||||
def get_string(cmd):
|
||||
return subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode('utf8').strip()
|
||||
|
||||
|
||||
# Parse arguments.
|
||||
mode = None
|
||||
base_branch = 'main'
|
||||
if len(sys.argv) >= 2:
|
||||
# Note that recursive conflict resolution strategy has to reversed in rebase compared to merge.
|
||||
# See https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt--m
|
||||
if sys.argv[1] == '--rebase':
|
||||
mode = 'rebase'
|
||||
recursive_format_commit_merge_options = '-Xignore-all-space -Xtheirs'
|
||||
elif sys.argv[1] == '--merge':
|
||||
mode = 'merge'
|
||||
recursive_format_commit_merge_options = '-Xignore-all-space -Xours'
|
||||
if len(sys.argv) == 4:
|
||||
if sys.argv[2] == '--base_branch':
|
||||
base_branch = sys.argv[3]
|
||||
|
||||
if mode is None:
|
||||
print("Merge or rebase Blender main (or another base branch) into a branch in 3 steps,")
|
||||
print("to automatically merge clang-format changes.")
|
||||
print("")
|
||||
print(" --rebase Perform equivalent of 'git rebase main'")
|
||||
print(" --merge Perform equivalent of 'git merge main'")
|
||||
print("")
|
||||
print("Optional arguments:")
|
||||
print(" --base_branch <branch name> Use given branch instead of main")
|
||||
print(" (assuming that base branch has already been updated")
|
||||
print(" and has the initial clang-format commit).")
|
||||
sys.exit(0)
|
||||
|
||||
# Verify we are in the right directory.
|
||||
root_path = get_string(['git', 'rev-parse', '--show-superproject-working-tree'])
|
||||
if os.path.realpath(root_path) != os.path.realpath(os.getcwd()):
|
||||
print("BLENDER MERGE: must run from blender repository root directory")
|
||||
sys.exit(1)
|
||||
|
||||
# Abort if a rebase is still progress.
|
||||
rebase_merge = get_string(['git', 'rev-parse', '--git-path', 'rebase-merge'])
|
||||
rebase_apply = get_string(['git', 'rev-parse', '--git-path', 'rebase-apply'])
|
||||
merge_head = get_string(['git', 'rev-parse', '--git-path', 'MERGE_HEAD'])
|
||||
if os.path.exists(rebase_merge) or \
|
||||
os.path.exists(rebase_apply) or \
|
||||
os.path.exists(merge_head):
|
||||
print("BLENDER MERGE: rebase or merge in progress, complete it first")
|
||||
sys.exit(1)
|
||||
|
||||
# Abort if uncommitted changes.
|
||||
changes = get_string(['git', 'status', '--porcelain', '--untracked-files=no'])
|
||||
if len(changes) != 0:
|
||||
print("BLENDER MERGE: detected uncommitted changes, can't run")
|
||||
sys.exit(1)
|
||||
|
||||
# Setup command, with commit message for merge commits.
|
||||
if mode == 'rebase':
|
||||
mode_cmd = 'rebase'
|
||||
else:
|
||||
branch = get_string(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
|
||||
mode_cmd = 'merge --no-edit -m "Merge \'' + base_branch + '\' into \'' + branch + '\'"'
|
||||
|
||||
# Rebase up to the clang-format commit.
|
||||
code = os.system('git merge-base --is-ancestor ' + pre_format_commit + ' HEAD')
|
||||
if code != 0:
|
||||
code = os.system('git ' + mode_cmd + ' ' + pre_format_commit)
|
||||
if code != 0:
|
||||
print("BLENDER MERGE: resolve conflicts, complete " + mode + " and run again")
|
||||
sys.exit(code)
|
||||
|
||||
# Rebase clang-format commit.
|
||||
code = os.system('git merge-base --is-ancestor ' + format_commits[-1] + ' HEAD')
|
||||
if code != 0:
|
||||
os.system('git ' + mode_cmd + ' ' + recursive_format_commit_merge_options + ' ' + format_commits[-1])
|
||||
paths = get_string(('git', '--no-pager', 'diff', '--name-only', format_commits[-1])).replace('\n', ' ')
|
||||
if sys.platform == 'win32' and len(paths) > 8000:
|
||||
# Windows command-line does not accept more than 8191 chars.
|
||||
os.system('make format')
|
||||
else:
|
||||
os.system('make format PATHS="' + paths + '"')
|
||||
os.system('git add -u')
|
||||
count = int(get_string(['git', 'rev-list', '--count', '' + format_commits[-1] + '..HEAD']))
|
||||
if count == 1 or mode == 'merge':
|
||||
# Amend if we just have a single commit or are merging.
|
||||
os.system('git commit --amend --no-edit')
|
||||
else:
|
||||
# Otherwise create a commit for formatting.
|
||||
os.system('git commit -m "Cleanup: apply clang format"')
|
||||
|
||||
# Rebase remaining commits
|
||||
code = os.system('git ' + mode_cmd + ' ' + base_branch)
|
||||
if code != 0:
|
||||
print("BLENDER MERGE: resolve conflicts, complete " + mode + " and you're done")
|
||||
else:
|
||||
print("BLENDER MERGE: done")
|
||||
sys.exit(code)
|
||||
Executable
+251
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Generates 'userdef_default_theme.c' from a 'userpref.blend' file.
|
||||
|
||||
Pass your user preferenes blend file to this script to update the C source file.
|
||||
|
||||
eg:
|
||||
|
||||
./tools/utils/blender_theme_as_c.py ~/.config/blender/2.80/config/userpref.blend
|
||||
|
||||
.. or find the latest:
|
||||
|
||||
./tools/utils/blender_theme_as_c.py $(find ~/.config/blender -name "userpref.blend" | sort | tail -1)
|
||||
"""
|
||||
|
||||
C_SOURCE_HEADER = r'''/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/**
|
||||
* Generated by 'tools/utils/blender_theme_as_c.py'
|
||||
*
|
||||
* Do not hand edit this file!
|
||||
*/
|
||||
|
||||
#include "DNA_userdef_types.h"
|
||||
|
||||
#include "BLO_readfile.h"
|
||||
|
||||
/* clang-format off */
|
||||
|
||||
#ifdef __LITTLE_ENDIAN__
|
||||
# define RGBA(c) {((c) >> 24) & 0xff, ((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
|
||||
# define RGB(c) {((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
|
||||
#else
|
||||
# define RGBA(c) {(c) & 0xff, ((c) >> 8) & 0xff, ((c) >> 16) & 0xff, ((c) >> 24) & 0xff}
|
||||
# define RGB(c) {(c) & 0xff, ((c) >> 8) & 0xff, ((c) >> 16) & 0xff}
|
||||
#endif
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def round_float_32(f):
|
||||
from struct import pack, unpack
|
||||
return unpack("f", pack("f", f))[0]
|
||||
|
||||
|
||||
def repr_f32(f):
|
||||
f_round = round_float_32(f)
|
||||
f_str = repr(f)
|
||||
f_str_frac = f_str.partition(".")[2]
|
||||
if not f_str_frac:
|
||||
return f_str
|
||||
for i in range(1, len(f_str_frac)):
|
||||
f_test = round(f, i)
|
||||
f_test_round = round_float_32(f_test)
|
||||
if f_test_round == f_round:
|
||||
return "%.*f" % (i, f_test)
|
||||
return f_str
|
||||
|
||||
|
||||
import os
|
||||
|
||||
# Avoid maintaining multiple blendfile modules
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "modules"))
|
||||
del sys
|
||||
|
||||
source_dst = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..", "..", "..",
|
||||
"release", "datafiles", "userdef", "userdef_default_theme.c",
|
||||
)
|
||||
|
||||
dna_rename_defs_h = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..", "..", "..",
|
||||
"source", "blender", "makesdna", "intern", "dna_rename_defs.h",
|
||||
)
|
||||
|
||||
|
||||
def dna_rename_defs(blend):
|
||||
"""
|
||||
"""
|
||||
from blendfile import DNAName
|
||||
import re
|
||||
re_dna_struct_rename_elem = re.compile(
|
||||
r'DNA_STRUCT_RENAME_ELEM+\('
|
||||
r'([a-zA-Z0-9_]+)' r',\s*'
|
||||
r'([a-zA-Z0-9_]+)' r',\s*'
|
||||
r'([a-zA-Z0-9_]+)' r'\)',
|
||||
)
|
||||
with open(dna_rename_defs_h, 'r', encoding='utf-8') as fh:
|
||||
data = fh.read()
|
||||
for l in data.split('\n'):
|
||||
m = re_dna_struct_rename_elem.match(l)
|
||||
if m is not None:
|
||||
struct_name, member_storage, member_runtime = m.groups()
|
||||
struct_name = struct_name.encode('utf-8')
|
||||
member_storage = member_storage.encode('utf-8')
|
||||
member_runtime = member_runtime.encode('utf-8')
|
||||
dna_struct = blend.structs[blend.sdna_index_from_id[struct_name]]
|
||||
for field in dna_struct.fields:
|
||||
dna_name = field.dna_name
|
||||
if member_storage == dna_name.name_only:
|
||||
field.dna_name = dna_name = DNAName(dna_name.name_full)
|
||||
del dna_struct.field_from_name[dna_name.name_only]
|
||||
dna_name.name_full = dna_name.name_full.replace(member_storage, member_runtime)
|
||||
dna_name.name_only = member_runtime
|
||||
dna_struct.field_from_name[dna_name.name_only] = field
|
||||
|
||||
|
||||
def theme_data(userpref_filename):
|
||||
import blendfile
|
||||
blend = blendfile.open_blend(userpref_filename)
|
||||
dna_rename_defs(blend)
|
||||
u = next((c for c in blend.blocks if c.code == b'USER'), None)
|
||||
# theme_type = b.sdna_index_from_id[b'bTheme']
|
||||
t = u.get_pointer((b'themes', b'first'))
|
||||
t.refine_type(b'bTheme')
|
||||
return blend, t
|
||||
|
||||
|
||||
def is_ignore_dna_name(name):
|
||||
if name.startswith(b'_'):
|
||||
return True
|
||||
elif name in {
|
||||
b'active_theme_area',
|
||||
}:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def write_member(fw, indent, b, theme, ls):
|
||||
path_old = ()
|
||||
|
||||
for key, value in ls:
|
||||
key = key if type(key) is tuple else (key,)
|
||||
path_new = key[:-1]
|
||||
|
||||
if tuple(path_new) != tuple(path_old):
|
||||
if path_old:
|
||||
p = len(path_old) - 1
|
||||
while p >= 0 and (p >= len(path_new) or path_new[p] != path_old[p]):
|
||||
indent = p + 1
|
||||
fw('\t' * indent)
|
||||
fw('},\n')
|
||||
p -= 1
|
||||
del p
|
||||
|
||||
p = 0
|
||||
for p in range(min(len(path_old), len(path_new))):
|
||||
if path_old[p] != key[p]:
|
||||
break
|
||||
else:
|
||||
p = p + 1
|
||||
|
||||
for i, c in enumerate(path_new[p:]):
|
||||
indent = p + i + 1
|
||||
fw('\t' * indent)
|
||||
if type(c) is bytes:
|
||||
attr = c.decode('ascii')
|
||||
fw(f'.{attr} = ')
|
||||
fw('{\n')
|
||||
|
||||
if not is_ignore_dna_name(key[-1]):
|
||||
indent = '\t' * (len(path_new) + 1)
|
||||
attr = key[-1].decode('ascii')
|
||||
if isinstance(value, float):
|
||||
if value != 0.0:
|
||||
value_repr = repr_f32(value)
|
||||
fw(f'{indent}.{attr} = {value_repr}f,\n')
|
||||
elif isinstance(value, int):
|
||||
if value != 0:
|
||||
fw(f'{indent}.{attr} = {value},\n')
|
||||
elif isinstance(value, bytes):
|
||||
if set(value) != {0}:
|
||||
if len(value) == 3:
|
||||
value_repr = "".join(f'{ub:02x}' for ub in value)
|
||||
fw(f'{indent}.{attr} = RGB(0x{value_repr}),\n')
|
||||
elif len(value) == 4:
|
||||
value_repr = "".join(f'{ub:02x}' for ub in value)
|
||||
fw(f'{indent}.{attr} = RGBA(0x{value_repr}),\n')
|
||||
else:
|
||||
value = value.rstrip(b'\x00')
|
||||
is_ascii = True
|
||||
for ub in value:
|
||||
if not (ub >= 32 and ub < 127):
|
||||
is_ascii = False
|
||||
break
|
||||
if is_ascii:
|
||||
value_repr = value.decode('ascii')
|
||||
fw(f'{indent}.{attr} = "{value_repr}",\n')
|
||||
else:
|
||||
value_repr = "".join(f'{ub:02x}' for ub in value)
|
||||
fw(f'{indent}.{attr} = {{{value_repr}}},\n')
|
||||
else:
|
||||
fw(f'{indent}.{attr} = {value},\n')
|
||||
path_old = path_new
|
||||
|
||||
|
||||
def convert_data(blend, theme, f):
|
||||
fw = f.write
|
||||
fw(C_SOURCE_HEADER)
|
||||
fw('const bTheme U_theme_default = {\n')
|
||||
ls = list(theme.items_recursive_iter(use_nil=False))
|
||||
write_member(fw, 1, blend, theme, ls)
|
||||
|
||||
fw('};\n')
|
||||
fw('\n')
|
||||
fw('/* clang-format on */\n')
|
||||
|
||||
|
||||
def file_remove_empty_braces(source_dst):
|
||||
with open(source_dst, 'r', encoding='utf-8') as fh:
|
||||
data = fh.read()
|
||||
# Remove:
|
||||
# .foo = { }
|
||||
import re
|
||||
|
||||
def key_replace(match):
|
||||
return ""
|
||||
data_prev = None
|
||||
# Braces may become empty by removing nested
|
||||
while data != data_prev:
|
||||
data_prev = data
|
||||
data = re.sub(
|
||||
r'\s+\.[a-zA-Z_0-9]+\s+=\s+\{\s*\},',
|
||||
key_replace, data, re.MULTILINE,
|
||||
)
|
||||
|
||||
# Use two spaces instead of tabs.
|
||||
data = data.replace('\t', ' ')
|
||||
|
||||
with open(source_dst, 'w', encoding='utf-8') as fh:
|
||||
fh.write(data)
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
blend, theme = theme_data(sys.argv[-1])
|
||||
with open(source_dst, 'w', encoding='utf-8') as fh:
|
||||
convert_data(blend, theme, fh)
|
||||
|
||||
# Microsoft Visual Studio doesn't support empty braces.
|
||||
file_remove_empty_braces(source_dst)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+263
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Example use:
|
||||
|
||||
credits_git_gen.py --source=/src/blender --range=SHA1..HEAD
|
||||
"""
|
||||
|
||||
from git_log import GitCommitIter
|
||||
import unicodedata as ud
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Lookup Table to clean up the credits
|
||||
#
|
||||
# This is a combination of unifying git logs as well as
|
||||
# name change requested by the authors.
|
||||
|
||||
author_table = {
|
||||
"Aaron": "Aaron Carlisle",
|
||||
"Your Name": "Aaron Carlisle",
|
||||
"Alan": "Alan Troth",
|
||||
"andreas atteneder": "Andreas Atteneder",
|
||||
"Ankit": "Ankit Meel",
|
||||
"Antonioya": "Antonio Vazquez",
|
||||
"Antonio Vazquez": "Antonio Vazquez",
|
||||
"Antony Ryakiotakis": "Antony Riakiotakis",
|
||||
"Amélie Fondevilla": "Amelie Fondevilla",
|
||||
"bastien": "Bastien Montagne",
|
||||
"mont29": "Bastien Montagne",
|
||||
"bjornmose": "Bjorn Mose",
|
||||
"meta-androcto": "Brendon Murphy",
|
||||
"Brecht van Lommel": "Brecht Van Lommel",
|
||||
"Brecht Van Lömmel": "Brecht Van Lommel",
|
||||
"recht Van Lommel": "Brecht Van Lommel",
|
||||
"Clément Foucault": "Clément Foucault",
|
||||
"Clément": "Clément Foucault",
|
||||
"fclem": "Clément Foucault",
|
||||
"Clment Foucault": "Clément Foucault",
|
||||
"christian brinkmann": "Christian Brinkmann",
|
||||
"ZanQdo": "Daniel Salazar",
|
||||
"unclezeiv": "Davide Vercelli",
|
||||
"dilithjay": "Dilith Jayakody",
|
||||
"gaiaclary": "Gaia Clary",
|
||||
"Diego Hernan Borghetti": "Diego Borghetti",
|
||||
"Dotsnov Valentin": "Dontsov Valentin",
|
||||
"Eitan": "Eitan Traurig",
|
||||
"EitanSomething": "Eitan Traurig",
|
||||
"Erik": "Erik Abrahamsson",
|
||||
"Erick Abrahammson": "Erik Abrahamsson",
|
||||
"Eric Abrahamsson": "Erik Abrahamsson",
|
||||
"Ethan-Hall": "Ethan Hall",
|
||||
"filedescriptor": "Falk David",
|
||||
"Germano": "Germano Cavalcante",
|
||||
"Germano Cavalcantemano-wii": "Germano Cavalcante",
|
||||
"mano-wii": "Germano Cavalcante",
|
||||
"gsr": "Guillermo S. Romero",
|
||||
"Henrik Dick (weasel)": "Henrik Dick",
|
||||
"howardt": "Howard Trickey",
|
||||
"Iliay Katueshenock": "Iliya Katueshenock",
|
||||
"MOD": "Iliya Katueshenock",
|
||||
"Inês Almeida": "Ines Almeida",
|
||||
"brita": "Ines Almeida",
|
||||
"Ivan": "Ivan Perevala",
|
||||
"jensverwiebe": "Jens Verwiebe",
|
||||
"Jesse Y": "Jesse Yurkovich",
|
||||
"Joe Eagar": "Joseph Eagar",
|
||||
"Johnny Matthews (guitargeek)": "Johnny Matthews",
|
||||
"guitargeek": "Johnny Matthews",
|
||||
"jon denning": "Jon Denning",
|
||||
"julianeisel": "Julian Eisel",
|
||||
"Severin": "Julian Eisel",
|
||||
"Alex Strand": "Kenzie Strand",
|
||||
"Kevin Dietrich": "Kévin Dietrich",
|
||||
"Leon Leno": "Leon Schittek",
|
||||
"Lukas Toenne": "Lukas Tönne",
|
||||
"Mikhail": "Mikhail Matrosov",
|
||||
"OmarSquircleArt": "Omar Emara",
|
||||
"lazydodo": "Ray Molenkamp",
|
||||
"Ray molenkamp": "Ray Molenkamp",
|
||||
"Author Name": "Robert Guetzkow",
|
||||
"Sybren A. Stüvel": "Sybren A. Stüvel",
|
||||
"Simon": "Simon G",
|
||||
"Stephan": "Stephan Seitz",
|
||||
"Sebastian Herhoz": "Sebastian Herholz",
|
||||
"blender": "Sergey Sharybin",
|
||||
"Vuk GardaÅ¡eviÄ": "Vuk Gardašević",
|
||||
"ianwill": "Willian Padovani Germano",
|
||||
"Yiming Wu": "YimingWu",
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Class for generating credits
|
||||
|
||||
class CreditUser:
|
||||
__slots__ = (
|
||||
"commit_total",
|
||||
"year_min",
|
||||
"year_max",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.commit_total = 0
|
||||
|
||||
|
||||
class Credits:
|
||||
__slots__ = (
|
||||
"users",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.users = {}
|
||||
|
||||
def process_commit(self, c):
|
||||
# Normalize author string into canonical form, prevents duplicate credit users
|
||||
author = ud.normalize('NFC', c.author)
|
||||
author = author_table.get(author, author)
|
||||
year = c.date.year
|
||||
cu = self.users.get(author)
|
||||
if cu is None:
|
||||
cu = self.users[author] = CreditUser()
|
||||
cu.year_min = year
|
||||
cu.year_max = year
|
||||
|
||||
cu.commit_total += 1
|
||||
cu.year_min = min(cu.year_min, year)
|
||||
cu.year_max = max(cu.year_max, year)
|
||||
|
||||
def process(self, commit_iter):
|
||||
for i, c in enumerate(commit_iter):
|
||||
self.process_commit(c)
|
||||
if not (i % 100):
|
||||
print(i)
|
||||
|
||||
def write(self, filepath,
|
||||
is_main_credits=True,
|
||||
contrib_companies=(),
|
||||
sort="name"):
|
||||
|
||||
# patch_word = "patch", "patches"
|
||||
commit_word = "commit", "commits"
|
||||
|
||||
sorted_authors = {}
|
||||
if sort == "commit":
|
||||
sorted_authors = dict(sorted(self.users.items(), key=lambda item: item[1].commit_total))
|
||||
else:
|
||||
sorted_authors = dict(sorted(self.users.items()))
|
||||
|
||||
with open(filepath, 'w', encoding="ascii", errors='xmlcharrefreplace') as file:
|
||||
file.write("<h3>Individual Contributors</h3>\n\n")
|
||||
for author, cu in sorted_authors.items():
|
||||
file.write("{:s}, {:,d} {:s} {:s}<br />\n".format(
|
||||
author,
|
||||
cu.commit_total,
|
||||
commit_word[cu.commit_total > 1],
|
||||
("" if not is_main_credits else
|
||||
("- {:d}".format(cu.year_min) if cu.year_min == cu.year_max else
|
||||
("({:d} - {:d})".format(cu.year_min, cu.year_max))))))
|
||||
file.write("\n\n")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Companies, hard coded
|
||||
if is_main_credits:
|
||||
file.write("<h3>Contributions from Companies & Organizations</h3>\n")
|
||||
file.write("<p>\n")
|
||||
for line in contrib_companies:
|
||||
file.write("{:s}<br />\n".format(line))
|
||||
file.write("</p>\n")
|
||||
|
||||
import datetime
|
||||
now = datetime.datetime.now()
|
||||
fn = __file__.split("\\")[-1].split("/")[-1]
|
||||
file.write(
|
||||
"<p><center><i>Generated by '{:s}' {:d}/{:d}/{:d}</i></center></p>\n".format(
|
||||
fn, now.year, now.month, now.day
|
||||
))
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
usage_text = "Review revisions."
|
||||
|
||||
epilog = "This script is used to generate credits"
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
|
||||
|
||||
parser.add_argument(
|
||||
"--source", dest="source_dir",
|
||||
metavar='PATH',
|
||||
required=True,
|
||||
help="Path to git repository",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--range",
|
||||
dest="range_sha1",
|
||||
metavar='SHA1_RANGE',
|
||||
required=True,
|
||||
help="Range to use, eg: 169c95b8..HEAD",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--sort", dest="sort",
|
||||
metavar='METHOD',
|
||||
required=False,
|
||||
help="Sort credits by 'name' (default) or 'commit'",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
def is_credit_commit_valid(c):
|
||||
ignore_dir = (
|
||||
b"blender/extern/",
|
||||
b"blender/intern/opennl/",
|
||||
)
|
||||
|
||||
if not any(f for f in c.files if not f.startswith(ignore_dir)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# TODO, there are for sure more companies then are currently listed.
|
||||
# 1 liners for in html syntax
|
||||
contrib_companies = (
|
||||
"<b>Unity Technologies</b> - FBX Exporter",
|
||||
"<b>BioSkill GmbH</b> - H3D compatibility for X3D Exporter, "
|
||||
"OBJ Nurbs Import/Export",
|
||||
"<b>AutoCRC</b> - Improvements to fluid particles, vertex color baking",
|
||||
"<b>Adidas</b> - Principled BSDF shader in Cycles",
|
||||
"<b>AMD</b> - Cycles HIP GPU rendering, CPU optimizations",
|
||||
"<b>Intel</b> - Cycles oneAPI GPU rendering, CPU optimizations",
|
||||
"<b>NVIDIA</b> - Cycles OptiX GPU rendering, USD importer",
|
||||
"<b>Facebook</b> - Cycles subsurface scattering improvements",
|
||||
"<b>Apple</b> - Cycles Metal GPU backend",
|
||||
)
|
||||
|
||||
credits = Credits()
|
||||
# commit_range = "HEAD~10..HEAD"
|
||||
# commit_range = "blender-v2.81-release..blender-v2.82-release"
|
||||
# commit_range = "blender-v2.82-release"
|
||||
commit_range = args.range_sha1
|
||||
sort = args.sort
|
||||
citer = GitCommitIter(args.source_dir, commit_range)
|
||||
credits.process((c for c in citer if is_credit_commit_valid(c)))
|
||||
credits.write("credits.html",
|
||||
is_main_credits=True,
|
||||
contrib_companies=contrib_companies,
|
||||
sort=sort)
|
||||
print("Written: credits.html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+193
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import collections
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Hashes to be ignored
|
||||
#
|
||||
# The system sometimes fails to match commits and suggests to backport
|
||||
# revision which was already ported. In order to solve that we can:
|
||||
#
|
||||
# - Explicitly ignore some of the commits.
|
||||
# - Move the synchronization point forward.
|
||||
IGNORE_HASHES = {
|
||||
}
|
||||
|
||||
# Start revisions from both repositories.
|
||||
CYCLES_START_COMMIT = b"b941eccba81bbb1309a0eb4977fc3a77796f4ada" # blender-v2.92
|
||||
BLENDER_START_COMMIT = b"02948a2cab44f74ed101fc1b2ad9fe4431123e85" # v2.92
|
||||
|
||||
# Prefix which is common for all the subjects.
|
||||
GIT_SUBJECT_COMMON_PREFIX = b"Subject: [PATCH] "
|
||||
|
||||
# Marker which indicates begin of new file in the patch set.
|
||||
GIT_FILE_SECTION_MARKER = b"diff --git"
|
||||
|
||||
# Marker of the end of the patchset.
|
||||
GIT_PATCHSET_END_MARKER = b"-- "
|
||||
|
||||
# Prefix of topic to be omitted
|
||||
SUBJECT_SKIP_PREFIX = (
|
||||
b"Cycles: ",
|
||||
b"cycles: ",
|
||||
b"Cycles Standalone: ",
|
||||
b"Cycles standalone: ",
|
||||
b"cycles standalone: ",
|
||||
)
|
||||
|
||||
|
||||
def subject_strip(common_prefix, subject):
|
||||
for prefix in SUBJECT_SKIP_PREFIX:
|
||||
full_prefix = common_prefix + prefix
|
||||
if subject.startswith(full_prefix):
|
||||
subject = subject[len(full_prefix):].capitalize()
|
||||
subject = common_prefix + subject
|
||||
break
|
||||
return subject
|
||||
|
||||
|
||||
def replace_file_prefix(path, prefix, replace_prefix):
|
||||
tokens = path.split(b' ')
|
||||
prefix_len = len(prefix)
|
||||
for i, t in enumerate(tokens):
|
||||
for x in (b"a/", b"b/"):
|
||||
if t.startswith(x + prefix):
|
||||
tokens[i] = x + replace_prefix + t[prefix_len + 2:]
|
||||
return b' '.join(tokens)
|
||||
|
||||
|
||||
def cleanup_patch(patch, accept_prefix, replace_prefix):
|
||||
assert accept_prefix[0] != b'/'
|
||||
assert replace_prefix[0] != b'/'
|
||||
|
||||
full_accept_prefix = GIT_FILE_SECTION_MARKER + b" a/" + accept_prefix
|
||||
|
||||
with open(patch, "rb") as f:
|
||||
content = f.readlines()
|
||||
|
||||
clean_content = []
|
||||
do_skip = False
|
||||
for line in content:
|
||||
if line.startswith(GIT_SUBJECT_COMMON_PREFIX):
|
||||
# Skip possible prefix like "Cycles:", we already know change is
|
||||
# about Cycles since it's being committed to a Cycles repository.
|
||||
line = subject_strip(GIT_SUBJECT_COMMON_PREFIX, line)
|
||||
|
||||
# Dots usually are omitted in the topic
|
||||
line = line.replace(b".\n", b"\n")
|
||||
elif line.startswith(GIT_FILE_SECTION_MARKER):
|
||||
if not line.startswith(full_accept_prefix):
|
||||
do_skip = True
|
||||
else:
|
||||
do_skip = False
|
||||
line = replace_file_prefix(line, accept_prefix, replace_prefix)
|
||||
elif line.startswith(GIT_PATCHSET_END_MARKER):
|
||||
do_skip = False
|
||||
elif line.startswith(b"---") or line.startswith(b"+++"):
|
||||
line = replace_file_prefix(line, accept_prefix, replace_prefix)
|
||||
|
||||
if not do_skip:
|
||||
clean_content.append(line)
|
||||
|
||||
with open(patch, "wb") as f:
|
||||
f.writelines(clean_content)
|
||||
|
||||
|
||||
# Get mapping from commit subject to commit hash.
|
||||
#
|
||||
# It'll actually include timestamp of the commit to the map key, so commits with
|
||||
# the same subject wouldn't conflict with each other.
|
||||
def commit_map_get(repository, path, start_commit):
|
||||
command = (b"git",
|
||||
b"--git-dir=" + os.path.join(repository, b'.git'),
|
||||
b"--work-tree=" + repository,
|
||||
b"log", b"--format=%H %at %s", b"--reverse",
|
||||
start_commit + b'..HEAD',
|
||||
b'--',
|
||||
os.path.join(repository, path),
|
||||
b':(exclude)' + os.path.join(repository, b'intern/cycles/blender'))
|
||||
lines = subprocess.check_output(command).split(b"\n")
|
||||
commit_map = collections.OrderedDict()
|
||||
for line in lines:
|
||||
if line:
|
||||
commit_sha, stamped_subject = line.split(b' ', 1)
|
||||
stamp, subject = stamped_subject.split(b' ', 1)
|
||||
subject = subject_strip(b"", subject).rstrip(b".")
|
||||
stamped_subject = stamp + b" " + subject
|
||||
|
||||
if commit_sha in IGNORE_HASHES:
|
||||
continue
|
||||
commit_map[stamped_subject] = commit_sha
|
||||
return commit_map
|
||||
|
||||
|
||||
# Get difference between two lists of commits.
|
||||
# Returns two lists: first are the commits to be ported from Cycles to Blender,
|
||||
# second one are the commits to be ported from Blender to Cycles.
|
||||
def commits_get_difference(cycles_map, blender_map):
|
||||
cycles_to_blender = []
|
||||
for stamped_subject, commit_hash in cycles_map.items():
|
||||
if stamped_subject not in blender_map:
|
||||
cycles_to_blender.append(commit_hash)
|
||||
|
||||
blender_to_cycles = []
|
||||
for stamped_subject, commit_hash in blender_map.items():
|
||||
if stamped_subject not in cycles_map:
|
||||
blender_to_cycles.append(commit_hash)
|
||||
|
||||
return cycles_to_blender, blender_to_cycles
|
||||
|
||||
|
||||
# Transfer commits from one repository to another.
|
||||
# Doesn't do actual commit just for the safety.
|
||||
def transfer_commits(commit_hashes,
|
||||
from_repository,
|
||||
to_repository,
|
||||
dst_is_cycles):
|
||||
patch_index = 1
|
||||
for commit_hash in commit_hashes:
|
||||
command = (
|
||||
b"git",
|
||||
b"--git-dir=" + os.path.join(from_repository, b'.git'),
|
||||
b"--work-tree=" + from_repository,
|
||||
b"format-patch", b"-1",
|
||||
b"--start-number", bytes(str(patch_index), 'utf-8'),
|
||||
b"-o", to_repository,
|
||||
commit_hash,
|
||||
b'--',
|
||||
b':(exclude)' + os.path.join(from_repository, b'intern/cycles/blender'),
|
||||
)
|
||||
patch_file = subprocess.check_output(command).rstrip(b"\n")
|
||||
if dst_is_cycles:
|
||||
cleanup_patch(patch_file, b"intern/cycles", b"src")
|
||||
else:
|
||||
cleanup_patch(patch_file, b"src", b"intern/cycles")
|
||||
patch_index += 1
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: %s /path/to/cycles/ /path/to/blender/" % sys.argv[0])
|
||||
return
|
||||
|
||||
cycles_repository = sys.argv[1].encode()
|
||||
blender_repository = sys.argv[2].encode()
|
||||
|
||||
cycles_map = commit_map_get(cycles_repository, b'', CYCLES_START_COMMIT)
|
||||
blender_map = commit_map_get(blender_repository, b"intern/cycles", BLENDER_START_COMMIT)
|
||||
diff = commits_get_difference(cycles_map, blender_map)
|
||||
|
||||
transfer_commits(diff[0], cycles_repository, blender_repository, False)
|
||||
transfer_commits(diff[1], blender_repository, cycles_repository, True)
|
||||
|
||||
print("Missing commits were saved to the blender and cycles repositories.")
|
||||
print("Check them and if they're all fine run:")
|
||||
print("")
|
||||
print(" git am *.patch")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
class COLORS:
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
||||
|
||||
|
||||
VERBOSE = False
|
||||
|
||||
#########################################
|
||||
# Generic helper functions.
|
||||
|
||||
|
||||
def logVerbose(*args):
|
||||
if VERBOSE:
|
||||
print(*args)
|
||||
|
||||
|
||||
def logHeader(*args):
|
||||
print(COLORS.HEADER + COLORS.BOLD, end="")
|
||||
print(*args, end="")
|
||||
print(COLORS.ENDC)
|
||||
|
||||
|
||||
def logWarning(*args):
|
||||
print(COLORS.WARNING + COLORS.BOLD, end="")
|
||||
print(*args, end="")
|
||||
print(COLORS.ENDC)
|
||||
|
||||
|
||||
def logOk(*args):
|
||||
print(COLORS.OKGREEN + COLORS.BOLD, end="")
|
||||
print(*args, end="")
|
||||
print(COLORS.ENDC)
|
||||
|
||||
|
||||
def progress(count, total, prefix="", suffix=""):
|
||||
if VERBOSE:
|
||||
return
|
||||
|
||||
size = shutil.get_terminal_size((80, 20))
|
||||
|
||||
if prefix != "":
|
||||
prefix = prefix + " "
|
||||
if suffix != "":
|
||||
suffix = " " + suffix
|
||||
|
||||
bar_len = size.columns - len(prefix) - len(suffix) - 10
|
||||
filled_len = int(round(bar_len * count / float(total)))
|
||||
|
||||
percents = round(100.0 * count / float(total), 1)
|
||||
bar = '=' * filled_len + '-' * (bar_len - filled_len)
|
||||
|
||||
sys.stdout.write('%s[%s] %s%%%s\r' % (prefix, bar, percents, suffix))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def progressClear():
|
||||
if VERBOSE:
|
||||
return
|
||||
|
||||
size = shutil.get_terminal_size((80, 20))
|
||||
sys.stdout.write(" " * size.columns + "\r")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def humanReadableTimeDifference(seconds):
|
||||
hours = int(seconds) // 60 // 60
|
||||
seconds = seconds - hours * 60 * 60
|
||||
minutes = int(seconds) // 60
|
||||
seconds = seconds - minutes * 60
|
||||
if hours == 0:
|
||||
return "%02d:%05.2f" % (minutes, seconds)
|
||||
else:
|
||||
return "%02d:%02d:%05.2f" % (hours, minutes, seconds)
|
||||
|
||||
|
||||
def humanReadableTimeToSeconds(time):
|
||||
tokens = time.split(".")
|
||||
result = 0
|
||||
if len(tokens) == 2:
|
||||
result = float("0." + tokens[1])
|
||||
mult = 1
|
||||
for token in reversed(tokens[0].split(":")):
|
||||
result += int(token) * mult
|
||||
mult *= 60
|
||||
return result
|
||||
|
||||
#########################################
|
||||
# Benchmark specific helper functions.
|
||||
|
||||
|
||||
def configureArgumentParser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Cycles benchmark helper script.")
|
||||
parser.add_argument("-b", "--binary",
|
||||
help="Full file path to Blender's binary " +
|
||||
"to use for rendering",
|
||||
default="blender")
|
||||
parser.add_argument("-f", "--files", nargs='+')
|
||||
parser.add_argument("-v", "--verbose",
|
||||
help="Perform fully verbose communication",
|
||||
action="store_true",
|
||||
default=False)
|
||||
return parser
|
||||
|
||||
|
||||
def benchmarkFile(blender, blendfile, stats):
|
||||
logHeader("Begin benchmark of file {}" . format(blendfile))
|
||||
# Prepare some regex for parsing
|
||||
re_path_tracing = re.compile(".*Path Tracing Tile ([0-9]+)/([0-9]+)$")
|
||||
re_total_render_time = re.compile(r".*Total render time: ([0-9]+(\.[0-9]+)?)")
|
||||
re_render_time_no_sync = re.compile(
|
||||
".*Render time \\(without synchronization\\): ([0-9]+(\\.[0-9]+)?)")
|
||||
re_pipeline_time = re.compile(r"Time: ([0-9:\.]+) \(Saving: ([0-9:\.]+)\)")
|
||||
# Prepare output folder.
|
||||
# TODO(sergey): Use some proper output folder.
|
||||
output_folder = "/tmp/"
|
||||
# Configure command for the current file.
|
||||
command = (blender,
|
||||
"--background",
|
||||
"-noaudio",
|
||||
"--factory-startup",
|
||||
blendfile,
|
||||
"--engine", "CYCLES",
|
||||
"--debug-cycles",
|
||||
"--render-output", output_folder,
|
||||
"--render-format", "PNG",
|
||||
"-f", "1")
|
||||
# Run Blender with configured command line.
|
||||
logVerbose("About to execute command: {}" . format(command))
|
||||
start_time = time.time()
|
||||
process = subprocess.Popen(command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
# Keep reading status while Blender is alive.
|
||||
total_render_time = "N/A"
|
||||
render_time_no_sync = "N/A"
|
||||
pipeline_render_time = "N/A"
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if line == b"" and process.poll() is not None:
|
||||
break
|
||||
line = line.decode().strip()
|
||||
if line == "":
|
||||
continue
|
||||
logVerbose("Line from stdout: {}" . format(line))
|
||||
match = re_path_tracing.match(line)
|
||||
if match:
|
||||
current_tiles = int(match.group(1))
|
||||
total_tiles = int(match.group(2))
|
||||
elapsed_time = time.time() - start_time
|
||||
elapsed_time_str = humanReadableTimeDifference(elapsed_time)
|
||||
progress(current_tiles,
|
||||
total_tiles,
|
||||
prefix="Path Tracing Tiles {}" . format(elapsed_time_str))
|
||||
match = re_total_render_time.match(line)
|
||||
if match:
|
||||
total_render_time = float(match.group(1))
|
||||
match = re_render_time_no_sync.match(line)
|
||||
if match:
|
||||
render_time_no_sync = float(match.group(1))
|
||||
match = re_pipeline_time.match(line)
|
||||
if match:
|
||||
pipeline_render_time = humanReadableTimeToSeconds(match.group(1))
|
||||
|
||||
if process.returncode != 0:
|
||||
return False
|
||||
|
||||
# Clear line used by progress.
|
||||
progressClear()
|
||||
print("Total pipeline render time: {} ({} sec)"
|
||||
. format(humanReadableTimeDifference(pipeline_render_time),
|
||||
pipeline_render_time))
|
||||
print("Total Cycles render time: {} ({} sec)"
|
||||
. format(humanReadableTimeDifference(total_render_time),
|
||||
total_render_time))
|
||||
print("Pure Cycles render time (without sync): {} ({} sec)"
|
||||
. format(humanReadableTimeDifference(render_time_no_sync),
|
||||
render_time_no_sync))
|
||||
logOk("Successfully rendered")
|
||||
stats[blendfile] = {'PIPELINE_TOTAL': pipeline_render_time,
|
||||
'CYCLES_TOTAL': total_render_time,
|
||||
'CYCLES_NO_SYNC': render_time_no_sync}
|
||||
return True
|
||||
|
||||
|
||||
def benchmarkAll(blender, files):
|
||||
stats = {}
|
||||
for blendfile in files:
|
||||
try:
|
||||
benchmarkFile(blender, blendfile, stats)
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
logWarning("Rendering aborted!")
|
||||
return
|
||||
|
||||
|
||||
def main():
|
||||
parser = configureArgumentParser()
|
||||
args = parser.parse_args()
|
||||
if args.verbose:
|
||||
global VERBOSE
|
||||
VERBOSE = True
|
||||
benchmarkAll(args.binary, args.files)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
'''
|
||||
Define the command 'print_struct_c99' for gdb,
|
||||
useful for creating a literal for a nested runtime struct.
|
||||
|
||||
Example use:
|
||||
|
||||
(gdb) source tools/utils/gdb_struct_repr_c99.py
|
||||
(gdb) print_struct_c99 scene->toolsettings
|
||||
'''
|
||||
|
||||
|
||||
class PrintStructC99(gdb.Command):
|
||||
def __init__(self):
|
||||
super(PrintStructC99, self).__init__(
|
||||
"print_struct_c99",
|
||||
gdb.COMMAND_USER,
|
||||
)
|
||||
|
||||
def get_count_heading(self, string):
|
||||
for i, s in enumerate(string):
|
||||
if s != ' ':
|
||||
break
|
||||
return i
|
||||
|
||||
def extract_typename(self, string):
|
||||
first_line = string.split('\n')[0]
|
||||
return first_line.split('=')[1][:-1].strip()
|
||||
|
||||
def invoke(self, arg, from_tty):
|
||||
ret_ptype = gdb.execute('ptype {}'.format(arg), to_string=True)
|
||||
tname = self.extract_typename(ret_ptype)
|
||||
print('{} {} = {{'.format(tname, arg))
|
||||
r = gdb.execute('p {}'.format(arg), to_string=True)
|
||||
r = r.split('\n')
|
||||
for rr in r[1:]:
|
||||
if '=' not in rr:
|
||||
print(rr)
|
||||
continue
|
||||
hs = self.get_count_heading(rr)
|
||||
rr_s = rr.strip().split('=', 1)
|
||||
rr_rval = rr_s[1].strip()
|
||||
print(' ' * hs + '.' + rr_s[0] + '= ' + rr_rval)
|
||||
|
||||
|
||||
print('Running GDB from: %s\n' % (gdb.PYTHONDIR))
|
||||
gdb.execute("set print pretty")
|
||||
gdb.execute('set pagination off')
|
||||
gdb.execute('set print repeats 0')
|
||||
gdb.execute('set print elements unlimited')
|
||||
# instantiate
|
||||
PrintStructC99()
|
||||
@@ -0,0 +1,191 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Simple module for inspecting git commits
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
class GitCommit:
|
||||
__slots__ = (
|
||||
"sha1",
|
||||
# to extract more info
|
||||
"_git_dir",
|
||||
|
||||
# cached values
|
||||
"_author",
|
||||
"_date",
|
||||
"_body",
|
||||
"_files",
|
||||
"_files_status",
|
||||
)
|
||||
|
||||
def __init__(self, sha1, git_dir):
|
||||
self.sha1 = sha1
|
||||
self._git_dir = git_dir
|
||||
|
||||
self._author = \
|
||||
self._date = \
|
||||
self._body = \
|
||||
self._files = \
|
||||
self._files_status = \
|
||||
None
|
||||
|
||||
def cache(self):
|
||||
""" Cache all properties
|
||||
"""
|
||||
self.author
|
||||
self.date
|
||||
self.body
|
||||
self.files
|
||||
self.files_status
|
||||
|
||||
def _log_format(self, format, args=()):
|
||||
# sha1 = self.sha1.decode('ascii')
|
||||
cmd = (
|
||||
"git",
|
||||
"--git-dir",
|
||||
self._git_dir,
|
||||
"log",
|
||||
"-1", # only this rev
|
||||
self.sha1,
|
||||
"--format=" + format,
|
||||
) + args
|
||||
# print(" ".join(cmd))
|
||||
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
return p.stdout.read()
|
||||
|
||||
@property
|
||||
def sha1_short(self):
|
||||
cmd = (
|
||||
"git",
|
||||
"--git-dir",
|
||||
self._git_dir,
|
||||
"rev-parse",
|
||||
"--short",
|
||||
self.sha1,
|
||||
)
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
return p.stdout.read().strip().decode('ascii')
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
ret = self._author
|
||||
if ret is None:
|
||||
content = self._log_format("%an")[:-1]
|
||||
ret = content.decode("utf8", errors="ignore")
|
||||
self._author = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
ret = self._date
|
||||
if ret is None:
|
||||
import datetime
|
||||
ret = datetime.datetime.fromtimestamp(int(self._log_format("%ct")))
|
||||
self._date = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
ret = self._body
|
||||
if ret is None:
|
||||
content = self._log_format("%B")[:-1]
|
||||
ret = content.decode("utf8", errors="ignore")
|
||||
self._body = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return self.body.lstrip().partition("\n")[0]
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
ret = self._files
|
||||
if ret is None:
|
||||
ret = [f for f in self._log_format("format:", args=("--name-only",)).split(b"\n") if f]
|
||||
self._files = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def files_status(self):
|
||||
ret = self._files_status
|
||||
if ret is None:
|
||||
ret = [f.split(None, 1) for f in self._log_format("format:", args=("--name-status",)).split(b"\n") if f]
|
||||
self._files_status = ret
|
||||
return ret
|
||||
|
||||
|
||||
class GitCommitIter:
|
||||
__slots__ = (
|
||||
"_path",
|
||||
"_git_dir",
|
||||
"_sha1_range",
|
||||
"_process",
|
||||
)
|
||||
|
||||
def __init__(self, path, sha1_range):
|
||||
self._path = path
|
||||
self._git_dir = os.path.join(path, ".git")
|
||||
self._sha1_range = sha1_range
|
||||
self._process = None
|
||||
|
||||
def __iter__(self):
|
||||
cmd = (
|
||||
"git",
|
||||
"--git-dir",
|
||||
self._git_dir,
|
||||
"log",
|
||||
self._sha1_range,
|
||||
"--format=%H",
|
||||
)
|
||||
# print(" ".join(cmd))
|
||||
|
||||
self._process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
sha1 = self._process.stdout.readline()[:-1]
|
||||
if sha1:
|
||||
return GitCommit(sha1, self._git_dir)
|
||||
else:
|
||||
raise StopIteration
|
||||
|
||||
|
||||
class GitRepo:
|
||||
__slots__ = (
|
||||
"_path",
|
||||
"_git_dir",
|
||||
)
|
||||
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
self._git_dir = os.path.join(path, ".git")
|
||||
|
||||
@property
|
||||
def branch(self):
|
||||
cmd = (
|
||||
"git",
|
||||
"--git-dir",
|
||||
self._git_dir,
|
||||
"rev-parse",
|
||||
"--abbrev-ref",
|
||||
"HEAD",
|
||||
)
|
||||
# print(" ".join(cmd))
|
||||
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
return p.stdout.read()
|
||||
Executable
+253
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
This is a tool for reviewing commit ranges, writing into accept/reject files.
|
||||
|
||||
Useful for reviewing revisions to backport to stable builds.
|
||||
|
||||
Example usage:
|
||||
|
||||
./git_log_review_commits.py --source=../../.. --range=HEAD~40..HEAD --filter=BUGFIX
|
||||
"""
|
||||
|
||||
|
||||
class _Getch:
|
||||
"""
|
||||
Gets a single character from standard input.
|
||||
Does not echo to the screen.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.impl = _GetchWindows()
|
||||
except ImportError:
|
||||
self.impl = _GetchUnix()
|
||||
|
||||
def __call__(self):
|
||||
return self.impl()
|
||||
|
||||
|
||||
class _GetchUnix:
|
||||
|
||||
def __init__(self):
|
||||
import tty
|
||||
import sys
|
||||
|
||||
def __call__(self):
|
||||
import sys
|
||||
import tty
|
||||
import termios
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
return ch
|
||||
|
||||
|
||||
class _GetchWindows:
|
||||
|
||||
def __init__(self):
|
||||
import msvcrt
|
||||
|
||||
def __call__(self):
|
||||
import msvcrt
|
||||
return msvcrt.getch()
|
||||
|
||||
|
||||
getch = _Getch()
|
||||
# ------------------------------------------------------------------------------
|
||||
# Pretty Printing
|
||||
|
||||
USE_COLOR = True
|
||||
|
||||
if USE_COLOR:
|
||||
color_codes = {
|
||||
'black': '\033[0;30m',
|
||||
'bright_gray': '\033[0;37m',
|
||||
'blue': '\033[0;34m',
|
||||
'white': '\033[1;37m',
|
||||
'green': '\033[0;32m',
|
||||
'bright_blue': '\033[1;34m',
|
||||
'cyan': '\033[0;36m',
|
||||
'bright_green': '\033[1;32m',
|
||||
'red': '\033[0;31m',
|
||||
'bright_cyan': '\033[1;36m',
|
||||
'purple': '\033[0;35m',
|
||||
'bright_red': '\033[1;31m',
|
||||
'yellow': '\033[0;33m',
|
||||
'bright_purple': '\033[1;35m',
|
||||
'dark_gray': '\033[1;30m',
|
||||
'bright_yellow': '\033[1;33m',
|
||||
'normal': '\033[0m',
|
||||
}
|
||||
|
||||
def colorize(msg, color=None):
|
||||
return (color_codes[color] + msg + color_codes['normal'])
|
||||
else:
|
||||
def colorize(msg, color=None):
|
||||
return msg
|
||||
bugfix = ""
|
||||
# avoid encoding issues
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
|
||||
sys.stdin = os.fdopen(sys.stdin.fileno(), "rb")
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True)
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True)
|
||||
|
||||
|
||||
def print_commit(c):
|
||||
print("------------------------------------------------------------------------------")
|
||||
print(colorize("{{GitCommit|%s}}" % c.sha1.decode(), color='green'), end=" ")
|
||||
# print("Author: %s" % colorize(c.author, color='bright_blue'))
|
||||
print(colorize(c.author, color='bright_blue'))
|
||||
print()
|
||||
print(colorize(c.body, color='normal'))
|
||||
print()
|
||||
print(colorize("Files: (%d)" % len(c.files_status), color='yellow'))
|
||||
for f in c.files_status:
|
||||
print(colorize(" %s %s" % (f[0].decode('ascii'), f[1].decode('ascii')), 'yellow'))
|
||||
print()
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
usage_text = "Review revisions."
|
||||
|
||||
epilog = "This script is typically used to help write release notes"
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
|
||||
|
||||
parser.add_argument(
|
||||
"--source", dest="source_dir",
|
||||
metavar='PATH', required=True,
|
||||
help="Path to git repository")
|
||||
parser.add_argument(
|
||||
"--range", dest="range_sha1",
|
||||
metavar='SHA1_RANGE', required=True,
|
||||
help="Range to use, eg: 169c95b8..HEAD")
|
||||
parser.add_argument(
|
||||
"--author", dest="author",
|
||||
metavar='AUTHOR', type=str, required=False,
|
||||
help=("Method to filter commits in ['BUGFIX', todo]"))
|
||||
parser.add_argument(
|
||||
"--filter", dest="filter_type",
|
||||
metavar='FILTER', type=str, required=False,
|
||||
help=("Method to filter commits in ['BUGFIX', todo]"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
ACCEPT_FILE = "review_accept.txt"
|
||||
REJECT_FILE = "review_reject.txt"
|
||||
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
from git_log import GitCommitIter
|
||||
|
||||
# --------------
|
||||
# Filter Commits
|
||||
|
||||
def match(c):
|
||||
# filter_type
|
||||
if not args.filter_type:
|
||||
pass
|
||||
elif args.filter_type == 'BUGFIX':
|
||||
first_line = c.body.strip().split("\n")[0]
|
||||
assert len(first_line)
|
||||
if any(w for w in first_line.split() if w.lower().startswith(("fix", "bugfix", "bug-fix"))):
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
elif args.filter_type == 'NOISE':
|
||||
first_line = c.body.strip().split("\n")[0]
|
||||
assert len(first_line)
|
||||
if any(w for w in first_line.split() if w.lower().startswith("cleanup")):
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
raise Exception("Filter type %r isn't known" % args.filter_type)
|
||||
|
||||
# author
|
||||
if not args.author:
|
||||
pass
|
||||
elif args.author != c.author:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
commits = [c for c in GitCommitIter(args.source_dir, args.range_sha1) if match(c)]
|
||||
|
||||
# oldest first
|
||||
commits.reverse()
|
||||
|
||||
tot_accept = 0
|
||||
tot_reject = 0
|
||||
|
||||
def exit_message():
|
||||
print(" Written",
|
||||
colorize(ACCEPT_FILE, color='green'), "(%d)" % tot_accept,
|
||||
colorize(REJECT_FILE, color='red'), "(%d)" % tot_reject,
|
||||
)
|
||||
|
||||
for i, c in enumerate(commits):
|
||||
if os.name == "posix":
|
||||
# Also clears scroll-back.
|
||||
os.system("tput reset")
|
||||
else:
|
||||
print('\x1b[2J') # clear
|
||||
|
||||
sha1 = c.sha1
|
||||
|
||||
# diff may scroll off the screen, that's OK
|
||||
os.system("git --git-dir %s show %s --format=%%n" % (c._git_dir, sha1.decode('ascii')))
|
||||
print("")
|
||||
print_commit(c)
|
||||
sys.stdout.flush()
|
||||
# print(ch)
|
||||
while True:
|
||||
print("Space=" + colorize("Accept", 'green'),
|
||||
"Enter=" + colorize("Skip", 'red'),
|
||||
"Ctrl+C or Q=" + colorize("Quit", color='white'),
|
||||
"[%d of %d]" % (i + 1, len(commits)),
|
||||
"(+%d | -%d)" % (tot_accept, tot_reject),
|
||||
)
|
||||
ch = getch()
|
||||
|
||||
if ch == b'\x03' or ch == b'q':
|
||||
# Ctrl+C
|
||||
exit_message()
|
||||
print("Goodbye! (%s)" % c.sha1.decode())
|
||||
return
|
||||
|
||||
elif ch == b' ':
|
||||
log_filepath = ACCEPT_FILE
|
||||
tot_accept += 1
|
||||
break
|
||||
elif ch == b'\r':
|
||||
log_filepath = REJECT_FILE
|
||||
tot_reject += 1
|
||||
break
|
||||
else:
|
||||
print("Unknown input %r" % ch)
|
||||
|
||||
with open(log_filepath, 'ab') as f:
|
||||
f.write(sha1 + b'\n')
|
||||
|
||||
exit_message()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+770
@@ -0,0 +1,770 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
This is a tool for reviewing commit ranges, writing into accept/reject files,
|
||||
and optionally generate release-log-ready data.
|
||||
|
||||
Useful for reviewing revisions to back-port to stable builds.
|
||||
|
||||
Note that, if any of the data files generated already exist, they will be extended
|
||||
with new revisions, not overwritten.
|
||||
|
||||
Note that, for the most complex 'wiki-ready' file generated by `--accept-releaselog`,
|
||||
proof-reading after this tool has ran is heavily suggested!
|
||||
|
||||
Example usage:
|
||||
|
||||
./git_log_review_commits_advanced.py --source ../../.. --range HEAD~40..HEAD --filter 'BUGFIX' --accept-pretty --accept-releaselog --blender-rev 2.79
|
||||
|
||||
To add list of fixes between RC2 and RC3, and list both RC2 and RC3 fixes also in their own sections:
|
||||
|
||||
./git_log_review_commits_advanced.py --source ../../.. --range <RC2 revision>..<RC3 revision> --filter 'BUGFIX' --accept-pretty --accept-releaselog --blender-rev 2.79 --blender-rstate=RC3 --blender-rstate-list="RC2,RC3"
|
||||
|
||||
To exclude all commits from some given files, by sha1 or by commit message (from previously generated release logs) - much handy when going over commits which were partially cherry-picked into a previous release branch e.g.:
|
||||
|
||||
./git_log_review_commits_advanced.py --source ../../.. --range HEAD~40..HEAD --filter 'BUGFIX' --filter-exclude-sha1-fromfiles "review_accept.txt" "review_reject.txt" --filter-exclude-fromreleaselogs "review_accept_release_log.txt" --accept-pretty --accept-releaselog --blender-rev 2.75
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import re
|
||||
|
||||
ACCEPT_FILE = "review_accept.txt"
|
||||
REJECT_FILE = "review_reject.txt"
|
||||
ACCEPT_LOG_FILE = "review_accept_log.txt"
|
||||
ACCEPT_PRETTY_FILE = "review_accept_pretty.txt"
|
||||
ACCEPT_RELEASELOG_FILE = "review_accept_release_log.txt"
|
||||
|
||||
IGNORE_START_LINE = "<!-- IGNORE_START -->"
|
||||
IGNORE_END_LINE = "<!-- IGNORE_END -->"
|
||||
|
||||
_cwd = os.getcwd()
|
||||
__doc__ = __doc__ + \
|
||||
"\nRaw GIT revisions files:\n\t* Accepted: %s\n\t* Rejected: %s\n\n" \
|
||||
"Basic log accepted revisions: %s\n\nWiki-printed accepted revisions: %s\n\n" \
|
||||
"Full release notes wiki page: %s\n" \
|
||||
% (os.path.join(_cwd, ACCEPT_FILE), os.path.join(_cwd, REJECT_FILE),
|
||||
os.path.join(_cwd, ACCEPT_LOG_FILE), os.path.join(_cwd, ACCEPT_PRETTY_FILE),
|
||||
os.path.join(_cwd, ACCEPT_RELEASELOG_FILE))
|
||||
del _cwd
|
||||
|
||||
|
||||
class _Getch:
|
||||
"""
|
||||
Gets a single character from standard input.
|
||||
Does not echo to the screen.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.impl = _GetchWindows()
|
||||
except ImportError:
|
||||
self.impl = _GetchUnix()
|
||||
|
||||
def __call__(self):
|
||||
return self.impl()
|
||||
|
||||
|
||||
class _GetchUnix:
|
||||
|
||||
def __init__(self):
|
||||
import tty
|
||||
import sys
|
||||
|
||||
def __call__(self):
|
||||
import sys
|
||||
import tty
|
||||
import termios
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
return ch
|
||||
|
||||
|
||||
class _GetchWindows:
|
||||
|
||||
def __init__(self):
|
||||
import msvcrt
|
||||
|
||||
def __call__(self):
|
||||
import msvcrt
|
||||
return msvcrt.getch()
|
||||
|
||||
|
||||
getch = _Getch()
|
||||
# ------------------------------------------------------------------------------
|
||||
# Pretty Printing
|
||||
|
||||
USE_COLOR = True
|
||||
|
||||
if USE_COLOR:
|
||||
color_codes = {
|
||||
'black': '\033[0;30m',
|
||||
'bright_gray': '\033[0;37m',
|
||||
'blue': '\033[0;34m',
|
||||
'white': '\033[1;37m',
|
||||
'green': '\033[0;32m',
|
||||
'bright_blue': '\033[1;34m',
|
||||
'cyan': '\033[0;36m',
|
||||
'bright_green': '\033[1;32m',
|
||||
'red': '\033[0;31m',
|
||||
'bright_cyan': '\033[1;36m',
|
||||
'purple': '\033[0;35m',
|
||||
'bright_red': '\033[1;31m',
|
||||
'yellow': '\033[0;33m',
|
||||
'bright_purple': '\033[1;35m',
|
||||
'dark_gray': '\033[1;30m',
|
||||
'bright_yellow': '\033[1;33m',
|
||||
'normal': '\033[0m',
|
||||
}
|
||||
|
||||
def colorize(msg, color=None):
|
||||
return (color_codes[color] + msg + color_codes['normal'])
|
||||
else:
|
||||
def colorize(msg, color=None):
|
||||
return msg
|
||||
bugfix = ""
|
||||
|
||||
|
||||
BUGFIX_CATEGORIES = (
|
||||
("Objects / Animation / GP", (
|
||||
"Animation",
|
||||
"Constraints",
|
||||
"Grease Pencil",
|
||||
"Objects",
|
||||
"Dependency Graph",
|
||||
),
|
||||
),
|
||||
|
||||
("Data / Geometry", (
|
||||
"Armatures",
|
||||
"Curve/Text Editing",
|
||||
"Mesh Editing",
|
||||
"Meta Editing",
|
||||
"Modifiers",
|
||||
"Material / Texture",
|
||||
),
|
||||
),
|
||||
|
||||
("Physics / Simulations / Sculpt / Paint", (
|
||||
"Particles",
|
||||
"Physics / Hair / Simulations",
|
||||
"Sculpting / Painting",
|
||||
),
|
||||
),
|
||||
|
||||
("Image / Video / Render", (
|
||||
"Image / UV Editing",
|
||||
"Masking",
|
||||
"Motion Tracking",
|
||||
"Movie Clip Editor",
|
||||
"Nodes / Compositor",
|
||||
"Render",
|
||||
"Render: Cycles",
|
||||
"Render: Freestyle",
|
||||
"Sequencer",
|
||||
),
|
||||
),
|
||||
|
||||
("UI / Spaces / Transform", (
|
||||
"3D View",
|
||||
"Input (NDOF / 3D Mouse)",
|
||||
"Outliner",
|
||||
"Text Editor",
|
||||
"Transform",
|
||||
"User Interface",
|
||||
),
|
||||
),
|
||||
|
||||
("Game Engine", (
|
||||
),
|
||||
),
|
||||
|
||||
("System / Misc", (
|
||||
"Audio",
|
||||
"Collada",
|
||||
"File I/O",
|
||||
"Other",
|
||||
"Python",
|
||||
"System",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
sys.stdin = os.fdopen(sys.stdin.fileno(), "rb")
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True)
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True)
|
||||
|
||||
|
||||
def gen_commit_summary(c):
|
||||
# In git, all commit message lines until first empty one are part of 'summary'.
|
||||
return c.body.split("\n\n")[0].strip(" :.;-\n").replace("\n", " ")
|
||||
|
||||
|
||||
def print_commit(c):
|
||||
print("------------------------------------------------------------------------------")
|
||||
print(colorize(c.sha1.decode(), color='green'), end=" ")
|
||||
print(colorize(c.date.strftime("%Y/%m/%d"), color='purple'), end=" ")
|
||||
print(colorize(c.author, color='bright_blue'))
|
||||
print()
|
||||
print(colorize(c.body, color='normal'))
|
||||
print()
|
||||
print(colorize("Files: (%d)" % len(c.files_status), color='yellow'))
|
||||
for f in c.files_status:
|
||||
print(colorize(" %s %s" % (f[0].decode('ascii'), f[1].decode('ascii')), 'yellow'))
|
||||
print()
|
||||
|
||||
|
||||
def gen_commit_log(c):
|
||||
return "rB%s %s %-30s %s" % (c.sha1.decode()[:10], c.date.strftime("%Y/%m/%d"),
|
||||
c.author, gen_commit_summary(c))
|
||||
|
||||
|
||||
re_bugify_str = r"T([0-9]{1,})"
|
||||
re_bugify = re.compile(re_bugify_str)
|
||||
re_commitify = re.compile(r"\W(r(?:B|BA|BAC|BTS)[0-9a-fA-F]{6,})")
|
||||
re_prettify = re.compile(r"(.{,20}?)(Fix(?:ing|es)?\s*(?:for)?\s*" + re_bugify_str + r")\s*[-:,]*\s*", re.IGNORECASE)
|
||||
|
||||
|
||||
def gen_commit_message_pretty(c, unreported=None):
|
||||
body = gen_commit_summary(c)
|
||||
|
||||
tbody = re_prettify.sub(r"Fix {{BugReport|\3}}: \1", body)
|
||||
if tbody == body:
|
||||
if unreported is not None:
|
||||
unreported[0] = True
|
||||
tbody = "Fix unreported: %s" % body
|
||||
body = re_bugify.sub(r"{{BugReport|\1}}", tbody)
|
||||
body = re_commitify.sub(r"{{GitCommit|\1}}", body)
|
||||
|
||||
return body
|
||||
|
||||
|
||||
def gen_commit_pretty(c, unreported=None, rstate=None):
|
||||
body = gen_commit_message_pretty(c, unreported)
|
||||
|
||||
if rstate is not None:
|
||||
return "* [%s] %s ({{GitCommit|rB%s}})." % (rstate, body, c.sha1.decode()[:10])
|
||||
return "* %s ({{GitCommit|rB%s}})." % (rstate, body, c.sha1.decode()[:10])
|
||||
|
||||
|
||||
def gen_commit_unprettify(body):
|
||||
if body.startswith("* ["):
|
||||
end = body.find("]")
|
||||
if end > 0:
|
||||
body = body[end + 2:] # +2 to remove ] itself, and following space.
|
||||
start = body.rfind("({{GitCommit|rB")
|
||||
if start > 0:
|
||||
body = body[:start - 1] # -1 to remove trailing space.
|
||||
return body
|
||||
|
||||
|
||||
def print_categories_tree():
|
||||
for i, (main_cat, sub_cats) in enumerate(BUGFIX_CATEGORIES):
|
||||
print("\t[%d] %s" % (i, main_cat))
|
||||
for j, sub_cat in enumerate(sub_cats):
|
||||
print("\t\t[%d] %s" % (j, sub_cat))
|
||||
|
||||
|
||||
def release_log_extract_messages(path):
|
||||
messages = set()
|
||||
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
ignore = False
|
||||
header = True
|
||||
for l in f:
|
||||
if IGNORE_END_LINE in l:
|
||||
ignore = False
|
||||
continue
|
||||
elif ignore or IGNORE_START_LINE in l:
|
||||
ignore = True
|
||||
continue
|
||||
l = l.strip(" \n")
|
||||
if header and not l.startswith("=="):
|
||||
continue # Header, we don't care here.
|
||||
header = False
|
||||
if not l.startswith("==") and "Fix " in l:
|
||||
messages.add(gen_commit_unprettify(l))
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def release_log_init(path, source_dir, blender_rev, start_sha1, end_sha1, rstate, rstate_list):
|
||||
from git_log import GitRepo
|
||||
|
||||
if rstate is not None:
|
||||
header = "= Blender %s: Bug Fixes =\n\n" \
|
||||
"[%s] Changes from revision {{GitCommit|rB%s}} to {{GitCommit|rB%s}}, inclusive.\n\n" \
|
||||
% (blender_rev, rstate, start_sha1[:10], end_sha1[:10])
|
||||
else:
|
||||
header = "= Blender %s: Bug Fixes =\n\n" \
|
||||
"Changes from revision {{GitCommit|rB%s}} to {{GitCommit|rB%s}}, inclusive.\n\n" \
|
||||
% (blender_rev, start_sha1[:10], end_sha1[:10])
|
||||
|
||||
release_log = {"__HEADER__": header, "__COUNT__": [0, 0], "__RSTATES__": {k: [] for k in rstate_list}}
|
||||
|
||||
if os.path.exists(path):
|
||||
branch = GitRepo(source_dir).branch.decode().strip()
|
||||
|
||||
sub_cats_to_main_cats = {s_cat: m_cat[0] for m_cat in BUGFIX_CATEGORIES for s_cat in m_cat[1]}
|
||||
main_cats = {m_cat[0] for m_cat in BUGFIX_CATEGORIES}
|
||||
with open(path, 'r') as f:
|
||||
header = []
|
||||
main_cat = None
|
||||
sub_cat = None
|
||||
ignore = False
|
||||
for l in f:
|
||||
if IGNORE_END_LINE in l:
|
||||
ignore = False
|
||||
continue
|
||||
elif ignore or IGNORE_START_LINE in l:
|
||||
ignore = True
|
||||
continue
|
||||
l = l.strip(" \n")
|
||||
if not header:
|
||||
header.append(l)
|
||||
for hl in f:
|
||||
if IGNORE_END_LINE in hl:
|
||||
ignore = False
|
||||
continue
|
||||
elif ignore or IGNORE_START_LINE in hl:
|
||||
ignore = True
|
||||
continue
|
||||
hl = hl.strip(" \n")
|
||||
if hl.startswith("=="):
|
||||
main_cat = hl.strip(" =")
|
||||
if main_cat not in main_cats:
|
||||
sub_cat = main_cat
|
||||
main_cat = sub_cats_to_main_cats.get(main_cat, None)
|
||||
else:
|
||||
sub_cat = None
|
||||
#~ print("hl MAINCAT:", hl, main_cat, " | ", sub_cat)
|
||||
break
|
||||
header.append(hl)
|
||||
|
||||
if rstate is not None:
|
||||
release_log["__HEADER__"] = "%s[%s] Changes from revision {{GitCommit|%s}} to " \
|
||||
"{{GitCommit|%s}}, inclusive (''%s'' branch).\n\n" \
|
||||
"" % ("\n".join(header), rstate,
|
||||
start_sha1[:10], end_sha1[:10], branch)
|
||||
else:
|
||||
release_log["__HEADER__"] = "%sChanges from revision {{GitCommit|%s}} to {{GitCommit|%s}}, " \
|
||||
"inclusive (''%s'' branch).\n\n" \
|
||||
"" % ("\n".join(header), start_sha1[:10], end_sha1[:10], branch)
|
||||
count = release_log["__COUNT__"] = [0, 0]
|
||||
continue
|
||||
|
||||
if l.startswith("==="):
|
||||
sub_cat = l.strip(" =")
|
||||
if sub_cat in sub_cats_to_main_cats:
|
||||
main_cat = sub_cats_to_main_cats.get(sub_cat, None)
|
||||
elif sub_cat in main_cats:
|
||||
main_cat = sub_cat
|
||||
sub_cat = None
|
||||
else:
|
||||
main_cat = None
|
||||
#~ print("l SUBCAT:", l, main_cat, " | ", sub_cat)
|
||||
elif l.startswith("=="):
|
||||
main_cat = l.strip(" =")
|
||||
if main_cat not in main_cats:
|
||||
sub_cat = main_cat
|
||||
main_cat = sub_cats_to_main_cats.get(main_cat, None)
|
||||
else:
|
||||
sub_cat = None
|
||||
#~ print("l MAINCAT:", l, main_cat, " | ", sub_cat)
|
||||
elif "Fix " in l:
|
||||
if "Fix {{BugReport|" in l:
|
||||
main_cat_data, _ = release_log.setdefault(main_cat, ({}, {}))
|
||||
main_cat_data.setdefault(sub_cat, []).append(l)
|
||||
count[0] += 1
|
||||
#~ print("l REPORTED:", l)
|
||||
else:
|
||||
_, main_cat_data_unreported = release_log.setdefault(main_cat, ({}, {}))
|
||||
main_cat_data_unreported.setdefault(sub_cat, []).append(l)
|
||||
count[1] += 1
|
||||
#~ print("l UNREPORTED:", l)
|
||||
l_rstate = l.strip("* ")
|
||||
if l_rstate.startswith("["):
|
||||
end = l_rstate.find("]")
|
||||
if end > 0:
|
||||
rstate = l_rstate[1:end]
|
||||
if rstate in release_log["__RSTATES__"]:
|
||||
release_log["__RSTATES__"][rstate].append("* %s" % l_rstate[end + 1:].strip())
|
||||
|
||||
return release_log
|
||||
|
||||
|
||||
def write_release_log(path, release_log, c, cat, rstate, rstate_list):
|
||||
import io
|
||||
|
||||
main_cat, sub_cats = BUGFIX_CATEGORIES[cat[0]]
|
||||
sub_cat = sub_cats[cat[1]] if cat[1] is not None else None
|
||||
|
||||
main_cat_data, main_cat_data_unreported = release_log.setdefault(main_cat, ({}, {}))
|
||||
unreported = [False]
|
||||
entry = gen_commit_pretty(c, unreported, rstate)
|
||||
if unreported[0]:
|
||||
main_cat_data_unreported.setdefault(sub_cat, []).append(entry)
|
||||
release_log["__COUNT__"][1] += 1
|
||||
else:
|
||||
main_cat_data.setdefault(sub_cat, []).append(entry)
|
||||
release_log["__COUNT__"][0] += 1
|
||||
|
||||
if rstate in release_log["__RSTATES__"]:
|
||||
release_log["__RSTATES__"][rstate].append(gen_commit_pretty(c))
|
||||
|
||||
lines = []
|
||||
main_cat_lines = []
|
||||
sub_cat_lines = []
|
||||
for main_cat, sub_cats in BUGFIX_CATEGORIES:
|
||||
main_cat_data = release_log.get(main_cat, ({}, {}))
|
||||
main_cat_lines[:] = ["== %s ==" % main_cat]
|
||||
for data in main_cat_data:
|
||||
entries = data.get(None, [])
|
||||
if entries:
|
||||
main_cat_lines.extend(entries)
|
||||
main_cat_lines.append("")
|
||||
if len(main_cat_lines) == 1:
|
||||
main_cat_lines.append("")
|
||||
for sub_cat in sub_cats:
|
||||
sub_cat_lines[:] = ["=== %s ===" % sub_cat]
|
||||
for data in main_cat_data:
|
||||
entries = data.get(sub_cat, [])
|
||||
if entries:
|
||||
sub_cat_lines.extend(entries)
|
||||
sub_cat_lines.append("")
|
||||
if len(sub_cat_lines) > 2:
|
||||
main_cat_lines += sub_cat_lines
|
||||
if len(main_cat_lines) > 2:
|
||||
lines += main_cat_lines
|
||||
|
||||
if None in release_log:
|
||||
main_cat_data = release_log.get(None, ({}, {}))
|
||||
main_cat_lines[:] = ["== %s ==\n\n" % "UNSORTED"]
|
||||
for data in main_cat_data:
|
||||
entries = data.get(None, [])
|
||||
if entries:
|
||||
main_cat_lines.extend(entries)
|
||||
main_cat_lines.append("")
|
||||
if len(main_cat_lines) > 2:
|
||||
lines += main_cat_lines
|
||||
|
||||
with open(path, 'w') as f:
|
||||
f.write(release_log["__HEADER__"])
|
||||
|
||||
count = release_log["__COUNT__"]
|
||||
f.write("%s\n" % IGNORE_START_LINE)
|
||||
f.write("Total fixed bugs: %d (%d from tracker, %d reported/found by other ways).\n\n"
|
||||
"" % (sum(count), count[0], count[1]))
|
||||
f.write("%s\n%s\n\n" % ("{{Note|Note|Before RC1 (i.e. during regular development of next version in main "
|
||||
"branch), only fixes of issues which already existed in previous official releases are "
|
||||
"listed here. Fixes for regressions introduced since last release, or for new "
|
||||
"features, are '''not''' listed here.<br/>For following RCs and final release, "
|
||||
"'''all''' backported fixes are listed.}}", IGNORE_END_LINE))
|
||||
|
||||
f.write("\n".join(lines))
|
||||
f.write("\n")
|
||||
|
||||
f.write("%s\n\n<hr/>\n\n" % IGNORE_START_LINE)
|
||||
for rst in rstate_list:
|
||||
entries = release_log["__RSTATES__"].get(rst, [])
|
||||
if entries:
|
||||
f.write("== %s ==\n" % rst)
|
||||
f.write("For %s, %d bugs were fixed:\n\n" % (rst, len(entries)))
|
||||
f.write("\n".join(entries))
|
||||
f.write("\n\n")
|
||||
f.write("%s\n" % IGNORE_END_LINE)
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
global __doc__
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
usage_text = __doc__
|
||||
|
||||
epilog = "This script is typically used to help write release notes"
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
parser.add_argument(
|
||||
"--source", dest="source_dir",
|
||||
metavar='PATH', required=True,
|
||||
help="Path to git repository")
|
||||
parser.add_argument(
|
||||
"--range", dest="range_sha1",
|
||||
metavar='SHA1_RANGE', required=False,
|
||||
help="Range to use, eg: 169c95b8..HEAD")
|
||||
parser.add_argument(
|
||||
"--author", dest="author",
|
||||
metavar='AUTHOR', type=str, required=False,
|
||||
help=("Author(s) to filter commits ("))
|
||||
parser.add_argument(
|
||||
"--filter", dest="filter_type",
|
||||
metavar='FILTER', type=str, required=False,
|
||||
help=("Method to filter commits in ['BUGFIX', 'NOISE']"))
|
||||
parser.add_argument(
|
||||
"--filter-exclude-sha1", dest="filter_exclude_sha1_list",
|
||||
default=[], required=False, type=lambda s: s.split(","),
|
||||
help=("Coma-separated list of commits to ignore/skip"))
|
||||
parser.add_argument(
|
||||
"--filter-exclude-sha1-fromfiles", dest="filter_exclude_sha1_filepaths",
|
||||
default="", required=False, nargs='*',
|
||||
help=("One or more text files storing list of commits to ignore/skip"))
|
||||
parser.add_argument(
|
||||
"--filter-exclude-fromreleaselogs", dest="filter_exclude_releaselogs",
|
||||
default="", required=False, nargs='*',
|
||||
help=("One or more text files storing release logs, to ignore/skip their entries "
|
||||
"(based on message comparison, not commit sha1)"))
|
||||
parser.add_argument(
|
||||
"--accept-log", dest="accept_log",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Also output more complete info about accepted commits (summary, author...)"))
|
||||
parser.add_argument(
|
||||
"--accept-pretty", dest="accept_pretty",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Also output pretty-printed accepted commits (nearly ready for WIKI release notes)"))
|
||||
parser.add_argument(
|
||||
"--accept-releaselog", dest="accept_releaselog",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Also output accepted commits as a wiki release log page (adds sorting by categories)"))
|
||||
parser.add_argument(
|
||||
"--blender-rev", dest="blender_rev",
|
||||
default=None, required=False,
|
||||
help=("Blender revision (only used to generate release notes page)"))
|
||||
parser.add_argument(
|
||||
"--blender-rstate", dest="blender_rstate",
|
||||
default="alpha", required=False,
|
||||
help=("Blender release state (like alpha, beta, rc1, final, corr_a, corr_b, etc.), "
|
||||
"each revision will be tagged by given one"))
|
||||
parser.add_argument(
|
||||
"--blender-rstate-list", dest="blender_rstate_list",
|
||||
default="", required=False, type=lambda s: s.split(","),
|
||||
help=("Blender release state(s) to additionally list in their own sections "
|
||||
"(e.g. pass 'RC2' to list fixes between RC1 and RC2, ie tagged as RC2, etc.)"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
for path in args.filter_exclude_sha1_filepaths:
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
args.filter_exclude_sha1_list += [sha1 for l in f for sha1 in l.split()]
|
||||
args.filter_exclude_sha1_list = {sha1.encode() for sha1 in args.filter_exclude_sha1_list}
|
||||
|
||||
messages = set()
|
||||
for path in args.filter_exclude_releaselogs:
|
||||
messages |= release_log_extract_messages(path)
|
||||
args.filter_exclude_releaselogs = messages
|
||||
|
||||
from git_log import GitCommitIter
|
||||
|
||||
# --------------
|
||||
# Filter Commits
|
||||
|
||||
def match(c):
|
||||
# filter_type
|
||||
if not args.filter_type:
|
||||
pass
|
||||
elif args.filter_type == 'BUGFIX':
|
||||
first_line = c.body.split("\n\n")[0].strip(" :.;-\n").replace("\n", " ")
|
||||
assert len(first_line)
|
||||
if any(w for w in first_line.split() if w.lower().startswith(("fix", "bugfix", "bug-fix"))):
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
elif args.filter_type == 'NOISE':
|
||||
first_line = c.body.strip().split("\n")[0]
|
||||
assert len(first_line)
|
||||
if any(w for w in first_line.split() if w.lower().startswith("cleanup")):
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
raise Exception("Filter type %r isn't known" % args.filter_type)
|
||||
|
||||
# author
|
||||
if not args.author:
|
||||
pass
|
||||
elif args.author != c.author:
|
||||
return False
|
||||
|
||||
# commits to exclude
|
||||
if c.sha1 in args.filter_exclude_sha1_list:
|
||||
return False
|
||||
|
||||
# exclude by commit message (because cherry-pick totally breaks relations with original commit...)
|
||||
if args.filter_exclude_releaselogs:
|
||||
if gen_commit_message_pretty(c) in args.filter_exclude_releaselogs:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if args.accept_releaselog:
|
||||
blender_rev = args.blender_rev or "<UNKNOWN>"
|
||||
commits = tuple(GitCommitIter(args.source_dir, args.range_sha1))
|
||||
release_log = release_log_init(ACCEPT_RELEASELOG_FILE, args.source_dir, blender_rev,
|
||||
commits[-1].sha1.decode(), commits[0].sha1.decode(),
|
||||
args.blender_rstate, args.blender_rstate_list)
|
||||
commits = [c for c in commits if match(c)]
|
||||
else:
|
||||
commits = [c for c in GitCommitIter(args.source_dir, args.range_sha1) if match(c)]
|
||||
|
||||
# oldest first
|
||||
commits.reverse()
|
||||
|
||||
tot_accept = 0
|
||||
tot_reject = 0
|
||||
|
||||
def exit_message():
|
||||
print(" Written",
|
||||
colorize(ACCEPT_FILE, color='green'), "(%d)" % tot_accept,
|
||||
colorize(ACCEPT_LOG_FILE, color='yellow'), "(%d)" % tot_accept,
|
||||
colorize(ACCEPT_PRETTY_FILE, color='blue'), "(%d)" % tot_accept,
|
||||
colorize(REJECT_FILE, color='red'), "(%d)" % tot_reject,
|
||||
)
|
||||
|
||||
def get_cat(ch, max_idx):
|
||||
cat = -1
|
||||
try:
|
||||
cat = int(ch)
|
||||
except:
|
||||
pass
|
||||
if 0 <= cat < max_idx:
|
||||
return cat
|
||||
print("Invalid input %r" % ch)
|
||||
return None
|
||||
|
||||
for i, c in enumerate(commits):
|
||||
if os.name == "posix":
|
||||
# Also clears scroll-back.
|
||||
os.system("tput reset")
|
||||
else:
|
||||
print('\x1b[2J') # clear
|
||||
|
||||
sha1 = c.sha1
|
||||
|
||||
# diff may scroll off the screen, that's OK
|
||||
os.system("git --git-dir %s show %s --format=%%n" % (c._git_dir, sha1.decode('ascii')))
|
||||
print("")
|
||||
print_commit(c)
|
||||
sys.stdout.flush()
|
||||
|
||||
accept = False
|
||||
while True:
|
||||
print("Space=" + colorize("Accept", 'green'),
|
||||
"Enter=" + colorize("Skip", 'red'),
|
||||
"Ctrl+C or X=" + colorize("Exit", color='white'),
|
||||
"[%d of %d]" % (i + 1, len(commits)),
|
||||
"(+%d | -%d)" % (tot_accept, tot_reject),
|
||||
)
|
||||
ch = getch()
|
||||
|
||||
if ch == b'\x03' or ch == b'x':
|
||||
# Ctrl+C
|
||||
exit_message()
|
||||
print("Goodbye! (%s)" % c.sha1.decode())
|
||||
return False
|
||||
elif ch == b' ':
|
||||
log_filepath = ACCEPT_FILE
|
||||
log_filepath_log = ACCEPT_LOG_FILE
|
||||
log_filepath_pretty = ACCEPT_PRETTY_FILE
|
||||
tot_accept += 1
|
||||
|
||||
if args.accept_releaselog: # Enter sub-loop for category selection.
|
||||
done_main = True
|
||||
c1 = c2 = None
|
||||
while True:
|
||||
if c1 is None:
|
||||
print("Select main category (V=View all categories, M=Commit message): \n\t%s"
|
||||
"" % " | ".join("[%d] %s" % (i, cat[0]) for i, cat in enumerate(BUGFIX_CATEGORIES)))
|
||||
else:
|
||||
main_cat = BUGFIX_CATEGORIES[c1][0]
|
||||
sub_cats = BUGFIX_CATEGORIES[c1][1]
|
||||
if not sub_cats:
|
||||
break
|
||||
print("[%d] %s: Select sub category "
|
||||
"(V=View all categories, M=Commit message, Enter=No sub-categories, "
|
||||
"Backspace=Select other main category): \n\t%s"
|
||||
"" % (c1, main_cat,
|
||||
" | ".join("[%d] %s" % (i, cat) for i, cat in enumerate(sub_cats))))
|
||||
|
||||
ch = getch()
|
||||
|
||||
if ch == b'\x7f': # backspace
|
||||
done_main = False
|
||||
break
|
||||
elif ch == b'\x03' or ch == b'x':
|
||||
# Ctrl+C
|
||||
exit_message()
|
||||
print("Goodbye! (%s)" % c.sha1.decode())
|
||||
return
|
||||
elif ch == b'v':
|
||||
print_categories_tree()
|
||||
print("")
|
||||
elif ch == b'm':
|
||||
print_commit(c)
|
||||
print("")
|
||||
elif c1 is None:
|
||||
c1 = get_cat(ch, len(BUGFIX_CATEGORIES))
|
||||
elif c2 is None:
|
||||
if ch == b'\r':
|
||||
break
|
||||
elif ch == b'\x7f': # backspace
|
||||
c1 = None
|
||||
continue
|
||||
c2 = get_cat(ch, len(BUGFIX_CATEGORIES[c1][1]))
|
||||
if c2 is not None:
|
||||
break
|
||||
else:
|
||||
print("BUG! this should not happen!")
|
||||
|
||||
if done_main is False:
|
||||
# Go back to main loop, this commit is no more accepted nor rejected.
|
||||
tot_accept -= 1
|
||||
continue
|
||||
|
||||
write_release_log(ACCEPT_RELEASELOG_FILE, release_log, c, (c1, c2),
|
||||
args.blender_rstate, args.blender_rstate_list)
|
||||
break
|
||||
elif ch == b'\r':
|
||||
log_filepath = REJECT_FILE
|
||||
log_filepath_log = None
|
||||
log_filepath_pretty = None
|
||||
tot_reject += 1
|
||||
break
|
||||
else:
|
||||
print("Invalid input %r" % ch)
|
||||
|
||||
with open(log_filepath, 'ab') as f:
|
||||
f.write(sha1 + b'\n')
|
||||
|
||||
if args.accept_pretty and log_filepath_pretty:
|
||||
with open(log_filepath_pretty, 'a') as f:
|
||||
f.write(gen_commit_pretty(c, rstate=args.blender_rstate) + "\n")
|
||||
|
||||
if args.accept_log and log_filepath_log:
|
||||
with open(log_filepath_log, 'a') as f:
|
||||
f.write(gen_commit_log(c) + "\n")
|
||||
|
||||
exit_message()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+294
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Created by Robert Wenzlaff (Det. Thorn).
|
||||
# Oct. 30, 2003
|
||||
|
||||
from tkinter import (
|
||||
Button,
|
||||
Canvas,
|
||||
Checkbutton,
|
||||
END,
|
||||
Frame,
|
||||
IntVar,
|
||||
Label,
|
||||
RIDGE,
|
||||
Text,
|
||||
Tk,
|
||||
)
|
||||
|
||||
color = ("black", "white", "darkgreen", "gray")
|
||||
|
||||
|
||||
class App:
|
||||
|
||||
def __init__(self, master):
|
||||
frame = Frame(master, borderwidth=5)
|
||||
frame.grid(column=0, row=0, pady=5)
|
||||
|
||||
self.state = []
|
||||
self.states = 256
|
||||
self.laststate = 2 # 0=Black, 1=White, 2=Transp.
|
||||
|
||||
self.size = 16
|
||||
self.gridsz = 20
|
||||
|
||||
for x in range(1024):
|
||||
self.state.append(2)
|
||||
|
||||
self.screen = Canvas(frame, height=320, width=320, bg=color[2])
|
||||
self.screen.bind("<Button-1>", self.scrnclick1)
|
||||
self.screen.bind("<Button-3>", self.scrnclick2)
|
||||
self.screen.bind("<B1-Motion>", self.scrndrag)
|
||||
|
||||
for x in range(16):
|
||||
self.screen.create_line((x * 20, 0, x * 20, 320), fill=color[3])
|
||||
self.screen.create_line((0, x * 20, 320, x * 20), fill=color[3])
|
||||
|
||||
self.screen.grid(row=0, column=0, columnspan=5)
|
||||
|
||||
frame2 = Frame(master, borderwidth=5)
|
||||
frame2.grid(column=0, row=1, pady=5)
|
||||
|
||||
self.clear = Button(frame2, text="Clear", command=self.clearit)
|
||||
self.clear.grid(row=0, column=0, pady=20)
|
||||
|
||||
self.doit = Button(frame2, text="Print", command=self.doit)
|
||||
self.doit.grid(row=0, column=1, pady=20)
|
||||
|
||||
#self.doitlab = Label(frame2, text="(Output to stdout)");
|
||||
#self.doitlab.grid(row=1, column=1);
|
||||
|
||||
self.parse = Button(frame2, text="Parse", command=self.parsetext)
|
||||
self.parse.grid(row=0, column=2, pady=20)
|
||||
|
||||
self.large = 0
|
||||
self.dummy = IntVar()
|
||||
self.largeb = Checkbutton(frame2, text="Large", var=self.dummy, command=self.changesize)
|
||||
self.largeb.grid(row=0, column=3, pady=20)
|
||||
|
||||
self.prev = Canvas(frame2, height=17, width=17, bg=color[2], relief=RIDGE)
|
||||
self.prev.grid(row=0, column=4, pady=20, padx=20)
|
||||
|
||||
# DataParsers
|
||||
self.bmlabel = Label(frame2, text="Bitmap Data (paste hex from code)")
|
||||
self.bmlabel.grid(row=2, column=0, columnspan=5, sticky="W")
|
||||
|
||||
self.bmentry = Text(frame2, width=80, height=9, font="Times 8")
|
||||
self.bmentry.bind("<Leave>", self.bmtextpaste)
|
||||
self.bmentry.grid(row=3, column=0, columnspan=5, pady=5)
|
||||
|
||||
self.msklabel = Label(frame2, text="Mask Data (paste hex from code)")
|
||||
self.msklabel.grid(row=4, column=0, columnspan=5, sticky="W")
|
||||
|
||||
self.mskentry = Text(frame2, width=80, height=9, font="Times 8")
|
||||
self.mskentry.bind("<Leave>", self.msktextpaste)
|
||||
self.mskentry.grid(row=5, column=0, columnspan=5, pady=5)
|
||||
|
||||
def changesize(self):
|
||||
self.large = ~self.large
|
||||
if self.large:
|
||||
self.size = 32
|
||||
self.gridsz = 10
|
||||
self.states = 1024
|
||||
oldstate = self.state
|
||||
self.state = []
|
||||
for n in range(1024):
|
||||
col = (n // 2) % 16
|
||||
row = int(n // 64)
|
||||
self.state.append(oldstate[16 * row + col])
|
||||
oldstate = []
|
||||
else:
|
||||
self.size = 16
|
||||
self.gridsz = 20
|
||||
self.states = 256
|
||||
oldstate = self.state
|
||||
self.state = []
|
||||
for n in range(1024):
|
||||
if not ((n % 2) or ((n // 32) % 2)):
|
||||
self.state.append(oldstate[n])
|
||||
for n in range(256, 1024):
|
||||
self.state.append(2)
|
||||
oldstate = []
|
||||
|
||||
# Insert scaling here
|
||||
|
||||
self.updatescrn()
|
||||
self.prev.config(width=self.size + 1, height=self.size + 1)
|
||||
for n in range(self.states):
|
||||
self.updateprev(n)
|
||||
#self.prev.grid(row=0, column=4, padx=self.gridsz, pady=self.gridsz)
|
||||
|
||||
def scrnclick1(self, event):
|
||||
self.scrnclick(event, 1)
|
||||
|
||||
def scrnclick2(self, event):
|
||||
self.scrnclick(event, -1)
|
||||
|
||||
def scrnclick(self, event, direction):
|
||||
if (event.x > 319) or (event.y > 319) or (event.x < 0) or (event.y < 0):
|
||||
return
|
||||
|
||||
n = (event.x // self.gridsz) + self.size * (event.y // self.gridsz)
|
||||
|
||||
self.state[n] += direction
|
||||
self.state[n] %= 3
|
||||
|
||||
row = n % self.size
|
||||
col = n // self.size
|
||||
|
||||
self.screen.create_rectangle((self.gridsz * row + 1,
|
||||
self.gridsz * col + 1,
|
||||
self.gridsz * row + self.gridsz - 1,
|
||||
self.gridsz * col + self.gridsz - 1),
|
||||
fill=color[self.state[n]], outline="")
|
||||
|
||||
self.laststate = self.state[n]
|
||||
self.updateprev(n)
|
||||
|
||||
def scrndrag(self, event):
|
||||
if (event.x > 319) or (event.y > 319) or (event.x < 0) or (event.y < 0):
|
||||
return
|
||||
|
||||
n = (event.x // self.gridsz) + self.size * (event.y // self.gridsz)
|
||||
|
||||
row = n % self.size
|
||||
col = n // self.size
|
||||
|
||||
self.screen.create_rectangle((self.gridsz * row + 1,
|
||||
self.gridsz * col + 1,
|
||||
self.gridsz * row + self.gridsz - 1,
|
||||
self.gridsz * col + self.gridsz - 1),
|
||||
fill=color[self.laststate], outline="")
|
||||
self.state[n] = self.laststate
|
||||
|
||||
self.updateprev(n)
|
||||
|
||||
def updateprev(self, n):
|
||||
x = n % self.size + 1
|
||||
y = n // self.size + 1
|
||||
|
||||
if self.large:
|
||||
pad = 12
|
||||
else:
|
||||
pad = 20
|
||||
|
||||
self.prev.create_line(x + 1, y + 1, x + 2, y + 1, fill=color[self.state[n]])
|
||||
self.prev.grid(row=0, column=4, padx=pad, pady=pad)
|
||||
|
||||
def updatescrn(self):
|
||||
self.screen.create_rectangle(0, 0, 320, 320, fill=color[2])
|
||||
for x in range(self.size):
|
||||
self.screen.create_line((x * self.gridsz, 0, x * self.gridsz, 320), fill=color[3])
|
||||
self.screen.create_line((0, x * self.gridsz, 320, x * self.gridsz), fill=color[3])
|
||||
for n in range(self.states):
|
||||
row = n % self.size
|
||||
col = n // self.size
|
||||
self.screen.create_rectangle((self.gridsz * row + 1,
|
||||
self.gridsz * col + 1,
|
||||
self.gridsz * row + self.gridsz - 1,
|
||||
self.gridsz * col + self.gridsz - 1),
|
||||
fill=color[self.state[n]], outline="")
|
||||
|
||||
def bmtextpaste(self, event):
|
||||
string = self.bmentry.get(1.0, END)
|
||||
self.bmentry.delete(1.0, END)
|
||||
string = string.replace("\t", "")
|
||||
self.bmentry.insert(END, string)
|
||||
|
||||
def msktextpaste(self, event):
|
||||
string = self.mskentry.get(1.0, END)
|
||||
self.mskentry.delete(1.0, END)
|
||||
string = string.replace("\t", "")
|
||||
self.mskentry.insert(END, string)
|
||||
|
||||
def parsetext(self):
|
||||
bmstring = self.bmentry.get(1.0, END)
|
||||
bmstring = bmstring.replace(",", " ")
|
||||
bmstring = bmstring.split()
|
||||
|
||||
mskstring = self.mskentry.get(1.0, END)
|
||||
mskstring = mskstring.replace(",", " ")
|
||||
mskstring = mskstring.split()
|
||||
|
||||
if len(bmstring) != len(mskstring):
|
||||
print("Mismatched data. Bitmap and mask must be same size,")
|
||||
return
|
||||
elif not (len(bmstring) == 32 or len(bmstring) == 128):
|
||||
print("Size Error, input must be 32 or 128 hex bytes. ")
|
||||
return
|
||||
|
||||
for n in range(self.states):
|
||||
self.state[n] = 0
|
||||
|
||||
m = 0
|
||||
for entry in bmstring:
|
||||
e = int(entry, 16)
|
||||
for bit in range(8):
|
||||
self.state[m] = (e & 1)
|
||||
e = e >> 1
|
||||
m += 1
|
||||
|
||||
m = 0
|
||||
for entry in mskstring:
|
||||
e = int(entry, 16)
|
||||
for bit in range(8):
|
||||
if not (e & 1):
|
||||
self.state[m] = 2
|
||||
e = e >> 1
|
||||
m += 1
|
||||
|
||||
self.updatescrn()
|
||||
for n in range(self.states):
|
||||
self.updateprev(n)
|
||||
|
||||
def clearit(self):
|
||||
for n in range(self.states):
|
||||
self.state[n] = 2
|
||||
self.updateprev(n)
|
||||
self.updatescrn()
|
||||
self.bmentry.delete(0.0, END)
|
||||
self.mskentry.delete(0.0, END)
|
||||
|
||||
def doit(self):
|
||||
mask = []
|
||||
bitmap = []
|
||||
numbytes = self.size * self.size // 8
|
||||
for i in range(numbytes):
|
||||
m = 0
|
||||
b = 0
|
||||
for j in range(8):
|
||||
m <<= 1
|
||||
b <<= 1
|
||||
if (self.state[(i * 8) + (7 - j)] != 2):
|
||||
m |= 1
|
||||
if (self.state[(i * 8) + (7 - j)] == 1):
|
||||
b |= 1
|
||||
#print((i * 8) + (7 - j), self.state[(i * 8) + (7 - j)], m)
|
||||
mask.append(m)
|
||||
bitmap.append(b)
|
||||
|
||||
print("\n\nstatic char bitmap[] = {", end=' ')
|
||||
for i in range(numbytes):
|
||||
b1 = bitmap[i]
|
||||
if not (i % 8):
|
||||
print("\n\t", end=' ')
|
||||
print("0x%(b1)02x, " % vars(), end=' ')
|
||||
print("\n};")
|
||||
|
||||
print("\nstatic char mask[] = {", end=' ')
|
||||
for i in range(numbytes):
|
||||
b1 = mask[i]
|
||||
if not (i % 8):
|
||||
print("\n\t", end=' ')
|
||||
print("0x%(b1)02x, " % vars(), end=' ')
|
||||
print("\n};")
|
||||
|
||||
|
||||
################## Main App #######################
|
||||
root = Tk()
|
||||
|
||||
app = App(root)
|
||||
root.title("Cursor Maker")
|
||||
|
||||
root.mainloop()
|
||||
@@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Converts 32x32 XPM images written be the gimp to GL stipples
|
||||
# takes XPM files as arguments, prints out C style definitions.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
xpm_ls = [f for f in sys.argv[1:] if f.lower().endswith(".xpm")]
|
||||
|
||||
print("Converting: " + " ".join(xpm_ls))
|
||||
|
||||
for xpm in xpm_ls:
|
||||
f = open(xpm, "r")
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
# all after first {
|
||||
data = data.split("{", 1)[1]
|
||||
|
||||
# all before first }
|
||||
data = data.rsplit("}", 1)[0]
|
||||
|
||||
data = data.replace("\n", "")
|
||||
|
||||
data = data.split(",")
|
||||
|
||||
w, h, c, dummy = map(int, data[0].strip("\"").split())
|
||||
|
||||
if w != 32 or h != 32 or c != 2:
|
||||
print("Skipping %r, expected 32x32, monochrome, got %s" %
|
||||
(xpm, data[0]))
|
||||
continue
|
||||
|
||||
# col_1 = data[1][1]
|
||||
col_2 = data[2][1]
|
||||
|
||||
data = [d[1:-1] for d in data[3:]]
|
||||
|
||||
bits = []
|
||||
|
||||
for d in data:
|
||||
for i, c in enumerate(d):
|
||||
bits.append('01'[(c == col_2)])
|
||||
|
||||
if len(bits) != 1024:
|
||||
print("Skipping %r, expected 1024 bits, got %d" %
|
||||
(xpm, len(bits)))
|
||||
continue
|
||||
|
||||
bits = "".join(bits)
|
||||
|
||||
chars = []
|
||||
|
||||
for i in range(0, len(bits), 8):
|
||||
chars.append("0x%.2x" % int(bits[i:i + 8], 2))
|
||||
|
||||
fout = sys.stdout
|
||||
|
||||
var = os.path.basename(xpm)
|
||||
var = os.path.splitext(var)[0]
|
||||
|
||||
fout.write("GLubyte stipple_%s[128] {\n\t" % var)
|
||||
for i, c in enumerate(chars):
|
||||
if i != 127:
|
||||
fout.write("%s, " % c)
|
||||
else:
|
||||
fout.write("%s" % c)
|
||||
|
||||
if not ((i + 1) % 8):
|
||||
fout.write("\n\t")
|
||||
fout.write("};\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,143 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
'''
|
||||
Created compact byte arrays which can be decoded into 2D shapes.
|
||||
(See 'GPU_batch_from_poly_2d_encoded').
|
||||
|
||||
- Objects must use the prefix "shape_"
|
||||
- Meshes and Curves are supported as input.
|
||||
- C and Python output is written to "output/"
|
||||
|
||||
The format is simple: a series of (X, Y) locations one byte each.
|
||||
Repeating the same value terminates the polygon, moving onto the next.
|
||||
|
||||
Example Use::
|
||||
|
||||
blender.bin -b --factory-startup my_shapes.blend --python make_shape_2d_from_blend.py
|
||||
'''
|
||||
import bpy
|
||||
import os
|
||||
|
||||
USE_C_STYLE = True
|
||||
USE_PY_STYLE = True
|
||||
|
||||
WRAP_LIMIT = 79
|
||||
TAB_WIDTH = 4
|
||||
|
||||
SUBDIR = "output"
|
||||
|
||||
|
||||
def float_to_ubyte(f):
|
||||
return max(0, min(255, int(round(f * 255.0))))
|
||||
|
||||
|
||||
def curve_to_loops(ob):
|
||||
import bmesh
|
||||
cu = ob.data
|
||||
|
||||
me = ob.to_mesh()
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(me)
|
||||
me = ob.to_mesh_clear()
|
||||
|
||||
bmesh.ops.beautify_fill(bm, faces=bm.faces, edges=bm.edges)
|
||||
|
||||
edges = bm.edges[:]
|
||||
edges.sort(key=lambda e: e.calc_length(), reverse=True)
|
||||
|
||||
for e in edges:
|
||||
if e.is_manifold:
|
||||
f_a, f_b = [f for f in e.link_faces]
|
||||
bmesh.utils.face_join((f_a, f_b), False)
|
||||
|
||||
edges = bm.edges[:]
|
||||
for e in edges:
|
||||
if e.is_wire:
|
||||
bm.edges.remove(e)
|
||||
|
||||
bm.normal_update()
|
||||
|
||||
data_all = []
|
||||
for f in bm.faces:
|
||||
points = []
|
||||
# Ensure all faces are pointing the correct direction
|
||||
# Note, we may want to use polygon sign for a second color
|
||||
# (via the material index).
|
||||
if f.normal.z > 0.0:
|
||||
loops = f.loops
|
||||
else:
|
||||
loops = reversed(f.loops)
|
||||
for l in loops:
|
||||
points.append(
|
||||
tuple(float_to_ubyte(axis) for axis in l.vert.co.xy)
|
||||
)
|
||||
data_all.append((points, f.material_index))
|
||||
bm.free()
|
||||
return data_all
|
||||
|
||||
|
||||
def write_c(ob):
|
||||
cu = ob.data
|
||||
name = ob.name
|
||||
with open(os.path.join(SUBDIR, name + ".c"), 'w') as fh:
|
||||
fw = fh.write
|
||||
fw(f"/* {name} */\n")
|
||||
fw(f"const uchar {name}[] = {{")
|
||||
line_len = WRAP_LIMIT
|
||||
line_is_first = True
|
||||
array_len = 0
|
||||
data_all = curve_to_loops(ob)
|
||||
for (points, material_index) in data_all:
|
||||
# TODO, material_index
|
||||
for p in points + [points[-1]]:
|
||||
line_len += 12
|
||||
if line_len >= WRAP_LIMIT:
|
||||
fw("\n\t")
|
||||
line_len = TAB_WIDTH
|
||||
line_is_first = True
|
||||
if not line_is_first:
|
||||
fw(" ")
|
||||
fw(", ".join([f"0x{axis:02x}" for axis in p]) + ",")
|
||||
line_is_first = False
|
||||
array_len += (len(points) + 1) * 2
|
||||
fw("\n};\n")
|
||||
# fw(f"const int data_len = {array_len}\n")
|
||||
|
||||
|
||||
def write_py(ob):
|
||||
cu = ob.data
|
||||
name = ob.name
|
||||
with open(os.path.join(SUBDIR, name + ".py"), 'w') as fh:
|
||||
fw = fh.write
|
||||
fw(f"# {name}\n")
|
||||
fw("data = (")
|
||||
line_len = WRAP_LIMIT
|
||||
fw = fh.write
|
||||
data_all = curve_to_loops(ob)
|
||||
for (points, material_index) in data_all:
|
||||
# TODO, material_index
|
||||
for p in points + [points[-1]]:
|
||||
line_len += 8
|
||||
if line_len >= WRAP_LIMIT:
|
||||
if p is not points[0]:
|
||||
fw("'")
|
||||
fw("\n b'")
|
||||
line_len = 6
|
||||
fw("".join([f"\\x{axis:02x}" for axis in p]))
|
||||
fw("'\n)\n")
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(SUBDIR, exist_ok=True)
|
||||
for ob in bpy.data.objects:
|
||||
if ob.type not in {'MESH', 'CURVE'}:
|
||||
continue
|
||||
if not ob.name.startswith('shape_'):
|
||||
continue
|
||||
if USE_C_STYLE:
|
||||
write_c(ob)
|
||||
if USE_PY_STYLE:
|
||||
write_py(ob)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user