Merge branch 'master' into xr-actions-D9124
|
@ -21,7 +21,7 @@ bl_info = {
|
|||
"name": "Grease Pencil Tools",
|
||||
"description": "Extra tools for Grease Pencil",
|
||||
"author": "Samuel Bernou, Antonio Vazquez, Daniel Martinez Lara, Matias Mendiola",
|
||||
"version": (1, 1, 3),
|
||||
"version": (1, 1, 5),
|
||||
"blender": (2, 91, 0),
|
||||
"location": "Sidebar > Grease Pencil > Grease Pencil Tools",
|
||||
"warning": "",
|
||||
|
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1,35 +1,193 @@
|
|||
import bpy
|
||||
import re
|
||||
import ssl
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
def unzip(zip_path, extract_dir_path):
|
||||
'''Get a zip path and a directory path to extract to'''
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(extract_dir_path)
|
||||
|
||||
def simple_dl_url(url, dest, fallback_url=None):
|
||||
## need to import urlib.request or linux module does not found 'request' using urllib directly
|
||||
## need to create an SSl context or linux fail returning unverified ssl
|
||||
# ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
try:
|
||||
urllib.request.urlretrieve(url, dest)
|
||||
except Exception as e:
|
||||
print('Error trying to download\n', e)
|
||||
if fallback_url:
|
||||
print('\nDownload page for manual install:', fallback_url)
|
||||
return e
|
||||
|
||||
def download_url(url, dest):
|
||||
'''download passed url to dest file (include filename)'''
|
||||
import shutil
|
||||
import time
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(url) as response, open(dest, 'wb') as out_file:
|
||||
shutil.copyfileobj(response, out_file)
|
||||
except Exception as e:
|
||||
print('Error trying to download\n', e)
|
||||
return e
|
||||
|
||||
print(f"Download time {time.time() - start_time:.2f}s",)
|
||||
|
||||
|
||||
def get_brushes(blend_fp):
|
||||
cur_brushes = [b.name for b in bpy.data.brushes]
|
||||
with bpy.data.libraries.load(str(blend_fp), link=False) as (data_from, data_to):
|
||||
# load brushes starting with 'tex' prefix if there are not already there
|
||||
data_to.brushes = [b for b in data_from.brushes if b.startswith('tex_') and not b in cur_brushes]
|
||||
# Add holdout
|
||||
if 'z_holdout' in data_from.brushes and not 'z_holdout' in cur_brushes:
|
||||
data_to.brushes.append('z_holdout')
|
||||
|
||||
## force fake user for the brushes
|
||||
for b in data_to.brushes:
|
||||
b.use_fake_user = True
|
||||
|
||||
return len(data_to.brushes)
|
||||
|
||||
class GP_OT_install_brush_pack(bpy.types.Operator):
|
||||
bl_idname = "gp.import_brush_pack"
|
||||
bl_label = "Import texture brush pack"
|
||||
bl_description = "import Grease Pencil brush pack from Grease Pencil addon"
|
||||
bl_label = "Download and import texture brush pack"
|
||||
bl_description = "Download and import Grease Pencil brush pack from the web (~3.7 Mo)"
|
||||
bl_options = {"REGISTER", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
from pathlib import Path
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return True
|
||||
|
||||
blendname = 'Official_GP_brush_pack_V1.blend'
|
||||
blend_fp = Path(__file__).parent / blendname
|
||||
print('blend_fp: ', blend_fp)
|
||||
|
||||
cur_brushes = [b.name for b in bpy.data.brushes]
|
||||
with bpy.data.libraries.load(str(blend_fp), link=False) as (data_from, data_to):
|
||||
# load brushes starting with 'tex' prefix if there are not already there
|
||||
data_to.brushes = [b for b in data_from.brushes if b.startswith('tex_') and not b in cur_brushes]
|
||||
# Add holdout
|
||||
if 'z_holdout' in data_from.brushes:
|
||||
data_to.brushes.append('z_holdout')
|
||||
|
||||
brush_count = len(data_to.brushes)
|
||||
## force fake user for the brushes
|
||||
for b in data_to.brushes:
|
||||
b.use_fake_user = True
|
||||
|
||||
if brush_count:
|
||||
self.report({'INFO'}, f'{brush_count} brushes installed')
|
||||
def _append_brushes(self, blend_fp):
|
||||
bct = get_brushes(blend_fp)
|
||||
if bct:
|
||||
self.report({'INFO'}, f'{bct} brushes installed')
|
||||
else:
|
||||
self.report({'WARNING'}, 'Brushes already loaded')
|
||||
|
||||
def _install_from_zip(self):
|
||||
## get blend file name
|
||||
blendname = None
|
||||
with zipfile.ZipFile(self.brushzip, 'r') as zfd:
|
||||
for f in zfd.namelist():
|
||||
if f.endswith('.blend'):
|
||||
blendname = f
|
||||
break
|
||||
if not blendname:
|
||||
self.report({'ERROR'}, f'blend file not found in zip {self.brushzip}')
|
||||
return
|
||||
|
||||
unzip(self.brushzip, self.temp)
|
||||
|
||||
self._append_brushes(Path(self.temp) / blendname)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
import tempfile
|
||||
import json
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
## get temp dir
|
||||
temp = tempfile.gettempdir()
|
||||
if not temp:
|
||||
self.report({'ERROR'}, 'no os temporary directory found to download brush pack (using python tempfile.gettempdir())')
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.temp = Path(temp)
|
||||
|
||||
## download link from gitlab
|
||||
# brush pack project https://gitlab.com/pepe-school-land/gp-brush-pack
|
||||
repo_url = r'https://gitlab.com/api/v4/projects/21994857'
|
||||
tree_url = f'{repo_url}/repository/tree'
|
||||
|
||||
## need to create an SSl context or linux fail and raise unverified ssl
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(tree_url) as response:
|
||||
html = response.read()
|
||||
except:
|
||||
## try loading from tempdir
|
||||
packs = [f for f in os.listdir(self.temp) if 'GP_brush_pack' in f and f.endswith('.blend')]
|
||||
if packs:
|
||||
packs.sort()
|
||||
self._append_brushes(Path(self.temp) / packs[-1])
|
||||
self.report({'WARNING'}, 'Brushes loaded from temp directory (No download)')
|
||||
# print('Could not reach web url : Brushes were loaded from temp directory file (No download)')
|
||||
return {"FINISHED"}
|
||||
|
||||
self.report({'ERROR'}, f'Check your internet connexion, Impossible to connect to url: {tree_url}')
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not html:
|
||||
self.report({'ERROR'}, f'No response read from: {tree_url}')
|
||||
return {"CANCELLED"}
|
||||
|
||||
tree_dic = json.loads(html)
|
||||
zips = [fi for fi in tree_dic if fi['type'] == 'blob' and fi['name'].endswith('.zip')]
|
||||
|
||||
if not zips:
|
||||
print(f'no zip file found in {tree_url}')
|
||||
return {"CANCELLED"}
|
||||
|
||||
## sort by name to get last
|
||||
zips.sort(key=lambda x: x['name'])
|
||||
last_zip = zips[-1]
|
||||
zipname = last_zip['name']
|
||||
id_num = last_zip['id']
|
||||
|
||||
|
||||
## url by filename
|
||||
# filepath_encode = urllib.parse.quote(zipname, safe='')# need safe to convert possible '/'
|
||||
# dl_url = f'{repo_url}/repository/files/{filepath_encode}/raw?ref=master'
|
||||
|
||||
## url by blobs
|
||||
dl_url = f"{repo_url}/repository/blobs/{id_num}/raw"
|
||||
|
||||
self.brushzip = self.temp / zipname
|
||||
|
||||
|
||||
### Load existing files instead of redownloading if exists and up to date (same hash)
|
||||
if self.brushzip.exists():
|
||||
### Test the hash against online git hash (check for update)
|
||||
BLOCK_SIZE = 524288# 512 Kb buf size
|
||||
file_hash = hashlib.sha1()
|
||||
file_hash.update(("blob %u\0" % os.path.getsize(self.brushzip)).encode('utf-8'))
|
||||
with open(self.brushzip, 'rb') as f:
|
||||
fb = f.read(BLOCK_SIZE)
|
||||
while len(fb) > 0:
|
||||
file_hash.update(fb)
|
||||
fb = f.read(BLOCK_SIZE)
|
||||
|
||||
if file_hash.hexdigest() == id_num: # same git SHA1
|
||||
## is up to date, install
|
||||
print(f'{self.brushzip} is up do date, appending brushes')
|
||||
self._install_from_zip()
|
||||
return {"FINISHED"}
|
||||
|
||||
## Download, unzip, use blend
|
||||
print(f'Downloading brushpack in {self.brushzip}')
|
||||
## https://cloud.blender.org/p/gallery/5f235cc297f8815e74ffb90b
|
||||
|
||||
fallback_url='https://gitlab.com/pepe-school-land/gp-brush-pack/-/blob/master/Official_GP_brush_pack_v01.zip'
|
||||
err = simple_dl_url(dl_url, str(self.brushzip), fallback_url)
|
||||
# err = download_url(dl_url, str(self.brushzip), fallback_url)
|
||||
|
||||
if err:
|
||||
self.report({'ERROR'}, 'Could not download brush pack. Check your internet connection. (see console for detail)')
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
print('Done')
|
||||
self._install_from_zip()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# This script is Free software. Please share and reuse.
|
||||
# ♡2010-2019 Adam Dominec <adominec@gmail.com>
|
||||
# ♡2010-2020 Adam Dominec <adominec@gmail.com>
|
||||
|
||||
## Code structure
|
||||
# This file consists of several components, in this order:
|
||||
|
@ -12,8 +12,8 @@
|
|||
bl_info = {
|
||||
"name": "Export Paper Model",
|
||||
"author": "Addam Dominec",
|
||||
"version": (1, 1),
|
||||
"blender": (2, 80, 0),
|
||||
"version": (1, 2),
|
||||
"blender": (2, 83, 0),
|
||||
"location": "File > Export > Paper Model",
|
||||
"warning": "",
|
||||
"description": "Export printable net of the active mesh",
|
||||
|
@ -22,21 +22,16 @@ bl_info = {
|
|||
}
|
||||
|
||||
# Task: split into four files (SVG and PDF separately)
|
||||
# does any portion of baking belong into the export module?
|
||||
# sketch out the code for GCODE and two-sided export
|
||||
# * does any portion of baking belong into the export module?
|
||||
# * sketch out the code for GCODE and two-sided export
|
||||
|
||||
# TODO:
|
||||
# sanitize the constructors Edge, Face, UVFace so that they don't edit their parent object
|
||||
# The Exporter classes should take parameters as a whole pack, and parse it themselves
|
||||
# remember objects selected before baking (except selected to active)
|
||||
# add 'estimated number of pages' to the export UI
|
||||
# QuickSweepline is very much broken -- it throws GeometryError for all nets > ~15 faces
|
||||
# rotate islands to minimize area -- and change that only if necessary to fill the page size
|
||||
# Sticker.vertices should be of type Vector
|
||||
|
||||
# check conflicts in island naming and either:
|
||||
# * append a number to the conflicting names or
|
||||
# * enumerate faces uniquely within all islands of the same name (requires a check that both label and abbr. equals)
|
||||
# * append a number to the conflicting names or
|
||||
# * enumerate faces uniquely within all islands of the same name (requires a check that both label and abbr. equals)
|
||||
|
||||
import bpy
|
||||
import bl_operators
|
||||
|
@ -127,7 +122,6 @@ def cage_fit(points, aspect):
|
|||
rot_polygon = [rot @ p for p in polygon]
|
||||
left, right = [fn(rot_polygon, key=lambda p: p.to_tuple()) for fn in (min, max)]
|
||||
bottom, top = [fn(rot_polygon, key=lambda p: p.yx.to_tuple()) for fn in (min, max)]
|
||||
#print(f"{rot_polygon.index(left)}-{rot_polygon.index(right)}, {rot_polygon.index(bottom)}-{rot_polygon.index(top)}")
|
||||
horz, vert = right - left, top - bottom
|
||||
# solve (rot * a).y == (rot * b).y
|
||||
yield max(aspect * horz.x, vert.y), sinx, cosx
|
||||
|
@ -143,9 +137,7 @@ def cage_fit(points, aspect):
|
|||
rot = M.Matrix(((cosy, -siny), (siny, cosy)))
|
||||
for p in rot_polygon:
|
||||
p[:] = rot @ p # note: this also modifies left, right, bottom, top
|
||||
#print(f"solve {aspect * (right - left).x} == {(top - bottom).y} with aspect = {aspect}")
|
||||
if left.x < right.x and bottom.y < top.y and all(left.x <= p.x <= right.x and bottom.y <= p.y <= top.y for p in rot_polygon):
|
||||
#print(f"yield {max(aspect * (right - left).x, (top - bottom).y)}")
|
||||
yield max(aspect * (right - left).x, (top - bottom).y), sinx*cosy + cosx*siny, cosx*cosy - sinx*siny
|
||||
polygon = [points[i] for i in M.geometry.convex_hull_2d(points)]
|
||||
height, sinx, cosx = min(guesses(polygon))
|
||||
|
@ -166,6 +158,16 @@ def create_blank_image(image_name, dimensions, alpha=1):
|
|||
return image
|
||||
|
||||
|
||||
def store_rna_properties(*datablocks):
|
||||
return [{prop.identifier: getattr(data, prop.identifier) for prop in data.rna_type.properties if not prop.is_readonly} for data in datablocks]
|
||||
|
||||
|
||||
def apply_rna_properties(memory, *datablocks):
|
||||
for recall, data in zip(memory, datablocks):
|
||||
for key, value in recall.items():
|
||||
setattr(data, key, value)
|
||||
|
||||
|
||||
class UnfoldError(ValueError):
|
||||
def mesh_select(self):
|
||||
if len(self.args) > 1:
|
||||
|
@ -217,7 +219,7 @@ class Unfolder:
|
|||
# Note about scale: input is directly in blender length
|
||||
# Mesh.scale_islands multiplies everything by a user-defined ratio
|
||||
# exporters (SVG or PDF) multiply everything by 1000 (output in millimeters)
|
||||
Exporter = SVG if properties.file_format == 'SVG' else PDF
|
||||
Exporter = Svg if properties.file_format == 'SVG' else Pdf
|
||||
filepath = properties.filepath
|
||||
extension = properties.file_format.lower()
|
||||
filepath = bpy.path.ensure_ext(filepath, "." + extension)
|
||||
|
@ -249,11 +251,9 @@ class Unfolder:
|
|||
sce = bpy.context.scene
|
||||
rd = sce.render
|
||||
bk = rd.bake
|
||||
# TODO: do we really need all this recollection?
|
||||
recall = rd.engine, sce.cycles.bake_type, sce.cycles.samples, bk.use_selected_to_active, bk.margin, bk.cage_extrusion, bk.use_cage, bk.use_clear
|
||||
recall = store_rna_properties(rd, bk, sce.cycles)
|
||||
rd.engine = 'CYCLES'
|
||||
recall_pass = {p: getattr(bk, f"use_pass_{p}") for p in ('ambient_occlusion', 'color', 'diffuse', 'direct', 'emit', 'glossy', 'indirect', 'transmission')}
|
||||
for p in recall_pass:
|
||||
for p in ('ambient_occlusion', 'color', 'diffuse', 'direct', 'emit', 'glossy', 'indirect', 'transmission'):
|
||||
setattr(bk, f"use_pass_{p}", (properties.output_type != 'TEXTURE'))
|
||||
lookup = {'TEXTURE': 'DIFFUSE', 'AMBIENT_OCCLUSION': 'AO', 'RENDER': 'COMBINED', 'SELECTED_TO_ACTIVE': 'COMBINED'}
|
||||
sce.cycles.bake_type = lookup[properties.output_type]
|
||||
|
@ -276,13 +276,9 @@ class Unfolder:
|
|||
elif image_packing == 'ISLAND_EMBED':
|
||||
self.mesh.save_separate_images(ppm, filepath, embed=Exporter.encode_image)
|
||||
|
||||
rd.engine, sce.cycles.bake_type, sce.cycles.samples, bk.use_selected_to_active, bk.margin, bk.cage_extrusion, bk.use_cage, bk.use_clear = recall
|
||||
for p, v in recall_pass.items():
|
||||
setattr(bk, f"use_pass_{p}", v)
|
||||
apply_rna_properties(recall, rd, bk, sce.cycles)
|
||||
|
||||
exporter = Exporter(page_size, properties.style, properties.output_margin, (properties.output_type == 'NONE'), properties.angle_epsilon)
|
||||
exporter.do_create_stickers = properties.do_create_stickers
|
||||
exporter.text_size = properties.sticker_width
|
||||
exporter = Exporter(properties)
|
||||
exporter.write(self.mesh, filepath)
|
||||
|
||||
|
||||
|
@ -336,8 +332,9 @@ class Mesh:
|
|||
if not (null_edges or null_faces or twisted_faces or inverted_scale):
|
||||
return True
|
||||
if inverted_scale:
|
||||
raise UnfoldError("The object is flipped inside-out.\n"
|
||||
"You can use Object -> Apply -> Scale to fix it. Export failed.")
|
||||
raise UnfoldError(
|
||||
"The object is flipped inside-out.\n"
|
||||
"You can use Object -> Apply -> Scale to fix it. Export failed.")
|
||||
disease = [("Remove Doubles", null_edges or null_faces), ("Triangulate", twisted_faces)]
|
||||
cure = " and ".join(s for s, k in disease if k)
|
||||
raise UnfoldError(
|
||||
|
@ -514,14 +511,13 @@ class Mesh:
|
|||
angle, _ = cage_fit(points, (cage_size.y - title_height) / cage_size.x)
|
||||
rot = M.Matrix.Rotation(angle, 2)
|
||||
for point in points:
|
||||
# note: we need an in-place operation, and Vector.rotate() seems to work for 3d vectors only
|
||||
point[:] = rot @ point
|
||||
point.rotate(rot)
|
||||
for marker in island.markers:
|
||||
marker.rot = rot @ marker.rot
|
||||
bottom_left = M.Vector((min(v.x for v in points), min(v.y for v in points) - title_height))
|
||||
#DEBUG
|
||||
top_right = M.Vector((max(v.x for v in points), max(v.y for v in points) - title_height))
|
||||
#print(f"fitted aspect: {(top_right.y - bottom_left.y) / (top_right.x - bottom_left.x)}")
|
||||
# DEBUG
|
||||
# top_right = M.Vector((max(v.x for v in points), max(v.y for v in points) - title_height))
|
||||
# print(f"fitted aspect: {(top_right.y - bottom_left.y) / (top_right.x - bottom_left.x)}")
|
||||
for point in points:
|
||||
point -= bottom_left
|
||||
island.bounding_box = M.Vector((max(v.x for v in points), max(v.y for v in points)))
|
||||
|
@ -666,7 +662,8 @@ class Mesh:
|
|||
|
||||
class Edge:
|
||||
"""Wrapper for BPy Edge"""
|
||||
__slots__ = ('data', 'va', 'vb', 'main_faces', 'uvedges',
|
||||
__slots__ = (
|
||||
'data', 'va', 'vb', 'main_faces', 'uvedges',
|
||||
'vector', 'angle',
|
||||
'is_main_cut', 'force_cut', 'priority', 'freestyle')
|
||||
|
||||
|
@ -689,10 +686,11 @@ class Edge:
|
|||
|
||||
def choose_main_faces(self):
|
||||
"""Choose two main faces that might get connected in an island"""
|
||||
from itertools import combinations
|
||||
loops = self.data.link_loops
|
||||
|
||||
def score(pair):
|
||||
return abs(pair[0].face.normal.dot(pair[1].face.normal))
|
||||
|
||||
loops = self.data.link_loops
|
||||
if len(loops) == 2:
|
||||
self.main_faces = list(loops)
|
||||
elif len(loops) > 2:
|
||||
|
@ -709,7 +707,7 @@ class Edge:
|
|||
self.angle = -3 # just a very sharp angle
|
||||
else:
|
||||
s = normal_a.cross(normal_b).dot(self.vector.normalized())
|
||||
s = max(min(s, 1.0), -1.0) # deal with rounding errors
|
||||
s = max(min(s, 1.0), -1.0) # deal with rounding errors
|
||||
self.angle = asin(s)
|
||||
if loop_a.link_loop_next.vert != loop_b.vert or loop_b.link_loop_next.vert != loop_a.vert:
|
||||
self.angle = abs(self.angle)
|
||||
|
@ -741,7 +739,8 @@ class Edge:
|
|||
|
||||
class Island:
|
||||
"""Part of the net to be exported"""
|
||||
__slots__ = ('mesh', 'faces', 'edges', 'vertices', 'fake_vertices', 'boundary', 'markers',
|
||||
__slots__ = (
|
||||
'mesh', 'faces', 'edges', 'vertices', 'fake_vertices', 'boundary', 'markers',
|
||||
'pos', 'bounding_box',
|
||||
'image_path', 'embedded_image',
|
||||
'number', 'label', 'abbreviation', 'title',
|
||||
|
@ -803,6 +802,7 @@ class Island:
|
|||
for loop, uvvertex in self.vertices.items():
|
||||
loop[tex].uv = uvvertex.co.x * scale_x, uvvertex.co.y * scale_y
|
||||
|
||||
|
||||
def join(uvedge_a, uvedge_b, size_limit=None, epsilon=1e-6):
|
||||
"""
|
||||
Try to join other island on given edge
|
||||
|
@ -1081,7 +1081,7 @@ class Page:
|
|||
|
||||
def __init__(self, num=1):
|
||||
self.islands = list()
|
||||
self.name = "page{}".format(num) # TODO delete me
|
||||
self.name = "page{}".format(num) # note: this is only used in svg files naming
|
||||
self.image_path = None
|
||||
|
||||
|
||||
|
@ -1098,7 +1098,8 @@ class UVEdge:
|
|||
"""Edge in 2D"""
|
||||
# Every UVEdge is attached to only one UVFace
|
||||
# UVEdges are doubled as needed because they both have to point clockwise around their faces
|
||||
__slots__ = ('va', 'vb', 'uvface', 'loop',
|
||||
__slots__ = (
|
||||
'va', 'vb', 'uvface', 'loop',
|
||||
'min', 'max', 'bottom', 'top',
|
||||
'neighbor_left', 'neighbor_right', 'sticker')
|
||||
|
||||
|
@ -1172,7 +1173,7 @@ class Arrow:
|
|||
|
||||
class Sticker:
|
||||
"""Mark in the document: sticker tab"""
|
||||
__slots__ = ('bounds', 'center', 'rot', 'text', 'width', 'vertices')
|
||||
__slots__ = ('bounds', 'center', 'points', 'rot', 'text', 'width')
|
||||
|
||||
def __init__(self, uvedge, default_width, index, other: UVEdge):
|
||||
"""Sticker is directly attached to the given UVEdge"""
|
||||
|
@ -1217,12 +1218,12 @@ class Sticker:
|
|||
len_a = min(len_a, (edge.length * sin_b) / (sin_a * cos_b + sin_b * cos_a))
|
||||
len_b = 0 if sin_b == 0 else min(sticker_width / sin_b, (edge.length - len_a * cos_a) / cos_b)
|
||||
|
||||
v3 = UVVertex(second_vertex.co + M.Matrix(((cos_b, -sin_b), (sin_b, cos_b))) @ edge * len_b / edge.length)
|
||||
v4 = UVVertex(first_vertex.co + M.Matrix(((-cos_a, -sin_a), (sin_a, -cos_a))) @ edge * len_a / edge.length)
|
||||
if v3.co != v4.co:
|
||||
self.vertices = [second_vertex, v3, v4, first_vertex]
|
||||
v3 = second_vertex.co + M.Matrix(((cos_b, -sin_b), (sin_b, cos_b))) @ edge * len_b / edge.length
|
||||
v4 = first_vertex.co + M.Matrix(((-cos_a, -sin_a), (sin_a, -cos_a))) @ edge * len_a / edge.length
|
||||
if v3 != v4:
|
||||
self.points = [second_vertex.co, v3, v4, first_vertex.co]
|
||||
else:
|
||||
self.vertices = [second_vertex, v3, first_vertex]
|
||||
self.points = [second_vertex.co, v3, first_vertex.co]
|
||||
|
||||
sin, cos = edge.y / edge.length, edge.x / edge.length
|
||||
self.rot = M.Matrix(((cos, -sin), (sin, cos)))
|
||||
|
@ -1232,7 +1233,7 @@ class Sticker:
|
|||
else:
|
||||
self.text = index
|
||||
self.center = (uvedge.va.co + uvedge.vb.co) / 2 + self.rot @ M.Vector((0, self.width * 0.2))
|
||||
self.bounds = [v3.co, v4.co, self.center] if v3.co != v4.co else [v3.co, self.center]
|
||||
self.bounds = [v3, v4, self.center] if v3 != v4 else [v3, self.center]
|
||||
|
||||
|
||||
class NumberAlone:
|
||||
|
@ -1251,19 +1252,22 @@ class NumberAlone:
|
|||
self.bounds = [self.center]
|
||||
|
||||
|
||||
class SVG:
|
||||
def init_exporter(self, properties):
|
||||
self.page_size = M.Vector((properties.output_size_x, properties.output_size_y))
|
||||
self.style = properties.style
|
||||
margin = properties.output_margin
|
||||
self.margin = M.Vector((margin, margin))
|
||||
self.pure_net = (properties.output_type == 'NONE')
|
||||
self.do_create_stickers = properties.do_create_stickers
|
||||
self.text_size = properties.sticker_width
|
||||
self.angle_epsilon = properties.angle_epsilon
|
||||
|
||||
|
||||
class Svg:
|
||||
"""Simple SVG exporter"""
|
||||
|
||||
def __init__(self, page_size: M.Vector, style, margin, pure_net=True, angle_epsilon=0.01):
|
||||
"""Initialize document settings.
|
||||
page_size: document dimensions in meters
|
||||
pure_net: if True, do not use image"""
|
||||
self.page_size = page_size
|
||||
self.pure_net = pure_net
|
||||
self.style = style
|
||||
self.margin = margin
|
||||
self.text_size = 12
|
||||
self.angle_epsilon = angle_epsilon
|
||||
def __init__(self, properties):
|
||||
init_exporter(self, properties)
|
||||
|
||||
@classmethod
|
||||
def encode_image(cls, bpy_image):
|
||||
|
@ -1275,10 +1279,9 @@ class SVG:
|
|||
bpy_image.save()
|
||||
return base64.encodebytes(open(filename, "rb").read()).decode('ascii')
|
||||
|
||||
def format_vertex(self, vector, pos=M.Vector((0, 0))):
|
||||
def format_vertex(self, vector):
|
||||
"""Return a string with both coordinates of the given vertex."""
|
||||
x, y = vector + pos
|
||||
return "{:.6f} {:.6f}".format((x + self.margin) * 1000, (self.page_size.y - y - self.margin) * 1000)
|
||||
return "{:.6f} {:.6f}".format((vector.x + self.margin.x) * 1000, (self.page_size.y - vector.y - self.margin.y) * 1000)
|
||||
|
||||
def write(self, mesh, filename):
|
||||
"""Write data to a file given by its name."""
|
||||
|
@ -1306,7 +1309,7 @@ class SVG:
|
|||
styleargs = {
|
||||
name: format_color(getattr(self.style, name)) for name in (
|
||||
"outer_color", "outbg_color", "convex_color", "concave_color", "freestyle_color",
|
||||
"inbg_color", "sticker_fill", "text_color")}
|
||||
"inbg_color", "sticker_color", "text_color")}
|
||||
styleargs.update({
|
||||
name: format_style[getattr(self.style, name)] for name in
|
||||
("outer_style", "convex_style", "concave_style", "freestyle_style")})
|
||||
|
@ -1315,7 +1318,7 @@ class SVG:
|
|||
("outer_alpha", "outer_color"), ("outbg_alpha", "outbg_color"),
|
||||
("convex_alpha", "convex_color"), ("concave_alpha", "concave_color"),
|
||||
("freestyle_alpha", "freestyle_color"),
|
||||
("inbg_alpha", "inbg_color"), ("sticker_alpha", "sticker_fill"),
|
||||
("inbg_alpha", "inbg_color"), ("sticker_alpha", "sticker_color"),
|
||||
("text_alpha", "text_color"))})
|
||||
styleargs.update({
|
||||
name: getattr(self.style, name) * self.style.line_width * 1000 for name in
|
||||
|
@ -1328,9 +1331,9 @@ class SVG:
|
|||
if page.image_path:
|
||||
print(
|
||||
self.image_linked_tag.format(
|
||||
pos="{0:.6f} {0:.6f}".format(self.margin*1000),
|
||||
width=(self.page_size.x - 2 * self.margin)*1000,
|
||||
height=(self.page_size.y - 2 * self.margin)*1000,
|
||||
pos="{0:.6f} {0:.6f}".format(self.margin.x*1000),
|
||||
width=(self.page_size.x - 2 * self.margin.x)*1000,
|
||||
height=(self.page_size.y - 2 * self.margin.y)*1000,
|
||||
path=path_convert(page.image_path)),
|
||||
file=f)
|
||||
if len(page.islands) > 1:
|
||||
|
@ -1359,20 +1362,20 @@ class SVG:
|
|||
print(
|
||||
self.text_tag.format(
|
||||
size=1000 * self.text_size,
|
||||
x=1000 * (island.bounding_box.x*0.5 + island.pos.x + self.margin),
|
||||
y=1000 * (self.page_size.y - island.pos.y - self.margin - 0.2 * self.text_size),
|
||||
x=1000 * (island.bounding_box.x*0.5 + island.pos.x + self.margin.x),
|
||||
y=1000 * (self.page_size.y - island.pos.y - self.margin.y - 0.2 * self.text_size),
|
||||
label=island.title),
|
||||
file=f)
|
||||
|
||||
data_markers, data_stickerfill, data_outer, data_convex, data_concave, data_freestyle = (list() for i in range(6))
|
||||
data_markers, data_stickerfill = list(), list()
|
||||
for marker in island.markers:
|
||||
if isinstance(marker, Sticker):
|
||||
data_stickerfill.append("M {} Z".format(
|
||||
line_through(self.format_vertex(vertex.co, island.pos) for vertex in marker.vertices)))
|
||||
line_through(self.format_vertex(co + island.pos) for co in marker.points)))
|
||||
if marker.text:
|
||||
data_markers.append(self.text_transformed_tag.format(
|
||||
label=marker.text,
|
||||
pos=self.format_vertex(marker.center, island.pos),
|
||||
pos=self.format_vertex(marker.center + island.pos),
|
||||
mat=format_matrix(marker.rot),
|
||||
size=marker.width * 1000))
|
||||
elif isinstance(marker, Arrow):
|
||||
|
@ -1380,29 +1383,30 @@ class SVG:
|
|||
position = marker.center + marker.size * marker.rot @ M.Vector((0, -0.9))
|
||||
data_markers.append(self.arrow_marker_tag.format(
|
||||
index=marker.text,
|
||||
arrow_pos=self.format_vertex(marker.center, island.pos),
|
||||
arrow_pos=self.format_vertex(marker.center + island.pos),
|
||||
scale=size,
|
||||
pos=self.format_vertex(position, island.pos - marker.size*M.Vector((0, 0.4))),
|
||||
pos=self.format_vertex(position + island.pos - marker.size*M.Vector((0, 0.4))),
|
||||
mat=format_matrix(size * marker.rot)))
|
||||
elif isinstance(marker, NumberAlone):
|
||||
data_markers.append(self.text_transformed_tag.format(
|
||||
label=marker.text,
|
||||
pos=self.format_vertex(marker.center, island.pos),
|
||||
pos=self.format_vertex(marker.center + island.pos),
|
||||
mat=format_matrix(marker.rot),
|
||||
size=marker.size * 1000))
|
||||
if data_stickerfill and self.style.sticker_fill[3] > 0:
|
||||
if data_stickerfill and self.style.sticker_color[3] > 0:
|
||||
print("<path class='sticker' d='", rows(data_stickerfill), "'/>", file=f)
|
||||
|
||||
data_outer, data_convex, data_concave, data_freestyle = (list() for i in range(4))
|
||||
outer_edges = set(island.boundary)
|
||||
while outer_edges:
|
||||
data_loop = list()
|
||||
uvedge = outer_edges.pop()
|
||||
while 1:
|
||||
if uvedge.sticker:
|
||||
data_loop.extend(self.format_vertex(vertex.co, island.pos) for vertex in uvedge.sticker.vertices[1:])
|
||||
data_loop.extend(self.format_vertex(co + island.pos) for co in uvedge.sticker.points[1:])
|
||||
else:
|
||||
vertex = uvedge.vb if uvedge.uvface.flipped else uvedge.va
|
||||
data_loop.append(self.format_vertex(vertex.co, island.pos))
|
||||
data_loop.append(self.format_vertex(vertex.co + island.pos))
|
||||
uvedge = uvedge.neighbor_right
|
||||
try:
|
||||
outer_edges.remove(uvedge)
|
||||
|
@ -1416,7 +1420,7 @@ class SVG:
|
|||
if edge.is_cut(uvedge.uvface.face) and not uvedge.sticker:
|
||||
continue
|
||||
data_uvedge = "M {}".format(
|
||||
line_through(self.format_vertex(vertex.co, island.pos) for vertex in (uvedge.va, uvedge.vb)))
|
||||
line_through(self.format_vertex(v.co + island.pos) for v in (uvedge.va, uvedge.vb)))
|
||||
if edge.freestyle:
|
||||
data_freestyle.append(data_uvedge)
|
||||
# each uvedge is in two opposite-oriented variants; we want to add each only once
|
||||
|
@ -1507,7 +1511,7 @@ class SVG:
|
|||
stroke-width: {inbg_width:.2}
|
||||
}}
|
||||
path.sticker {{
|
||||
fill: {sticker_fill};
|
||||
fill: {sticker_color};
|
||||
stroke: none;
|
||||
fill-opacity: {sticker_alpha:.2};
|
||||
}}
|
||||
|
@ -1526,7 +1530,7 @@ class SVG:
|
|||
</style>"""
|
||||
|
||||
|
||||
class PDF:
|
||||
class Pdf:
|
||||
"""Simple PDF exporter"""
|
||||
|
||||
mm_to_pt = 72 / 25.4
|
||||
|
@ -1536,16 +1540,34 @@ class PDF:
|
|||
667: '&ABEKPSVXY\x8a\x9fÀÁÂÃÄÅÈÉÊËÝÞ', 722: 'CDHNRUwÇÐÑÙÚÛÜ', 737: '©®', 778: 'GOQÒÓÔÕÖØ', 833: 'Mm¼½¾', 889: '%æ', 944: 'W\x9c', 1000: '\x85\x89\x8c\x97\x99Æ', 1015: '@', }
|
||||
character_width = {c: value for (value, chars) in character_width_packed.items() for c in chars}
|
||||
|
||||
def __init__(self, page_size: M.Vector, style, margin, pure_net=True, angle_epsilon=0.01):
|
||||
self.page_size = page_size
|
||||
self.style = style
|
||||
self.margin = M.Vector((margin, margin))
|
||||
self.pure_net = pure_net
|
||||
self.angle_epsilon = angle_epsilon
|
||||
def __init__(self, properties):
|
||||
init_exporter(self, properties)
|
||||
self.styles = dict()
|
||||
|
||||
def text_width(self, text, scale=None):
|
||||
return (scale or self.text_size) * sum(self.character_width.get(c, 556) for c in text) / 1000
|
||||
|
||||
def styling(self, name, do_stroke=True):
|
||||
s, m, l = (length * self.style.line_width * 1000 for length in (1, 4, 9))
|
||||
format_style = {'SOLID': [], 'DOT': [s, m], 'DASH': [m, l], 'LONGDASH': [l, m], 'DASHDOT': [l, m, s, m]}
|
||||
style, color, width = (getattr(self.style, f"{name}_{arg}", None) for arg in ("style", "color", "width"))
|
||||
style = style or 'SOLID'
|
||||
result = ["q"]
|
||||
if do_stroke:
|
||||
result += [
|
||||
"[ " + " ".join("{:.3f}".format(num) for num in format_style[style]) + " ] 0 d",
|
||||
"{0:.3f} {1:.3f} {2:.3f} RG".format(*color),
|
||||
"{:.3f} w".format(self.style.line_width * 1000 * width),
|
||||
]
|
||||
else:
|
||||
result.append("{0:.3f} {1:.3f} {2:.3f} rg".format(*color))
|
||||
if color[3] < 1:
|
||||
style_name = "R{:03}".format(round(1000 * color[3]))
|
||||
result.append("/{} gs".format(style_name))
|
||||
if style_name not in self.styles:
|
||||
self.styles[style_name] = {"CA": color[3], "ca": color[3]}
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def encode_image(cls, bpy_image):
|
||||
data = bytes(int(255 * px) for (i, px) in enumerate(bpy_image.pixels) if i % 4 != 3)
|
||||
|
@ -1557,10 +1579,12 @@ class PDF:
|
|||
|
||||
def write(self, mesh, filename):
|
||||
def format_dict(obj, refs=tuple()):
|
||||
return "<< " + "".join("/{} {}\n".format(key, format_value(value, refs)) for (key, value) in obj.items()) + ">>"
|
||||
content = "".join("/{} {}\n".format(key, format_value(value, refs)) for (key, value) in obj.items())
|
||||
return f"<< {content} >>"
|
||||
|
||||
def line_through(seq):
|
||||
return "".join("{0.x:.6f} {0.y:.6f} {1} ".format(1000*v.co, c) for (v, c) in zip(seq, chain("m", repeat("l"))))
|
||||
fmt = "{0.x:.6f} {0.y:.6f} {1} ".format
|
||||
return "".join(fmt(1000*co, cmd) for (co, cmd) in zip(seq, chain("m", repeat("l"))))
|
||||
|
||||
def format_value(value, refs=tuple()):
|
||||
if value in refs:
|
||||
|
@ -1579,82 +1603,64 @@ class PDF:
|
|||
return "/{}".format(value) # this script can output only PDF names, no strings
|
||||
|
||||
def write_object(index, obj, refs, f, stream=None):
|
||||
byte_count = f.write("{} 0 obj\n".format(index))
|
||||
byte_count = f.write("{} 0 obj\n".format(index).encode())
|
||||
if type(obj) is not dict:
|
||||
stream, obj = obj, dict()
|
||||
elif "stream" in obj:
|
||||
stream = obj.pop("stream")
|
||||
if stream:
|
||||
if True or type(stream) is bytes:
|
||||
obj["Filter"] = ["ASCII85Decode", "FlateDecode"]
|
||||
stream = encode(stream)
|
||||
obj["Filter"] = "FlateDecode"
|
||||
stream = encode(stream)
|
||||
obj["Length"] = len(stream)
|
||||
byte_count += f.write(format_dict(obj, refs))
|
||||
byte_count += f.write(format_dict(obj, refs).encode())
|
||||
if stream:
|
||||
byte_count += f.write("\nstream\n")
|
||||
byte_count += f.write(b"\nstream\n")
|
||||
byte_count += f.write(stream)
|
||||
byte_count += f.write("\nendstream")
|
||||
return byte_count + f.write("\nendobj\n")
|
||||
byte_count += f.write(b"\nendstream")
|
||||
return byte_count + f.write(b"\nendobj\n")
|
||||
|
||||
def encode(data):
|
||||
from base64 import a85encode
|
||||
from zlib import compress
|
||||
if hasattr(data, "encode"):
|
||||
data = data.encode()
|
||||
return a85encode(compress(data), adobe=True, wrapcol=250)[2:].decode()
|
||||
return compress(data)
|
||||
|
||||
page_size_pt = 1000 * self.mm_to_pt * self.page_size
|
||||
reset_style = ["Q"] # graphic command for later use
|
||||
root = {"Type": "Pages", "MediaBox": [0, 0, page_size_pt.x, page_size_pt.y], "Kids": list()}
|
||||
catalog = {"Type": "Catalog", "Pages": root}
|
||||
font = {
|
||||
"Type": "Font", "Subtype": "Type1", "Name": "F1",
|
||||
"BaseFont": "Helvetica", "Encoding": "MacRomanEncoding"}
|
||||
|
||||
dl = [length * self.style.line_width * 1000 for length in (1, 4, 9)]
|
||||
format_style = {
|
||||
'SOLID': list(), 'DOT': [dl[0], dl[1]], 'DASH': [dl[1], dl[2]],
|
||||
'LONGDASH': [dl[2], dl[1]], 'DASHDOT': [dl[2], dl[1], dl[0], dl[1]]}
|
||||
styles = {
|
||||
"Gtext": {"ca": self.style.text_color[3], "Font": [font, 1000 * self.text_size]},
|
||||
"Gsticker": {"ca": self.style.sticker_fill[3]}}
|
||||
for name in ("outer", "convex", "concave", "freestyle"):
|
||||
gs = {
|
||||
"LW": self.style.line_width * 1000 * getattr(self.style, name + "_width"),
|
||||
"CA": getattr(self.style, name + "_color")[3],
|
||||
"D": [format_style[getattr(self.style, name + "_style")], 0]}
|
||||
styles["G" + name] = gs
|
||||
for name in ("outbg", "inbg"):
|
||||
gs = {
|
||||
"LW": self.style.line_width * 1000 * getattr(self.style, name + "_width"),
|
||||
"CA": getattr(self.style, name + "_color")[3],
|
||||
"D": [format_style['SOLID'], 0]}
|
||||
styles["G" + name] = gs
|
||||
|
||||
objects = [root, catalog, font]
|
||||
objects.extend(styles.values())
|
||||
|
||||
for page in mesh.pages:
|
||||
commands = ["{0:.6f} 0 0 {0:.6f} 0 0 cm".format(self.mm_to_pt)]
|
||||
resources = {"Font": {"F1": font}, "ExtGState": styles, "XObject": dict()}
|
||||
resources = {"Font": {"F1": font}, "ExtGState": self.styles, "ProcSet": ["PDF"]}
|
||||
if any(island.embedded_image for island in page.islands):
|
||||
resources["XObject"] = dict()
|
||||
resources["ProcSet"].append("ImageC")
|
||||
for island in page.islands:
|
||||
commands.append("q 1 0 0 1 {0.x:.6f} {0.y:.6f} cm".format(1000*(self.margin + island.pos)))
|
||||
if island.embedded_image:
|
||||
identifier = "Im{}".format(len(resources["XObject"]) + 1)
|
||||
identifier = "I{}".format(len(resources["XObject"]) + 1)
|
||||
commands.append(self.command_image.format(1000 * island.bounding_box, identifier))
|
||||
objects.append(island.embedded_image)
|
||||
resources["XObject"][identifier] = island.embedded_image
|
||||
|
||||
if island.title:
|
||||
commands += self.styling("text", do_stroke=False)
|
||||
commands.append(self.command_label.format(
|
||||
size=1000*self.text_size,
|
||||
x=500 * (island.bounding_box.x - self.text_width(island.title)),
|
||||
y=1000 * 0.2 * self.text_size,
|
||||
label=island.title))
|
||||
commands += reset_style
|
||||
|
||||
data_markers, data_stickerfill, data_outer, data_convex, data_concave, data_freestyle = (list() for i in range(6))
|
||||
data_markers, data_stickerfill = list(), list()
|
||||
for marker in island.markers:
|
||||
if isinstance(marker, Sticker):
|
||||
data_stickerfill.append(line_through(marker.vertices) + "f")
|
||||
data_stickerfill.append(line_through(marker.points) + "f")
|
||||
if marker.text:
|
||||
data_markers.append(self.command_sticker.format(
|
||||
label=marker.text,
|
||||
|
@ -1678,16 +1684,17 @@ class PDF:
|
|||
mat=marker.rot,
|
||||
size=1000*marker.size))
|
||||
|
||||
data_outer, data_convex, data_concave, data_freestyle = (list() for i in range(4))
|
||||
outer_edges = set(island.boundary)
|
||||
while outer_edges:
|
||||
data_loop = list()
|
||||
uvedge = outer_edges.pop()
|
||||
while 1:
|
||||
if uvedge.sticker:
|
||||
data_loop.extend(uvedge.sticker.vertices[1:])
|
||||
data_loop.extend(uvedge.sticker.points[1:])
|
||||
else:
|
||||
vertex = uvedge.vb if uvedge.uvface.flipped else uvedge.va
|
||||
data_loop.append(vertex)
|
||||
data_loop.append(vertex.co)
|
||||
uvedge = uvedge.neighbor_right
|
||||
try:
|
||||
outer_edges.remove(uvedge)
|
||||
|
@ -1699,7 +1706,7 @@ class PDF:
|
|||
edge = mesh.edges[loop.edge]
|
||||
if edge.is_cut(uvedge.uvface.face) and not uvedge.sticker:
|
||||
continue
|
||||
data_uvedge = line_through((uvedge.va, uvedge.vb)) + "S"
|
||||
data_uvedge = line_through((uvedge.va.co, uvedge.vb.co)) + "S"
|
||||
if edge.freestyle:
|
||||
data_freestyle.append(data_uvedge)
|
||||
# each uvedge exists in two opposite-oriented variants; we want to add each only once
|
||||
|
@ -1711,56 +1718,52 @@ class PDF:
|
|||
if island.is_inside_out:
|
||||
data_convex, data_concave = data_concave, data_convex
|
||||
|
||||
if data_stickerfill and self.style.sticker_fill[3] > 0:
|
||||
commands.append("/Gsticker gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} rg".format(self.style.sticker_fill))
|
||||
commands.extend(data_stickerfill)
|
||||
if data_stickerfill and self.style.sticker_color[3] > 0:
|
||||
commands += chain(self.styling("sticker", do_stroke=False), data_stickerfill, reset_style)
|
||||
if data_freestyle:
|
||||
commands.append("/Gfreestyle gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.freestyle_color))
|
||||
commands.extend(data_freestyle)
|
||||
commands += chain(self.styling("freestyle"), data_freestyle, reset_style)
|
||||
if (data_convex or data_concave) and not self.pure_net and self.style.use_inbg:
|
||||
commands.append("/Ginbg gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.inbg_color))
|
||||
commands.extend(chain(data_convex, data_concave))
|
||||
commands += chain(self.styling("inbg"), data_convex, data_concave, reset_style)
|
||||
if data_convex:
|
||||
commands.append("/Gconvex gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.convex_color))
|
||||
commands.extend(data_convex)
|
||||
commands += chain(self.styling("convex"), data_convex, reset_style)
|
||||
if data_concave:
|
||||
commands.append("/Gconcave gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.concave_color))
|
||||
commands.extend(data_concave)
|
||||
commands += chain(self.styling("concave"), data_concave, reset_style)
|
||||
if data_outer:
|
||||
if not self.pure_net and self.style.use_outbg:
|
||||
commands.append("/Goutbg gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.outbg_color))
|
||||
commands.extend(data_outer)
|
||||
commands.append("/Gouter gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.outer_color))
|
||||
commands.extend(data_outer)
|
||||
commands.append("/Gtext gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} rg".format(self.style.text_color))
|
||||
commands.extend(data_markers)
|
||||
commands.append("Q")
|
||||
commands += chain(self.styling("outbg"), data_outer, reset_style)
|
||||
commands += chain(self.styling("outer"), data_outer, reset_style)
|
||||
if data_markers:
|
||||
commands += chain(self.styling("text", do_stroke=False), data_markers, reset_style)
|
||||
commands += reset_style # return from island to page coordinates
|
||||
content = "\n".join(commands)
|
||||
page = {"Type": "Page", "Parent": root, "Contents": content, "Resources": resources}
|
||||
root["Kids"].append(page)
|
||||
objects.extend((page, content))
|
||||
objects += page, content
|
||||
objects.extend(self.styles.values())
|
||||
|
||||
root["Count"] = len(root["Kids"])
|
||||
with open(filename, "w+") as f:
|
||||
with open(filename, "wb+") as f:
|
||||
xref_table = list()
|
||||
position = f.write("%PDF-1.4\n")
|
||||
position = 0
|
||||
position += f.write(b"%PDF-1.4\n")
|
||||
position += f.write(b"%\xde\xad\xbe\xef\n")
|
||||
for index, obj in enumerate(objects, 1):
|
||||
xref_table.append(position)
|
||||
position += write_object(index, obj, objects, f)
|
||||
xref_pos = position
|
||||
f.write("xref_table\n0 {}\n".format(len(xref_table) + 1))
|
||||
f.write("{:010} {:05} f\n".format(0, 65536))
|
||||
f.write("xref\n0 {}\n".format(len(xref_table) + 1).encode())
|
||||
f.write("{:010} {:05} f\r\n".format(0, 65535).encode())
|
||||
for position in xref_table:
|
||||
f.write("{:010} {:05} n\n".format(position, 0))
|
||||
f.write("trailer\n")
|
||||
f.write(format_dict({"Size": len(xref_table), "Root": catalog}, objects))
|
||||
f.write("\nstartxref\n{}\n%%EOF\n".format(xref_pos))
|
||||
f.write("{:010} {:05} n\r\n".format(position, 0).encode())
|
||||
f.write(b"trailer\n")
|
||||
f.write(format_dict({"Size": len(xref_table) + 1, "Root": catalog}, objects).encode())
|
||||
f.write("\nstartxref\n{}\n%%EOF\n".format(xref_pos).encode())
|
||||
|
||||
command_label = "/Gtext gs BT {x:.6f} {y:.6f} Td ({label}) Tj ET"
|
||||
command_label = "q /F1 {size:.6f} Tf BT {x:.6f} {y:.6f} Td ({label}) Tj ET Q"
|
||||
command_image = "q {0.x:.6f} 0 0 {0.y:.6f} 0 0 cm 1 0 0 -1 0 1 cm /{1} Do Q"
|
||||
command_sticker = "q {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {pos.x:.6f} {pos.y:.6f} cm BT {align:.6f} 0 Td /F1 {size:.6f} Tf ({label}) Tj ET Q"
|
||||
command_arrow = "q BT {pos.x:.6f} {pos.y:.6f} Td /F1 {size:.6f} Tf ({index}) Tj ET {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {arrow_pos.x:.6f} {arrow_pos.y:.6f} cm 0 0 m 1 -1 l 0 -0.25 l -1 -1 l f Q"
|
||||
command_number = "q {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {pos.x:.6f} {pos.y:.6f} cm BT /F1 {size:.6f} Tf ({label}) Tj ET Q"
|
||||
command_sticker = "q /F1 {size:.6f} Tf {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {pos.x:.6f} {pos.y:.6f} cm BT {align:.6f} 0 Td ({label}) Tj ET Q"
|
||||
command_arrow = "q /F1 {size:.6f} Tf BT {pos.x:.6f} {pos.y:.6f} Td ({index}) Tj ET {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {arrow_pos.x:.6f} {arrow_pos.y:.6f} cm 0 0 m 1 -1 l 0 -0.25 l -1 -1 l f Q"
|
||||
command_number = "q /F1 {size:.6f} Tf {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {pos.x:.6f} {pos.y:.6f} cm BT ({label}) Tj ET Q"
|
||||
|
||||
|
||||
class Unfold(bpy.types.Operator):
|
||||
|
@ -1957,7 +1960,7 @@ class PaperModelStyle(bpy.types.PropertyGroup):
|
|||
name="Inner Highlight Thickness", description="Relative thickness of the highlighting lines",
|
||||
default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR')
|
||||
|
||||
sticker_fill: bpy.props.FloatVectorProperty(
|
||||
sticker_color: bpy.props.FloatVectorProperty(
|
||||
name="Tabs Fill", description="Fill color of sticking tabs",
|
||||
default=(0.9, 0.9, 0.9, 1.0), min=0, max=1, subtype='COLOR', size=4)
|
||||
text_color: bpy.props.FloatVectorProperty(
|
||||
|
@ -1972,6 +1975,8 @@ class ExportPaperModel(bpy.types.Operator):
|
|||
bl_idname = "export_mesh.paper_model"
|
||||
bl_label = "Export Paper Model"
|
||||
bl_description = "Export the selected object's net and optionally bake its texture"
|
||||
bl_options = {'PRESET'}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
name="File Path", description="Target file to save the SVG", options={'SKIP_SAVE'})
|
||||
filename: bpy.props.StringProperty(
|
||||
|
@ -2034,13 +2039,16 @@ class ExportPaperModel(bpy.types.Operator):
|
|||
name="Scale", description="Divisor of all dimensions when exporting",
|
||||
default=1, soft_min=1.0, soft_max=100.0, subtype='FACTOR', precision=1)
|
||||
do_create_uvmap: bpy.props.BoolProperty(
|
||||
name="Create UVMap", description="Create a new UV Map showing the islands and page layout",
|
||||
name="Create UVMap",
|
||||
description="Create a new UV Map showing the islands and page layout",
|
||||
default=False, options={'SKIP_SAVE'})
|
||||
ui_expanded_document: bpy.props.BoolProperty(
|
||||
name="Show Document Settings Expanded", description="Shows the box 'Document Settings' expanded in user interface",
|
||||
name="Show Document Settings Expanded",
|
||||
description="Shows the box 'Document Settings' expanded in user interface",
|
||||
default=True, options={'SKIP_SAVE'})
|
||||
ui_expanded_style: bpy.props.BoolProperty(
|
||||
name="Show Style Settings Expanded", description="Shows the box 'Colors and Style' expanded in user interface",
|
||||
name="Show Style Settings Expanded",
|
||||
description="Shows the box 'Colors and Style' expanded in user interface",
|
||||
default=False, options={'SKIP_SAVE'})
|
||||
style: bpy.props.PointerProperty(type=PaperModelStyle)
|
||||
|
||||
|
@ -2058,8 +2066,9 @@ class ExportPaperModel(bpy.types.Operator):
|
|||
self.object = context.active_object
|
||||
self.unfolder = Unfolder(self.object)
|
||||
cage_size = M.Vector((sce.paper_model.output_size_x, sce.paper_model.output_size_y))
|
||||
self.unfolder.prepare(cage_size, scale=sce.unit_settings.scale_length/self.scale, limit_by_page=sce.paper_model.limit_by_page)
|
||||
if self.scale == 1:
|
||||
unfolder_scale = sce.unit_settings.scale_length/self.scale
|
||||
self.unfolder.prepare(cage_size, scale=unfolder_scale, limit_by_page=sce.paper_model.limit_by_page)
|
||||
if sce.paper_model.use_auto_scale:
|
||||
self.scale = ceil(self.get_scale_ratio(sce))
|
||||
|
||||
def recall(self):
|
||||
|
@ -2106,14 +2115,7 @@ class ExportPaperModel(bpy.types.Operator):
|
|||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.prop(self.properties, "do_create_uvmap")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.menu("VIEW3D_MT_paper_model_presets", text=bpy.types.VIEW3D_MT_paper_model_presets.bl_label)
|
||||
row.operator("export_mesh.paper_model_preset_add", text="", icon='ADD')
|
||||
row.operator("export_mesh.paper_model_preset_add", text="", icon='REMOVE').remove_active = True
|
||||
|
||||
layout.prop(self.properties, "scale", text="Scale: 1/")
|
||||
scale_ratio = self.get_scale_ratio(context.scene)
|
||||
if scale_ratio > 1:
|
||||
|
@ -2153,7 +2155,7 @@ class ExportPaperModel(bpy.types.Operator):
|
|||
box.prop(self.properties, "output_type")
|
||||
col = box.column()
|
||||
col.active = (self.output_type != 'NONE')
|
||||
if len(self.object.data.uv_layers) == 8:
|
||||
if len(self.object.data.uv_layers) >= 8:
|
||||
col.label(text="No UV slots left, No Texture is the only option.", icon='ERROR')
|
||||
elif context.scene.render.engine != 'CYCLES' and self.output_type != 'NONE':
|
||||
col.label(text="Cycles will be used for texture baking.", icon='ERROR')
|
||||
|
@ -2206,7 +2208,7 @@ class ExportPaperModel(bpy.types.Operator):
|
|||
sub.prop(self.style, "inbg_width", text="Relative width")
|
||||
col = box.column()
|
||||
col.active = self.do_create_stickers
|
||||
col.prop(self.style, "sticker_fill")
|
||||
col.prop(self.style, "sticker_color")
|
||||
box.prop(self.style, "text_color")
|
||||
|
||||
|
||||
|
@ -2271,31 +2273,6 @@ class SelectIsland(bpy.types.Operator):
|
|||
return {'FINISHED'}
|
||||
|
||||
|
||||
class VIEW3D_MT_paper_model_presets(bpy.types.Menu):
|
||||
bl_label = "Paper Model Presets"
|
||||
preset_subdir = "export_mesh"
|
||||
preset_operator = "script.execute_preset"
|
||||
draw = bpy.types.Menu.draw_preset
|
||||
|
||||
|
||||
class AddPresetPaperModel(bl_operators.presets.AddPresetBase, bpy.types.Operator):
|
||||
"""Add or remove a Paper Model Preset"""
|
||||
bl_idname = "export_mesh.paper_model_preset_add"
|
||||
bl_label = "Add Paper Model Preset"
|
||||
preset_menu = "VIEW3D_MT_paper_model_presets"
|
||||
preset_subdir = "export_mesh"
|
||||
preset_defines = ["op = bpy.context.active_operator"]
|
||||
|
||||
@property
|
||||
def preset_values(self):
|
||||
op = bpy.ops.export_mesh.paper_model
|
||||
properties = op.get_rna().bl_rna.properties.items()
|
||||
blacklist = bpy.types.Operator.bl_rna.properties.keys()
|
||||
return [
|
||||
"op.{}".format(prop_id) for (prop_id, prop) in properties
|
||||
if not (prop.is_hidden or prop.is_skip_save or prop_id in blacklist)]
|
||||
|
||||
|
||||
class VIEW3D_PT_paper_model_tools(bpy.types.Panel):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
|
@ -2332,7 +2309,10 @@ class VIEW3D_PT_paper_model_settings(bpy.types.Panel):
|
|||
|
||||
layout.operator("export_mesh.paper_model")
|
||||
props = sce.paper_model
|
||||
layout.prop(props, "scale", text="Model Scale: 1/")
|
||||
layout.prop(props, "use_auto_scale")
|
||||
sub = layout.row()
|
||||
sub.active = not props.use_auto_scale
|
||||
sub.prop(props, "scale", text="Model Scale: 1/")
|
||||
|
||||
layout.prop(props, "limit_by_page")
|
||||
col = layout.column()
|
||||
|
@ -2459,9 +2439,39 @@ class PaperModelSettings(bpy.types.PropertyGroup):
|
|||
output_size_y: bpy.props.FloatProperty(
|
||||
name="Height", description="Maximal height of an island",
|
||||
default=0.29, soft_min=0.148, soft_max=1.189, subtype="UNSIGNED", unit="LENGTH")
|
||||
use_auto_scale: bpy.props.BoolProperty(
|
||||
name="Automatic Scale", description="Scale the net automatically to fit on paper",
|
||||
default=True)
|
||||
scale: bpy.props.FloatProperty(
|
||||
name="Scale", description="Divisor of all dimensions when exporting",
|
||||
default=1, soft_min=1.0, soft_max=100.0, subtype='FACTOR', precision=1)
|
||||
default=1, soft_min=1.0, soft_max=100.0, subtype='FACTOR', precision=1,
|
||||
update=lambda settings, _: settings.__setattr__('use_auto_scale', False))
|
||||
|
||||
|
||||
def factory_update_addon_category(cls, prop):
|
||||
def func(self, context):
|
||||
if hasattr(bpy.types, cls.__name__):
|
||||
bpy.utils.unregister_class(cls)
|
||||
cls.bl_category = self[prop]
|
||||
bpy.utils.register_class(cls)
|
||||
return func
|
||||
|
||||
|
||||
class PaperAddonPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = __name__
|
||||
unfold_category: bpy.props.StringProperty(
|
||||
name="Unfold Panel Category", description="Category in 3D View Toolbox where the Unfold panel is displayed",
|
||||
default="Paper", update=factory_update_addon_category(VIEW3D_PT_paper_model_tools, 'unfold_category'))
|
||||
export_category: bpy.props.StringProperty(
|
||||
name="Export Panel Category", description="Category in 3D View Toolbox where the Export panel is displayed",
|
||||
default="Paper", update=factory_update_addon_category(VIEW3D_PT_paper_model_settings, 'export_category'))
|
||||
|
||||
def draw(self, context):
|
||||
sub = self.layout.column(align=True)
|
||||
sub.use_property_split = True
|
||||
sub.label(text="3D View Panel Category:")
|
||||
sub.prop(self, "unfold_category", text="Unfold Panel:")
|
||||
sub.prop(self, "export_category", text="Export Panel:")
|
||||
|
||||
|
||||
module_classes = (
|
||||
|
@ -2469,14 +2479,13 @@ module_classes = (
|
|||
ExportPaperModel,
|
||||
ClearAllSeams,
|
||||
SelectIsland,
|
||||
AddPresetPaperModel,
|
||||
FaceList,
|
||||
IslandList,
|
||||
PaperModelSettings,
|
||||
VIEW3D_MT_paper_model_presets,
|
||||
DATA_PT_paper_model_islands,
|
||||
VIEW3D_PT_paper_model_tools,
|
||||
VIEW3D_PT_paper_model_settings,
|
||||
PaperAddonPreferences,
|
||||
)
|
||||
|
||||
|
||||
|
@ -2493,6 +2502,10 @@ def register():
|
|||
default=-1, min=-1, max=100, options={'SKIP_SAVE'}, update=island_index_changed)
|
||||
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.prepend(menu_func_unfold)
|
||||
# Force an update on the panel category properties
|
||||
prefs = bpy.context.preferences.addons[__name__].preferences
|
||||
prefs.unfold_category = prefs.unfold_category
|
||||
prefs.export_category = prefs.export_category
|
||||
|
||||
|
||||
def unregister():
|
||||
|
|
|
@ -22,7 +22,7 @@ bl_info = {
|
|||
"name": "Collection Manager",
|
||||
"description": "Manage collections and their objects",
|
||||
"author": "Ryan Inch",
|
||||
"version": (2, 17, 1),
|
||||
"version": (2, 18, 0),
|
||||
"blender": (2, 80, 0),
|
||||
"location": "View3D - Object Mode (Shortcut - M)",
|
||||
"warning": '', # used for warning icon and text in addons panel
|
||||
|
@ -35,6 +35,7 @@ bl_info = {
|
|||
if "bpy" in locals():
|
||||
import importlib
|
||||
|
||||
importlib.reload(cm_init)
|
||||
importlib.reload(internals)
|
||||
importlib.reload(operator_utils)
|
||||
importlib.reload(operators)
|
||||
|
@ -45,6 +46,7 @@ if "bpy" in locals():
|
|||
importlib.reload(preferences)
|
||||
|
||||
else:
|
||||
from . import cm_init
|
||||
from . import internals
|
||||
from . import operator_utils
|
||||
from . import operators
|
||||
|
@ -55,124 +57,9 @@ else:
|
|||
from . import preferences
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
BoolProperty,
|
||||
StringProperty,
|
||||
PointerProperty,
|
||||
)
|
||||
|
||||
|
||||
class CollectionManagerProperties(PropertyGroup):
|
||||
cm_list_collection: CollectionProperty(type=internals.CMListCollection)
|
||||
cm_list_index: IntProperty()
|
||||
|
||||
show_exclude: BoolProperty(default=True, name="[EC] Exclude from View Layer")
|
||||
show_selectable: BoolProperty(default=True, name="[SS] Disable Selection")
|
||||
show_hide_viewport: BoolProperty(default=True, name="[VV] Hide in Viewport")
|
||||
show_disable_viewport: BoolProperty(default=False, name="[DV] Disable in Viewports")
|
||||
show_render: BoolProperty(default=False, name="[RR] Disable in Renders")
|
||||
show_holdout: BoolProperty(default=False, name="[HH] Holdout")
|
||||
show_indirect_only: BoolProperty(default=False, name="[IO] Indirect Only")
|
||||
|
||||
align_local_ops: BoolProperty(default=False, name="Align Local Options",
|
||||
description="Align local options in a column to the right")
|
||||
|
||||
in_phantom_mode: BoolProperty(default=False)
|
||||
|
||||
update_header: CollectionProperty(type=internals.CMListCollection)
|
||||
|
||||
ui_separator: StringProperty(name="", default="")
|
||||
|
||||
qcd_slots_blend_data: StringProperty()
|
||||
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = (
|
||||
internals.CMListCollection,
|
||||
internals.CMSendReport,
|
||||
operators.SetActiveCollection,
|
||||
operators.ExpandAllOperator,
|
||||
operators.ExpandSublevelOperator,
|
||||
operators.CMExcludeOperator,
|
||||
operators.CMUnExcludeAllOperator,
|
||||
operators.CMRestrictSelectOperator,
|
||||
operators.CMUnRestrictSelectAllOperator,
|
||||
operators.CMHideOperator,
|
||||
operators.CMUnHideAllOperator,
|
||||
operators.CMDisableViewportOperator,
|
||||
operators.CMUnDisableViewportAllOperator,
|
||||
operators.CMDisableRenderOperator,
|
||||
operators.CMUnDisableRenderAllOperator,
|
||||
operators.CMHoldoutOperator,
|
||||
operators.CMUnHoldoutAllOperator,
|
||||
operators.CMIndirectOnlyOperator,
|
||||
operators.CMUnIndirectOnlyAllOperator,
|
||||
operators.CMNewCollectionOperator,
|
||||
operators.CMRemoveCollectionOperator,
|
||||
operators.CMRemoveEmptyCollectionsOperator,
|
||||
operators.CMSelectCollectionObjectsOperator,
|
||||
operators.CMSetCollectionOperator,
|
||||
operators.CMPhantomModeOperator,
|
||||
operators.CMApplyPhantomModeOperator,
|
||||
preferences.CMPreferences,
|
||||
ui.CM_UL_items,
|
||||
ui.CollectionManager,
|
||||
ui.CMDisplayOptionsPanel,
|
||||
ui.SpecialsMenu,
|
||||
CollectionManagerProperties,
|
||||
)
|
||||
|
||||
@persistent
|
||||
def depsgraph_update_post_handler(dummy):
|
||||
if internals.move_triggered:
|
||||
internals.move_triggered = False
|
||||
return
|
||||
|
||||
internals.move_selection.clear()
|
||||
internals.move_active = None
|
||||
|
||||
@persistent
|
||||
def undo_redo_post_handler(dummy):
|
||||
internals.move_selection.clear()
|
||||
internals.move_active = None
|
||||
|
||||
|
||||
def menu_addition(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.operator('view3d.collection_manager')
|
||||
|
||||
if bpy.context.preferences.addons[__package__].preferences.enable_qcd:
|
||||
layout.operator('view3d.qcd_move_widget')
|
||||
|
||||
layout.separator()
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.Scene.collection_manager = PointerProperty(type=CollectionManagerProperties)
|
||||
|
||||
# create the global menu hotkey
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon: # not present when started with --background
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Object Mode')
|
||||
kmi = km.keymap_items.new('view3d.collection_manager', 'M', 'PRESS')
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
# Add Collection Manager & QCD Move Widget to the Object->Collections menu
|
||||
bpy.types.VIEW3D_MT_object_collection.prepend(menu_addition)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_post_handler)
|
||||
bpy.app.handlers.undo_post.append(undo_redo_post_handler)
|
||||
bpy.app.handlers.redo_post.append(undo_redo_post_handler)
|
||||
cm_init.register_cm()
|
||||
|
||||
if bpy.context.preferences.addons[__package__].preferences.enable_qcd:
|
||||
qcd_init.register_qcd()
|
||||
|
@ -181,22 +68,7 @@ def unregister():
|
|||
if bpy.context.preferences.addons[__package__].preferences.enable_qcd:
|
||||
qcd_init.unregister_qcd()
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# Remove Collection Manager & QCD Move Widget from the Object->Collections menu
|
||||
bpy.types.VIEW3D_MT_object_collection.remove(menu_addition)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_post_handler)
|
||||
bpy.app.handlers.undo_post.remove(undo_redo_post_handler)
|
||||
bpy.app.handlers.redo_post.remove(undo_redo_post_handler)
|
||||
|
||||
del bpy.types.Scene.collection_manager
|
||||
|
||||
# remove keymaps when add-on is deactivated
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
cm_init.unregister_cm()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# Copyright 2011, Ryan Inch
|
||||
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
|
||||
importlib.reload(internals)
|
||||
importlib.reload(operator_utils)
|
||||
importlib.reload(operators)
|
||||
importlib.reload(ui)
|
||||
importlib.reload(preferences)
|
||||
|
||||
else:
|
||||
from . import internals
|
||||
from . import operator_utils
|
||||
from . import operators
|
||||
from . import ui
|
||||
from . import preferences
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
BoolProperty,
|
||||
StringProperty,
|
||||
PointerProperty,
|
||||
)
|
||||
|
||||
|
||||
class CollectionManagerProperties(PropertyGroup):
|
||||
cm_list_collection: CollectionProperty(type=internals.CMListCollection)
|
||||
cm_list_index: IntProperty()
|
||||
|
||||
show_exclude: BoolProperty(default=True, name="[EC] Exclude from View Layer")
|
||||
show_selectable: BoolProperty(default=True, name="[SS] Disable Selection")
|
||||
show_hide_viewport: BoolProperty(default=True, name="[VV] Hide in Viewport")
|
||||
show_disable_viewport: BoolProperty(default=False, name="[DV] Disable in Viewports")
|
||||
show_render: BoolProperty(default=False, name="[RR] Disable in Renders")
|
||||
show_holdout: BoolProperty(default=False, name="[HH] Holdout")
|
||||
show_indirect_only: BoolProperty(default=False, name="[IO] Indirect Only")
|
||||
|
||||
align_local_ops: BoolProperty(default=False, name="Align Local Options",
|
||||
description="Align local options in a column to the right")
|
||||
|
||||
in_phantom_mode: BoolProperty(default=False)
|
||||
|
||||
update_header: CollectionProperty(type=internals.CMListCollection)
|
||||
|
||||
ui_separator: StringProperty(name="", default="")
|
||||
|
||||
qcd_slots_blend_data: StringProperty()
|
||||
|
||||
|
||||
addon_keymaps = []
|
||||
addon_disable_objects_hotkey_keymaps = []
|
||||
|
||||
classes = (
|
||||
internals.CMListCollection,
|
||||
internals.CMSendReport,
|
||||
operators.SetActiveCollection,
|
||||
operators.ExpandAllOperator,
|
||||
operators.ExpandSublevelOperator,
|
||||
operators.CMExcludeOperator,
|
||||
operators.CMUnExcludeAllOperator,
|
||||
operators.CMRestrictSelectOperator,
|
||||
operators.CMUnRestrictSelectAllOperator,
|
||||
operators.CMHideOperator,
|
||||
operators.CMUnHideAllOperator,
|
||||
operators.CMDisableViewportOperator,
|
||||
operators.CMUnDisableViewportAllOperator,
|
||||
operators.CMDisableRenderOperator,
|
||||
operators.CMUnDisableRenderAllOperator,
|
||||
operators.CMHoldoutOperator,
|
||||
operators.CMUnHoldoutAllOperator,
|
||||
operators.CMIndirectOnlyOperator,
|
||||
operators.CMUnIndirectOnlyAllOperator,
|
||||
operators.CMNewCollectionOperator,
|
||||
operators.CMRemoveCollectionOperator,
|
||||
operators.CMRemoveEmptyCollectionsOperator,
|
||||
operators.CMSelectCollectionObjectsOperator,
|
||||
operators.CMSetCollectionOperator,
|
||||
operators.CMPhantomModeOperator,
|
||||
operators.CMApplyPhantomModeOperator,
|
||||
operators.CMDisableObjectsOperator,
|
||||
operators.CMDisableUnSelectedObjectsOperator,
|
||||
operators.CMRestoreDisabledObjectsOperator,
|
||||
preferences.CMPreferences,
|
||||
ui.CM_UL_items,
|
||||
ui.CollectionManager,
|
||||
ui.CMDisplayOptionsPanel,
|
||||
ui.SpecialsMenu,
|
||||
CollectionManagerProperties,
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_update_post_handler(dummy):
|
||||
if internals.move_triggered:
|
||||
internals.move_triggered = False
|
||||
return
|
||||
|
||||
internals.move_selection.clear()
|
||||
internals.move_active = None
|
||||
|
||||
@persistent
|
||||
def undo_redo_post_handler(dummy):
|
||||
internals.move_selection.clear()
|
||||
internals.move_active = None
|
||||
|
||||
|
||||
def menu_addition(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.operator('view3d.collection_manager')
|
||||
|
||||
if bpy.context.preferences.addons[__package__].preferences.enable_qcd:
|
||||
layout.operator('view3d.qcd_move_widget')
|
||||
|
||||
layout.separator()
|
||||
|
||||
def disable_objects_menu_addition(self, context):
|
||||
preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
if preferences.enable_disable_objects_override:
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.operator('view3d.disable_selected_objects')
|
||||
layout.operator('view3d.disable_unselected_objects')
|
||||
layout.operator('view3d.restore_disabled_objects')
|
||||
|
||||
|
||||
def register_disable_objects_hotkeys():
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon: # not present when started with --background
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Object Mode')
|
||||
kmi = km.keymap_items.new('view3d.disable_selected_objects', 'H', 'PRESS')
|
||||
addon_disable_objects_hotkey_keymaps.append((km, kmi))
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Object Mode')
|
||||
kmi = km.keymap_items.new('view3d.disable_unselected_objects', 'H', 'PRESS', shift=True)
|
||||
addon_disable_objects_hotkey_keymaps.append((km, kmi))
|
||||
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Object Mode')
|
||||
kmi = km.keymap_items.new('view3d.restore_disabled_objects', 'H', 'PRESS', alt=True)
|
||||
addon_disable_objects_hotkey_keymaps.append((km, kmi))
|
||||
|
||||
def unregister_disable_objects_hotkeys():
|
||||
# remove keymaps when disable objects hotkeys are deactivated
|
||||
for km, kmi in addon_disable_objects_hotkey_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_disable_objects_hotkey_keymaps.clear()
|
||||
|
||||
|
||||
def register_cm():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.Scene.collection_manager = PointerProperty(type=CollectionManagerProperties)
|
||||
|
||||
# create the global menu hotkey
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon: # not present when started with --background
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Object Mode')
|
||||
kmi = km.keymap_items.new('view3d.collection_manager', 'M', 'PRESS')
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
# Add Collection Manager & QCD Move Widget to the Object->Collections menu
|
||||
bpy.types.VIEW3D_MT_object_collection.prepend(menu_addition)
|
||||
|
||||
# Add Disable Object Operators to the Object->Show/Hide menu
|
||||
bpy.types.VIEW3D_MT_object_showhide.append(disable_objects_menu_addition)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_post_handler)
|
||||
bpy.app.handlers.undo_post.append(undo_redo_post_handler)
|
||||
bpy.app.handlers.redo_post.append(undo_redo_post_handler)
|
||||
|
||||
preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
if preferences.enable_disable_objects_override:
|
||||
register_disable_objects_hotkeys()
|
||||
|
||||
def unregister_cm():
|
||||
preferences = bpy.context.preferences.addons[__package__].preferences
|
||||
if preferences.enable_disable_objects_override:
|
||||
unregister_disable_objects_hotkeys()
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# Remove Collection Manager & QCD Move Widget from the Object->Collections menu
|
||||
bpy.types.VIEW3D_MT_object_collection.remove(menu_addition)
|
||||
|
||||
# Remove Disable Object Operators to the Object->Show/Hide menu
|
||||
bpy.types.VIEW3D_MT_object_showhide.remove(disable_objects_menu_addition)
|
||||
|
||||
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_post_handler)
|
||||
bpy.app.handlers.undo_post.remove(undo_redo_post_handler)
|
||||
bpy.app.handlers.redo_post.remove(undo_redo_post_handler)
|
||||
|
||||
del bpy.types.Scene.collection_manager
|
||||
|
||||
# remove keymaps when add-on is deactivated
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
|
@ -1387,3 +1387,45 @@ class CMApplyPhantomModeOperator(Operator):
|
|||
cm.in_phantom_mode = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CMDisableObjectsOperator(Operator):
|
||||
'''Disable selected objects in viewports'''
|
||||
bl_label = "Disable Selected"
|
||||
bl_idname = "view3d.disable_selected_objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
obj.hide_viewport = True
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CMDisableUnSelectedObjectsOperator(Operator):
|
||||
'''Disable unselected objects in viewports'''
|
||||
bl_label = "Disable Unselected"
|
||||
bl_idname = "view3d.disable_unselected_objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
for obj in bpy.data.objects:
|
||||
if obj in context.visible_objects and not obj in context.selected_objects:
|
||||
obj.hide_viewport = True
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CMRestoreDisabledObjectsOperator(Operator):
|
||||
'''Restore disabled objects in viewports'''
|
||||
bl_label = "Restore Disabled Objects"
|
||||
bl_idname = "view3d.restore_disabled_objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
for obj in bpy.data.objects:
|
||||
if obj.hide_viewport:
|
||||
obj.hide_viewport = False
|
||||
obj.select_set(True)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
|
|
@ -26,8 +26,16 @@ from bpy.props import (
|
|||
FloatVectorProperty,
|
||||
)
|
||||
|
||||
from . import cm_init
|
||||
from . import qcd_init
|
||||
|
||||
def update_disable_objects_hotkeys_status(self, context):
|
||||
if self.enable_disable_objects_override:
|
||||
cm_init.register_disable_objects_hotkeys()
|
||||
|
||||
else:
|
||||
cm_init.unregister_disable_objects_hotkeys()
|
||||
|
||||
def update_qcd_status(self, context):
|
||||
if self.enable_qcd:
|
||||
qcd_init.register_qcd()
|
||||
|
@ -188,6 +196,17 @@ def set_tooltip_outline(self, values):
|
|||
class CMPreferences(AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
# ENABLE DISABLE OBJECTS OVERRIDE
|
||||
enable_disable_objects_override: BoolProperty(
|
||||
name="Disable Objects Override",
|
||||
description=(
|
||||
"Replace the object hiding hotkeys with object disabling hotkeys and add them to the Object->Show/Hide menu.\n"
|
||||
"Disabling objects prevents them from being automatically shown again when collections are unexcluded"
|
||||
),
|
||||
default=False,
|
||||
update=update_disable_objects_hotkeys_status,
|
||||
)
|
||||
|
||||
# ENABLE QCD BOOLS
|
||||
enable_qcd: BoolProperty(
|
||||
name="QCD",
|
||||
|
@ -426,6 +445,7 @@ class CMPreferences(AddonPreferences):
|
|||
layout = self.layout
|
||||
box = layout.box()
|
||||
|
||||
box.row().prop(self, "enable_disable_objects_override")
|
||||
box.row().prop(self, "enable_qcd")
|
||||
|
||||
if not self.enable_qcd:
|
||||
|
|
|
@ -1,13 +1,44 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# Copyright 2011, Ryan Inch
|
||||
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
|
||||
importlib.reload(internals)
|
||||
importlib.reload(qcd_move_widget)
|
||||
importlib.reload(qcd_operators)
|
||||
importlib.reload(ui)
|
||||
importlib.reload(preferences)
|
||||
|
||||
else:
|
||||
from . import internals
|
||||
from . import qcd_move_widget
|
||||
from . import qcd_operators
|
||||
from . import ui
|
||||
from . import preferences
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import bpy.utils.previews
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from . import internals
|
||||
from . import preferences
|
||||
from . import qcd_move_widget
|
||||
from . import qcd_operators
|
||||
from . import ui
|
||||
|
||||
addon_qcd_keymaps = []
|
||||
addon_qcd_view_hotkey_keymaps = []
|
||||
|
|