-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoperators_content.py
More file actions
321 lines (270 loc) · 11.3 KB
/
Copy pathoperators_content.py
File metadata and controls
321 lines (270 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
import os
import bpy
from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty
from . import page_manager
from .constants import MM_TO_M
def _active_page_or_report(operator, context):
page = page_manager.active_page(context.scene)
if page is None:
operator.report({'ERROR'}, "Create a GreasePress document first")
return None
page_manager.activate_page_collection(page, context.view_layer)
return page
def _select_only(context, obj):
for item in context.selected_objects:
item.select_set(False)
obj.select_set(True)
context.view_layer.objects.active = obj
def _link_exclusively_to_page(obj, page):
if obj.name not in page.objects:
page.objects.link(obj)
for collection in list(obj.users_collection):
if collection != page:
collection.objects.unlink(obj)
class EDITORIALGP_OT_add_grease_pencil(bpy.types.Operator):
bl_idname = "editorial_gp.add_grease_pencil"
bl_label = "Add Grease Pencil"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
page = _active_page_or_report(self, context)
if page is None:
return {'CANCELLED'}
try:
result = bpy.ops.object.grease_pencil_add(type='EMPTY', location=(0.0, 0.0, 0.01))
except Exception as exc:
self.report({'ERROR'}, f"Unable to create Grease Pencil: {exc}")
return {'CANCELLED'}
if result != {'FINISHED'} or context.object is None:
self.report({'ERROR'}, "Grease Pencil creation failed")
return {'CANCELLED'}
obj = context.object
obj.name = "GP_Artwork"
_link_exclusively_to_page(obj, page)
return {'FINISHED'}
class EDITORIALGP_OT_add_text(bpy.types.Operator):
bl_idname = "editorial_gp.add_text"
bl_label = "Add Text"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
page = _active_page_or_report(self, context)
if page is None:
return {'CANCELLED'}
curve = bpy.data.curves.new("Text", type='FONT')
curve.body = "Text"
curve.align_x = 'CENTER'
curve.align_y = 'CENTER'
curve.size = 0.012
obj = bpy.data.objects.new("Text", curve)
obj.location = (0.0, 0.0, 0.012)
page.objects.link(obj)
_select_only(context, obj)
return {'FINISHED'}
class EDITORIALGP_OT_add_background(bpy.types.Operator):
bl_idname = "editorial_gp.add_background"
bl_label = "Add Page Background"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
page = _active_page_or_report(self, context)
if page is None:
return {'CANCELLED'}
width_mm, height_mm = page_manager.page_dimensions_mm(context.scene)
half_w = width_mm * MM_TO_M * 0.5
half_h = height_mm * MM_TO_M * 0.5
mesh = bpy.data.meshes.new("Page Background")
mesh.from_pydata(
[(-half_w, -half_h, 0.0), (half_w, -half_h, 0.0),
(half_w, half_h, 0.0), (-half_w, half_h, 0.0)],
[], [(0, 1, 2, 3)]
)
mesh.update()
obj = bpy.data.objects.new("Page Background", mesh)
obj.location.z = -0.01
obj["greasepress_background"] = True
page.objects.link(obj)
material = bpy.data.materials.new("Page Background")
material.diffuse_color = (1.0, 1.0, 1.0, 1.0)
obj.data.materials.append(material)
_select_only(context, obj)
return {'FINISHED'}
class EDITORIALGP_OT_import_image(bpy.types.Operator, ImportHelper):
bl_idname = "editorial_gp.import_image"
bl_label = "Import Image"
bl_options = {'REGISTER', 'UNDO'}
filename_ext = ""
filter_glob: StringProperty(default="*.png;*.jpg;*.jpeg;*.tif;*.tiff;*.bmp;*.webp", options={'HIDDEN'})
def execute(self, context):
page = _active_page_or_report(self, context)
if page is None:
return {'CANCELLED'}
try:
image = bpy.data.images.load(self.filepath, check_existing=True)
except RuntimeError as exc:
self.report({'ERROR'}, str(exc))
return {'CANCELLED'}
# Use a real textured mesh rather than an Image Empty.
# Image Empties can be forced in front of every object and their
# apparent orientation depends on the viewport side, which makes
# page stacking unreliable.
page_w_mm, page_h_mm = page_manager.page_dimensions_mm(context.scene)
max_w = page_w_mm * MM_TO_M * 0.8
max_h = page_h_mm * MM_TO_M * 0.8
px_w = max(1, int(image.size[0]))
px_h = max(1, int(image.size[1]))
aspect = px_w / px_h
plane_w = max_w
plane_h = plane_w / aspect
if plane_h > max_h:
plane_h = max_h
plane_w = plane_h * aspect
half_w = plane_w * 0.5
half_h = plane_h * 0.5
mesh = bpy.data.meshes.new(f"{image.name}_Mesh")
mesh.from_pydata(
[(-half_w, -half_h, 0.0), (half_w, -half_h, 0.0),
(half_w, half_h, 0.0), (-half_w, half_h, 0.0)],
[], [(0, 1, 2, 3)]
)
mesh.update()
uv_layer = mesh.uv_layers.new(name="UVMap")
# Canonical UV orientation for the Top view used by GreasePress.
# After the switch to Blender's true Top view, the image plane must use
# the standard UV layout to avoid a visible 180° flip in the editor.
uv_values = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))
for loop, uv in zip(mesh.loops, uv_values):
uv_layer.data[loop.index].uv = uv
obj = bpy.data.objects.new(os.path.splitext(os.path.basename(self.filepath))[0], mesh)
obj.location = (0.0, 0.0, 0.005)
obj["greasepress_image"] = True
page.objects.link(obj)
material = bpy.data.materials.new(f"{image.name}_Material")
material.use_nodes = True
nodes = material.node_tree.nodes
links = material.node_tree.links
nodes.clear()
output = nodes.new("ShaderNodeOutputMaterial")
shader = nodes.new("ShaderNodeBsdfPrincipled")
texture = nodes.new("ShaderNodeTexImage")
texture.image = image
links.new(texture.outputs["Color"], shader.inputs["Base Color"])
links.new(texture.outputs["Alpha"], shader.inputs["Alpha"])
links.new(shader.outputs["BSDF"], output.inputs["Uprface"])
shader.inputs["Roughness"].default_value = 1.0
material.diffuse_color = (1.0, 1.0, 1.0, 1.0)
# Blender versions expose transparency through different material
# properties; set the available one without making registration fail.
if hasattr(material, "surface_render_method"):
try:
material.surface_render_method = 'DITHERED'
except Exception:
pass
elif hasattr(material, "blend_method"):
try:
material.blend_method = 'BLEND'
except Exception:
pass
mesh.materials.append(material)
# Ensure the imported bitmap is visible immediately in the 3D View.
# In Solid shading Blender otherwise displays textured planes as a
# flat grey rectangle. Material Preview renders the image material
# consistently in Blender 5.x.
for area in context.screen.areas if context.screen else []:
if area.type != 'VIEW_3D':
continue
space = area.spaces.active
if hasattr(space, "shading") and space.shading.type == 'SOLID':
space.shading.type = 'MATERIAL'
_select_only(context, obj)
return {'FINISHED'}
class EDITORIALGP_OT_move_selected_to_page(bpy.types.Operator):
bl_idname = "editorial_gp.move_selected_to_page"
bl_label = "Move Selected to Page"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return bool(context.selected_objects) and page_manager.active_page(context.scene) is not None
def execute(self, context):
page = _active_page_or_report(self, context)
moved = 0
for obj in list(context.selected_objects):
if obj.get("editorial_gp_guide"):
continue
_link_exclusively_to_page(obj, page)
moved += 1
self.report({'INFO'}, f"{moved} objects assigned to the current page")
return {'FINISHED'}
class EDITORIALGP_OT_select_page_contents(bpy.types.Operator):
bl_idname = "editorial_gp.select_page_contents"
bl_label = "Select Page Contents"
def execute(self, context):
page = _active_page_or_report(self, context)
if page is None:
return {'CANCELLED'}
bpy.ops.object.select_all(action='DESELECT')
for obj in page.objects:
obj.select_set(True)
if page.objects:
context.view_layer.objects.active = page.objects[0]
return {'FINISHED'}
class _StackBase:
direction = 1.0
@classmethod
def poll(cls, context):
return bool(context.selected_objects) and page_manager.active_page(context.scene) is not None
def execute(self, context):
page = page_manager.active_page(context.scene)
selected = [obj for obj in context.selected_objects if obj.name in page.objects]
if not selected:
self.report({'WARNING'}, "No object selected on the current page")
return {'CANCELLED'}
values = [obj.location.z for obj in page.objects if not obj.get("greasepress_background")]
anchor = (max(values) if self.direction > 0 else min(values)) if values else 0.0
for offset, obj in enumerate(selected, start=1):
obj.location.z = anchor + self.direction * 0.001 * offset
return {'FINISHED'}
class EDITORIALGP_OT_bring_forward(_StackBase, bpy.types.Operator):
bl_idname = "editorial_gp.bring_forward"
bl_label = "Bring Forward"
bl_options = {'REGISTER', 'UNDO'}
direction = 1.0
class EDITORIALGP_OT_send_backward(_StackBase, bpy.types.Operator):
bl_idname = "editorial_gp.send_backward"
bl_label = "Send Backward"
bl_options = {'REGISTER', 'UNDO'}
direction = -1.0
def normalize_image_uv(obj):
"""Normalize a GreasePress image plane for the canonical Top view."""
if not obj or not obj.get("greasepress_image") or obj.type != 'MESH':
return False
mesh = obj.data
if not mesh or not mesh.uv_layers or len(mesh.loops) < 4:
return False
uv_layer = mesh.uv_layers.active or mesh.uv_layers[0]
uv_values = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))
for loop, uv in zip(mesh.loops[:4], uv_values):
uv_layer.data[loop.index].uv = uv
obj["greasepress_uv_orientation"] = "TOP_VIEW_STANDARD"
mesh.update()
return True
def normalize_all_image_uvs():
changed = 0
for obj in bpy.data.objects:
if normalize_image_uv(obj):
changed += 1
return changed
classes = (
EDITORIALGP_OT_add_grease_pencil,
EDITORIALGP_OT_add_text,
EDITORIALGP_OT_add_background,
EDITORIALGP_OT_import_image,
EDITORIALGP_OT_move_selected_to_page,
EDITORIALGP_OT_select_page_contents,
EDITORIALGP_OT_bring_forward,
EDITORIALGP_OT_send_backward,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)