diff options
Diffstat (limited to 'uv_magic_uv/common.py')
-rw-r--r-- | uv_magic_uv/common.py | 534 |
1 files changed, 522 insertions, 12 deletions
diff --git a/uv_magic_uv/common.py b/uv_magic_uv/common.py index 475efd59..bad88167 100644 --- a/uv_magic_uv/common.py +++ b/uv_magic_uv/common.py @@ -20,19 +20,71 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from collections import defaultdict from pprint import pprint from math import fabs, sqrt +import os import bpy from mathutils import Vector import bmesh -DEBUG = False +__all__ = [ + 'is_console_mode', + 'is_debug_mode', + 'enable_debugg_mode', + 'disable_debug_mode', + 'debug_print', + 'check_version', + 'redraw_all_areas', + 'get_space', + 'mouse_on_region', + 'mouse_on_area', + 'mouse_on_regions', + 'create_bmesh', + 'create_new_uv_map', + 'get_island_info', + 'get_island_info_from_bmesh', + 'get_island_info_from_faces', + 'get_uvimg_editor_board_size', + 'calc_polygon_2d_area', + 'calc_polygon_3d_area', + 'measure_mesh_area', + 'measure_uv_area', + 'diff_point_to_segment', + 'get_loop_sequences', + 'get_overlapped_uv_info', + 'get_flipped_uv_info', +] + + +__DEBUG_MODE = True + + +def is_console_mode(): + if "MUV_CONSOLE_MODE" not in os.environ: + return False + return os.environ["MUV_CONSOLE_MODE"] == "True" + + +def is_debug_mode(): + return __DEBUG_MODE + + +def enable_debugg_mode(): + # pylint: disable=W0603 + global __DEBUG_MODE + __DEBUG_MODE = True + + +def disable_debug_mode(): + # pylint: disable=W0603 + global __DEBUG_MODE + __DEBUG_MODE = False def debug_print(*s): @@ -40,7 +92,7 @@ def debug_print(*s): Print message to console in debugging mode """ - if DEBUG: + if is_debug_mode(): pprint(s) @@ -91,6 +143,71 @@ def get_space(area_type, region_type, space_type): return (area, region, space) +def mouse_on_region(event, area_type, region_type): + pos = Vector((event.mouse_x, event.mouse_y)) + + _, region, _ = get_space(area_type, region_type, "") + if region is None: + return False + + if (pos.x > region.x) and (pos.x < region.x + region.width) and \ + (pos.y > region.y) and (pos.y < region.y + region.height): + return True + + return False + + +def mouse_on_area(event, area_type): + pos = Vector((event.mouse_x, event.mouse_y)) + + area, _, _ = get_space(area_type, "", "") + if area is None: + return False + + if (pos.x > area.x) and (pos.x < area.x + area.width) and \ + (pos.y > area.y) and (pos.y < area.y + area.height): + return True + + return False + + +def mouse_on_regions(event, area_type, regions): + if not mouse_on_area(event, area_type): + return False + + for region in regions: + result = mouse_on_region(event, area_type, region) + if result: + return True + + return False + + +def create_bmesh(obj): + bm = bmesh.from_edit_mesh(obj.data) + if check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + return bm + + +def create_new_uv_map(obj, name=None): + uv_maps_old = {l.name for l in obj.data.uv_layers} + bpy.ops.mesh.uv_texture_add() + uv_maps_new = {l.name for l in obj.data.uv_layers} + diff = uv_maps_new - uv_maps_old + + if not list(diff): + return None # no more UV maps can not be created + + # rename UV map + new = obj.data.uv_layers[list(diff)[0]] + if name: + new.name = name + + return new + + def __get_island_info(uv_layer, islands): """ get information about each island @@ -273,7 +390,7 @@ def measure_mesh_area(obj): return mesh_area -def measure_uv_area(obj): +def measure_uv_area(obj, tex_size=None): bm = bmesh.from_edit_mesh(obj.data) if check_version(2, 73, 0) >= 0: bm.verts.ensure_lookup_table() @@ -296,22 +413,38 @@ def measure_uv_area(obj): uvs = [l[uv_layer].uv for l in f.loops] f_uv_area = calc_polygon_2d_area(uvs) - if not tex_layer: - return None - img = f[tex_layer].image - # not found, try to search from node + # user specified + if tex_size: + uv_area = uv_area + f_uv_area * tex_size[0] * tex_size[1] + continue + + # try to find from texture_layer + img = None + if tex_layer: + img = f[tex_layer].image + + # not found, then try to search from node if not img: for mat in obj.material_slots: + if not mat.material.node_tree: + continue for node in mat.material.node_tree.nodes: tex_node_types = [ 'TEX_ENVIRONMENT', 'TEX_IMAGE', ] - if (node.type in tex_node_types) and node.image: - img = node.image + if node.type not in tex_node_types: + continue + if not node.image: + continue + img = node.image + + # can not find from node, so we can not get texture size if not img: return None - uv_area = uv_area + f_uv_area * img.size[0] * img.size[1] + + img_size = img.size + uv_area = uv_area + f_uv_area * img_size[0] * img_size[1] return uv_area @@ -602,3 +735,380 @@ def get_loop_sequences(bm, uv_layer, closed=False): return None, err return loop_seqs, "" + + +def __is_segment_intersect(start1, end1, start2, end2): + seg1 = end1 - start1 + seg2 = end2 - start2 + + a1 = -seg1.y + b1 = seg1.x + d1 = -(a1 * start1.x + b1 * start1.y) + + a2 = -seg2.y + b2 = seg2.x + d2 = -(a2 * start2.x + b2 * start2.y) + + seg1_line2_start = a2 * start1.x + b2 * start1.y + d2 + seg1_line2_end = a2 * end1.x + b2 * end1.y + d2 + + seg2_line1_start = a1 * start2.x + b1 * start2.y + d1 + seg2_line1_end = a1 * end2.x + b1 * end2.y + d1 + + if (seg1_line2_start * seg1_line2_end >= 0) or \ + (seg2_line1_start * seg2_line1_end >= 0): + return False, None + + u = seg1_line2_start / (seg1_line2_start - seg1_line2_end) + out = start1 + u * seg1 + + return True, out + + +class RingBuffer: + def __init__(self, arr): + self.__buffer = arr.copy() + self.__pointer = 0 + + def __repr__(self): + return repr(self.__buffer) + + def __len__(self): + return len(self.__buffer) + + def insert(self, val, offset=0): + self.__buffer.insert(self.__pointer + offset, val) + + def head(self): + return self.__buffer[0] + + def tail(self): + return self.__buffer[-1] + + def get(self, offset=0): + size = len(self.__buffer) + val = self.__buffer[(self.__pointer + offset) % size] + return val + + def next(self): + size = len(self.__buffer) + self.__pointer = (self.__pointer + 1) % size + + def reset(self): + self.__pointer = 0 + + def find(self, obj): + try: + idx = self.__buffer.index(obj) + except ValueError: + return None + return self.__buffer[idx] + + def find_and_next(self, obj): + size = len(self.__buffer) + idx = self.__buffer.index(obj) + self.__pointer = (idx + 1) % size + + def find_and_set(self, obj): + idx = self.__buffer.index(obj) + self.__pointer = idx + + def as_list(self): + return self.__buffer.copy() + + def reverse(self): + self.__buffer.reverse() + self.reset() + + +# clip: reference polygon +# subject: tested polygon +def __do_weiler_atherton_cliping(clip, subject, uv_layer, mode): + + clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops]) + if __is_polygon_flipped(clip_uvs): + clip_uvs.reverse() + subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops]) + if __is_polygon_flipped(subject_uvs): + subject_uvs.reverse() + + debug_print("===== Clip UV List =====") + debug_print(clip_uvs) + debug_print("===== Subject UV List =====") + debug_print(subject_uvs) + + # check if clip and subject is overlapped completely + if __is_polygon_same(clip_uvs, subject_uvs): + polygons = [subject_uvs.as_list()] + debug_print("===== Polygons Overlapped Completely =====") + debug_print(polygons) + return True, polygons + + # check if subject is in clip + if __is_points_in_polygon(subject_uvs, clip_uvs): + polygons = [subject_uvs.as_list()] + return True, polygons + + # check if clip is in subject + if __is_points_in_polygon(clip_uvs, subject_uvs): + polygons = [subject_uvs.as_list()] + return True, polygons + + # check if clip and subject is overlapped partially + intersections = [] + while True: + subject_uvs.reset() + while True: + uv_start1 = clip_uvs.get() + uv_end1 = clip_uvs.get(1) + uv_start2 = subject_uvs.get() + uv_end2 = subject_uvs.get(1) + intersected, point = __is_segment_intersect(uv_start1, uv_end1, + uv_start2, uv_end2) + if intersected: + clip_uvs.insert(point, 1) + subject_uvs.insert(point, 1) + intersections.append([point, + [clip_uvs.get(), clip_uvs.get(1)]]) + subject_uvs.next() + if subject_uvs.get() == subject_uvs.head(): + break + clip_uvs.next() + if clip_uvs.get() == clip_uvs.head(): + break + + debug_print("===== Intersection List =====") + debug_print(intersections) + + # no intersection, so subject and clip is not overlapped + if not intersections: + return False, None + + def get_intersection_pair(intersects, key): + for sect in intersects: + if sect[0] == key: + return sect[1] + + return None + + # make enter/exit pair + subject_uvs.reset() + subject_entering = [] + subject_exiting = [] + clip_entering = [] + clip_exiting = [] + intersect_uv_list = [] + while True: + pair = get_intersection_pair(intersections, subject_uvs.get()) + if pair: + sub = subject_uvs.get(1) - subject_uvs.get(-1) + inter = pair[1] - pair[0] + cross = sub.x * inter.y - inter.x * sub.y + if cross < 0: + subject_entering.append(subject_uvs.get()) + clip_exiting.append(subject_uvs.get()) + else: + subject_exiting.append(subject_uvs.get()) + clip_entering.append(subject_uvs.get()) + intersect_uv_list.append(subject_uvs.get()) + + subject_uvs.next() + if subject_uvs.get() == subject_uvs.head(): + break + + debug_print("===== Enter List =====") + debug_print(clip_entering) + debug_print(subject_entering) + debug_print("===== Exit List =====") + debug_print(clip_exiting) + debug_print(subject_exiting) + + # for now, can't handle the situation when fulfill all below conditions + # * two faces have common edge + # * each face is intersected + # * Show Mode is "Part" + # so for now, ignore this situation + if len(subject_entering) != len(subject_exiting): + if mode == 'FACE': + polygons = [subject_uvs.as_list()] + return True, polygons + return False, None + + def traverse(current_list, entering, exiting, p, current, other_list): + result = current_list.find(current) + if not result: + return None + if result != current: + print("Internal Error") + return None + + # enter + if entering.count(current) >= 1: + entering.remove(current) + + current_list.find_and_next(current) + current = current_list.get() + + while exiting.count(current) == 0: + p.append(current.copy()) + current_list.find_and_next(current) + current = current_list.get() + + # exit + p.append(current.copy()) + exiting.remove(current) + + other_list.find_and_set(current) + return other_list.get() + + # Traverse + polygons = [] + current_uv_list = subject_uvs + other_uv_list = clip_uvs + current_entering = subject_entering + current_exiting = subject_exiting + + poly = [] + current_uv = current_entering[0] + + while True: + current_uv = traverse(current_uv_list, current_entering, + current_exiting, poly, current_uv, other_uv_list) + + if current_uv_list == subject_uvs: + current_uv_list = clip_uvs + other_uv_list = subject_uvs + current_entering = clip_entering + current_exiting = clip_exiting + debug_print("-- Next: Clip --") + else: + current_uv_list = subject_uvs + other_uv_list = clip_uvs + current_entering = subject_entering + current_exiting = subject_exiting + debug_print("-- Next: Subject --") + + debug_print(clip_entering) + debug_print(clip_exiting) + debug_print(subject_entering) + debug_print(subject_exiting) + + if not clip_entering and not clip_exiting \ + and not subject_entering and not subject_exiting: + break + + polygons.append(poly) + + debug_print("===== Polygons Overlapped Partially =====") + debug_print(polygons) + + return True, polygons + + +def __is_polygon_flipped(points): + area = 0.0 + for i in range(len(points)): + uv1 = points.get(i) + uv2 = points.get(i + 1) + a = uv1.x * uv2.y - uv1.y * uv2.x + area = area + a + if area < 0: + # clock-wise + return True + return False + + +def __is_point_in_polygon(point, subject_points): + count = 0 + for i in range(len(subject_points)): + uv_start1 = subject_points.get(i) + uv_end1 = subject_points.get(i + 1) + uv_start2 = point + uv_end2 = Vector((1000000.0, point.y)) + intersected, _ = __is_segment_intersect(uv_start1, uv_end1, + uv_start2, uv_end2) + if intersected: + count = count + 1 + + return count % 2 + + +def __is_points_in_polygon(points, subject_points): + for i in range(len(points)): + internal = __is_point_in_polygon(points.get(i), subject_points) + if not internal: + return False + + return True + + +def get_overlapped_uv_info(bm, faces, uv_layer, mode): + # at first, check island overlapped + isl = get_island_info_from_faces(bm, faces, uv_layer) + overlapped_isl_pairs = [] + for i, i1 in enumerate(isl): + for i2 in isl[i + 1:]: + if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \ + (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y): + continue + overlapped_isl_pairs.append([i1, i2]) + + # next, check polygon overlapped + overlapped_uvs = [] + for oip in overlapped_isl_pairs: + for clip in oip[0]["faces"]: + f_clip = clip["face"] + for subject in oip[1]["faces"]: + f_subject = subject["face"] + + # fast operation, apply bounding box algorithm + if (clip["max_uv"].x < subject["min_uv"].x) or \ + (subject["max_uv"].x < clip["min_uv"].x) or \ + (clip["max_uv"].y < subject["min_uv"].y) or \ + (subject["max_uv"].y < clip["min_uv"].y): + continue + + # slow operation, apply Weiler-Atherton cliping algorithm + result, polygons = __do_weiler_atherton_cliping(f_clip, + f_subject, + uv_layer, mode) + if result: + subject_uvs = [l[uv_layer].uv.copy() + for l in f_subject.loops] + overlapped_uvs.append({"clip_face": f_clip, + "subject_face": f_subject, + "subject_uvs": subject_uvs, + "polygons": polygons}) + + return overlapped_uvs + + +def get_flipped_uv_info(faces, uv_layer): + flipped_uvs = [] + for f in faces: + polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops]) + if __is_polygon_flipped(polygon): + uvs = [l[uv_layer].uv.copy() for l in f.loops] + flipped_uvs.append({"face": f, "uvs": uvs, + "polygons": [polygon.as_list()]}) + + return flipped_uvs + + +def __is_polygon_same(points1, points2): + if len(points1) != len(points2): + return False + + pts1 = points1.as_list() + pts2 = points2.as_list() + + for p1 in pts1: + for p2 in pts2: + diff = p2 - p1 + if diff.length < 0.0000001: + pts2.remove(p2) + break + else: + return False + + return True |