GPencil Tools: Optimize Undo for Rotate Canvas
[blender-addons.git] / archimesh / achm_main_panel.py
blobd02087dae3add3c7a17aaa9645221df501d22ed3
1 # SPDX-FileCopyrightText: 2016-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ----------------------------------------------------------
6 # Main panel for different Archimesh general actions
7 # Author: Antonio Vazquez (antonioya)
9 # ----------------------------------------------------------
10 # noinspection PyUnresolvedReferences
11 import bpy
12 # noinspection PyUnresolvedReferences
13 from bpy.types import Operator, Panel, SpaceView3D
14 from math import sqrt, fabs, pi, asin
15 from .achm_tools import *
16 from .achm_gltools import *
19 # -----------------------------------------------------
20 # Verify if boolean already exist
21 # -----------------------------------------------------
22 def isboolean(myobject, childobject):
23 flag = False
24 for mod in myobject.modifiers:
25 if mod.type == 'BOOLEAN':
26 if mod.object == childobject:
27 flag = True
28 break
29 return flag
32 # ------------------------------------------------------
33 # Button: Action to link windows and doors
34 # ------------------------------------------------------
35 class ARCHIMESH_OT_Hole(Operator):
36 bl_idname = "object.archimesh_cut_holes"
37 bl_label = "Auto Holes"
38 bl_description = "Enable windows and doors holes for any selected object (needs wall thickness)"
39 bl_category = 'View'
40 bl_options = {'UNDO', 'REGISTER'}
42 # ------------------------------
43 # Execute
44 # ------------------------------
45 # noinspection PyMethodMayBeStatic
46 def execute(self, context):
47 scene = context.scene
48 listobj = []
49 # ---------------------------------------------------------------------
50 # Save the list of selected objects because the select flag is missed
51 # only can be windows or doors
52 # ---------------------------------------------------------------------
53 for obj in bpy.context.scene.objects:
54 # noinspection PyBroadException
55 try:
56 if obj["archimesh.hole_enable"]:
57 if obj.select_get() is True or scene.archimesh_select_only is False:
58 listobj.extend([obj])
59 except:
60 continue
61 # ---------------------------
62 # Get the baseboard object
63 # ---------------------------
64 mybaseboard = None
65 for child in context.object.children:
66 # noinspection PyBroadException
67 try:
68 if child["archimesh.room_baseboard"]:
69 mybaseboard = child
70 except:
71 continue
72 # ---------------------------
73 # Get the shell object
74 # ---------------------------
75 myshell = None
76 for child in context.object.children:
77 # noinspection PyBroadException
78 try:
79 if child["archimesh.room_shell"]:
80 myshell = child
81 except:
82 continue
84 # -----------------------------
85 # Remove all empty Boolean modifiers
86 # -----------------------------
87 for mod in context.object.modifiers:
88 if mod.type == 'BOOLEAN':
89 if mod.object is None:
90 bpy.ops.object.modifier_remove(modifier=mod.name)
92 # if thickness is 0, must be > 0
93 myroom = context.object
94 if myroom.RoomGenerator[0].wall_width == 0:
95 self.report({'WARNING'}, "Walls must have thickness for using autohole function. Change it and run again")
96 # -----------------------------
97 # Now apply Wall holes
98 # -----------------------------
99 for obj in listobj:
100 parentobj = context.object
101 # Parent the empty to the room (the parent of frame)
102 if obj.parent is not None:
103 bpy.ops.object.select_all(action='DESELECT')
104 parentobj.select_set(True)
105 obj.parent.select_set(True) # parent of object
106 bpy.ops.object.parent_set(type='OBJECT', keep_transform=False)
107 # ---------------------------------------
108 # Add the modifier to controller
109 # and the scale to use the same thickness
110 # ---------------------------------------
111 for child in obj.parent.children:
112 # noinspection PyBroadException
113 if "archimesh.ctrl_hole" in child and child["archimesh.ctrl_hole"]:
114 # apply scale
115 t = parentobj.RoomGenerator[0].wall_width
116 if t > 0:
117 child.scale.y = (t + 0.45) / (child.dimensions.y / child.scale.y) # Add some gap
118 else:
119 child.scale.y = 1
120 # add boolean modifier
121 if not isboolean(myroom, child):
122 set_modifier_boolean(myroom, child)
124 # ---------------------------------------
125 # Now add the modifiers to baseboard
126 # ---------------------------------------
127 if mybaseboard is not None:
128 for obj in bpy.context.scene.objects:
129 # noinspection PyBroadException
130 try:
131 if obj["archimesh.ctrl_base"]:
132 if obj.select_get() is True or scene.archimesh_select_only is False:
133 # add boolean modifier
134 if isboolean(mybaseboard, obj) is False:
135 set_modifier_boolean(mybaseboard, obj)
136 except:
137 pass
139 # ---------------------------------------
140 # Now add the modifiers to shell
141 # ---------------------------------------
142 if myshell is not None:
143 # Remove all empty Boolean modifiers
144 for mod in myshell.modifiers:
145 if mod.type == 'BOOLEAN':
146 if mod.object is None:
147 bpy.ops.object.modifier_remove(modifier=mod.name)
149 for obj in bpy.context.scene.objects:
150 # noinspection PyBroadException
151 try:
152 if obj["archimesh.ctrl_hole"]:
153 if obj.select_get() is True or scene.archimesh_select_only is False:
154 # add boolean modifier
155 if isboolean(myshell, obj) is False:
156 set_modifier_boolean(myshell, obj)
157 except:
158 pass
160 return {'FINISHED'}
163 # ------------------------------------------------------
164 # Button: Action to create room from grease pencil
165 # ------------------------------------------------------
166 class ARCHIMESH_OT_Pencil(Operator):
167 bl_idname = "object.archimesh_pencil_room"
168 bl_label = "Room from Draw"
169 bl_description = "Create a room base on grease pencil strokes (draw from top view (7 key))"
170 bl_category = 'View'
171 bl_options = {'UNDO', 'REGISTER'}
173 # ------------------------------
174 # Execute
175 # ------------------------------
176 def execute(self, context):
177 # Enable for debugging code
178 debugmode = False
180 scene = context.scene
181 mypoints = None
182 clearangles = None
184 if debugmode is True:
185 print("======================================================================")
186 print("== ==")
187 print("== Grease pencil strokes analysis ==")
188 print("== ==")
189 print("======================================================================")
191 # -----------------------------------
192 # Get grease pencil points
193 # -----------------------------------
194 # noinspection PyBroadException
195 try:
197 # noinspection PyBroadException
198 try:
199 pencil = bpy.context.object.grease_pencil.layers.active
200 except:
201 pencil = bpy.context.scene.grease_pencil.layers.active
203 if pencil.active_frame is not None:
204 for i, stroke in enumerate(pencil.active_frame.strokes):
205 stroke_points = pencil.active_frame.strokes[i].points
206 allpoints = [(point.co.x, point.co.y)
207 for point in stroke_points]
209 mypoints = []
210 idx = 0
211 x = 0
212 y = 0
213 orientation = None
214 old_orientation = None
216 for point in allpoints:
217 if idx == 0:
218 x = point[0]
219 y = point[1]
220 else:
221 abs_x = abs(point[0] - x)
222 abs_y = abs(point[1] - y)
224 if abs_y > abs_x:
225 orientation = "V"
226 else:
227 orientation = "H"
229 if old_orientation == orientation:
230 x = point[0]
231 y = point[1]
232 else:
233 mypoints.extend([(x, y)])
234 x = point[0]
235 y = point[1]
236 old_orientation = orientation
238 idx += 1
239 # Last point
240 mypoints.extend([(x, y)])
242 if debugmode is True:
243 print("\nPoints\n====================")
244 i = 0
245 for p in mypoints:
246 print(str(i) + ":" + str(p))
247 i += 1
248 # -----------------------------------
249 # Calculate distance between points
250 # -----------------------------------
251 if debugmode is True:
252 print("\nDistance\n====================")
253 i = len(mypoints)
254 distlist = []
255 for e in range(1, i):
256 d = sqrt(
257 ((mypoints[e][0] - mypoints[e - 1][0]) ** 2) + ((mypoints[e][1] - mypoints[e - 1][1]) ** 2))
258 # Imperial units if needed
259 if bpy.context.scene.unit_settings.system == "IMPERIAL":
260 d *= 3.2808399
262 distlist.extend([d])
264 if debugmode is True:
265 print(str(e - 1) + ":" + str(d))
266 # -----------------------------------
267 # Calculate angle of walls
268 # clamped to right angles
269 # -----------------------------------
270 if debugmode is True:
271 print("\nAngle\n====================")
273 i = len(mypoints)
274 anglelist = []
275 for e in range(1, i):
276 sinv = (mypoints[e][1] - mypoints[e - 1][1]) / sqrt(
277 ((mypoints[e][0] - mypoints[e - 1][0]) ** 2) + ((mypoints[e][1] - mypoints[e - 1][1]) ** 2))
278 a = asin(sinv)
279 # Clamp to 90 or 0 degrees
280 if fabs(a) > pi / 4:
281 b = pi / 2
282 else:
283 b = 0
285 anglelist.extend([b])
286 # Reverse de distance using angles (inverse angle to axis) for Vertical lines
287 if a < 0.0 and b != 0:
288 distlist[e - 1] *= -1 # reverse distance
290 # Reverse de distance for horizontal lines
291 if b == 0:
292 if mypoints[e - 1][0] > mypoints[e][0]:
293 distlist[e - 1] *= -1 # reverse distance
295 if debugmode is True:
296 print(str(e - 1) + ":" + str((a * 180) / pi) + "...:" + str(
297 (b * 180) / pi) + "--->" + str(distlist[e - 1]))
299 # ---------------------------------------
300 # Verify duplications and reduce noise
301 # ---------------------------------------
302 if len(anglelist) >= 1:
303 clearangles = []
304 cleardistan = []
305 i = len(anglelist)
306 oldangle = anglelist[0]
307 olddist = 0
308 for e in range(0, i):
309 if oldangle != anglelist[e]:
310 clearangles.extend([oldangle])
311 cleardistan.extend([olddist])
312 oldangle = anglelist[e]
313 olddist = distlist[e]
314 else:
315 olddist += distlist[e]
316 # last
317 clearangles.extend([oldangle])
318 cleardistan.extend([olddist])
320 # ----------------------------
321 # Create the room
322 # ----------------------------
323 if len(mypoints) > 1 and len(clearangles) > 0:
324 # Move cursor
325 bpy.context.scene.cursor.location.x = mypoints[0][0]
326 bpy.context.scene.cursor.location.y = mypoints[0][1]
327 bpy.context.scene.cursor.location.z = 0 # always on grid floor
329 # Add room mesh
330 bpy.ops.mesh.archimesh_room()
331 myroom = context.object
332 mydata = myroom.RoomGenerator[0]
333 # Number of walls
334 mydata.wall_num = len(mypoints) - 1
335 mydata.ceiling = scene.archimesh_ceiling
336 mydata.floor = scene.archimesh_floor
337 mydata.merge = scene.archimesh_merge
339 i = len(mypoints)
340 for e in range(0, i - 1):
341 if clearangles[e] == pi / 2:
342 if cleardistan[e] > 0:
343 mydata.walls[e].w = round(fabs(cleardistan[e]), 2)
344 mydata.walls[e].r = (fabs(clearangles[e]) * 180) / pi # from radians
345 else:
346 mydata.walls[e].w = round(fabs(cleardistan[e]), 2)
347 mydata.walls[e].r = (fabs(clearangles[e]) * 180 * -1) / pi # from radians
349 else:
350 mydata.walls[e].w = round(cleardistan[e], 2)
351 mydata.walls[e].r = (fabs(clearangles[e]) * 180) / pi # from radians
353 # Remove Grease pencil
354 if pencil is not None:
355 for frame in pencil.frames:
356 pencil.frames.remove(frame)
358 self.report({'INFO'}, "Archimesh: Room created from grease pencil strokes")
359 else:
360 self.report({'WARNING'}, "Archimesh: Not enough grease pencil strokes for creating room.")
362 return {'FINISHED'}
363 except:
364 self.report({'WARNING'}, "Archimesh: No grease pencil strokes. Do strokes in top view before creating room")
365 return {'CANCELLED'}
368 # ------------------------------------------------------------------
369 # Define panel class for main functions.
370 # ------------------------------------------------------------------
371 class ARCHIMESH_PT_Main(Panel):
372 bl_idname = "ARCHIMESH_PT_main"
373 bl_label = "Archimesh"
374 bl_space_type = "VIEW_3D"
375 bl_region_type = "UI"
376 bl_category = "Create"
377 bl_context = "objectmode"
378 bl_options = {'DEFAULT_CLOSED'}
380 # ------------------------------
381 # Draw UI
382 # ------------------------------
383 def draw(self, context):
384 layout = self.layout
385 scene = context.scene
387 myobj = context.object
388 # -------------------------------------------------------------------------
389 # If the selected object didn't be created with the group 'RoomGenerator',
390 # this button is not created.
391 # -------------------------------------------------------------------------
392 # noinspection PyBroadException
393 try:
394 if 'RoomGenerator' in myobj:
395 box = layout.box()
396 box.label(text="Room Tools", icon='MODIFIER')
397 row = box.row(align=False)
398 row.operator("object.archimesh_cut_holes", icon='GRID')
399 row.prop(scene, "archimesh_select_only")
401 # Export/Import
402 row = box.row(align=False)
403 row.operator("io_import.roomdata", text="Import", icon='COPYDOWN')
404 row.operator("io_export.roomdata", text="Export", icon='PASTEDOWN')
405 except:
406 pass
408 # -------------------------------------------------------------------------
409 # If the selected object isn't a kitchen
410 # this button is not created.
411 # -------------------------------------------------------------------------
412 # noinspection PyBroadException
413 try:
414 if myobj["archimesh.sku"] is not None:
415 box = layout.box()
416 box.label(text="Kitchen Tools", icon='MODIFIER')
417 # Export
418 row = box.row(align=False)
419 row.operator("io_export.kitchen_inventory", text="Export inventory", icon='PASTEDOWN')
420 except:
421 pass
423 # ------------------------------
424 # Elements Buttons
425 # ------------------------------
426 box = layout.box()
427 box.label(text="Elements", icon='GROUP')
428 row = box.row()
429 row.operator("mesh.archimesh_room")
430 row.operator("mesh.archimesh_column")
431 row = box.row()
432 row.operator("mesh.archimesh_door")
433 row = box.row()
434 row.operator("mesh.archimesh_window")
435 row.operator("mesh.archimesh_winpanel")
436 row = box.row()
437 row.operator("mesh.archimesh_kitchen")
438 row.operator("mesh.archimesh_shelves")
439 row = box.row()
440 row.operator("mesh.archimesh_stairs")
441 row.operator("mesh.archimesh_roof")
443 # ------------------------------
444 # Prop Buttons
445 # ------------------------------
446 box = layout.box()
447 box.label(text="Props", icon='LIGHT_DATA')
448 row = box.row()
449 row.operator("mesh.archimesh_books")
450 row.operator("mesh.archimesh_light")
451 row = box.row()
452 row.operator("mesh.archimesh_venetian")
453 row.operator("mesh.archimesh_roller")
454 row = box.row()
455 row.operator("mesh.archimesh_japan")
457 # ------------------------------
458 # OpenGL Buttons
459 # ------------------------------
460 box = layout.box()
461 box.label(text="Display hints", icon='QUESTION')
462 row = box.row()
463 if context.window_manager.archimesh_run_opengl is False:
464 icon = 'PLAY'
465 txt = 'Show'
466 else:
467 icon = "PAUSE"
468 txt = 'Hide'
469 row.operator("archimesh.runopenglbutton", text=txt, icon=icon)
470 row = box.row()
471 row.prop(scene, "archimesh_gl_measure", toggle=True, icon="ALIGN_CENTER")
472 row.prop(scene, "archimesh_gl_name", toggle=True, icon="OUTLINER_OB_FONT")
473 row.prop(scene, "archimesh_gl_ghost", icon='GHOST_ENABLED')
474 row = box.row()
475 row.prop(scene, "archimesh_text_color", text="")
476 row.prop(scene, "archimesh_walltext_color", text="")
477 row = box.row()
478 row.prop(scene, "archimesh_font_size")
479 row.prop(scene, "archimesh_wfont_size")
480 row = box.row()
481 row.prop(scene, "archimesh_hint_space")
482 # ------------------------------
483 # Grease pencil tools
484 # ------------------------------
485 box = layout.box()
486 box.label(text="Pencil Tools", icon='MODIFIER')
487 row = box.row(align=False)
488 row.operator("object.archimesh_pencil_room", icon='GREASEPENCIL')
489 row = box.row(align=False)
490 row.prop(scene, "archimesh_ceiling")
491 row.prop(scene, "archimesh_floor")
492 row.prop(scene, "archimesh_merge")
495 # -------------------------------------------------------------
496 # Defines button for enable/disable the tip display
498 # -------------------------------------------------------------
499 class ARCHIMESH_OT_HintDisplay(Operator):
500 bl_idname = "archimesh.runopenglbutton"
501 bl_label = "Display hint data manager"
502 bl_description = "Display additional information in the viewport"
503 bl_category = 'View'
505 _handle = None # keep function handler
507 # ----------------------------------
508 # Enable gl drawing adding handler
509 # ----------------------------------
510 @staticmethod
511 def handle_add(self, context):
512 if ARCHIMESH_OT_HintDisplay._handle is None:
513 ARCHIMESH_OT_HintDisplay._handle = SpaceView3D.draw_handler_add(draw_callback_px, (self, context),
514 'WINDOW',
515 'POST_PIXEL')
516 context.window_manager.archimesh_run_opengl = True
518 # ------------------------------------
519 # Disable gl drawing removing handler
520 # ------------------------------------
521 # noinspection PyUnusedLocal
522 @staticmethod
523 def handle_remove(self, context):
524 if ARCHIMESH_OT_HintDisplay._handle is not None:
525 SpaceView3D.draw_handler_remove(ARCHIMESH_OT_HintDisplay._handle, 'WINDOW')
526 ARCHIMESH_OT_HintDisplay._handle = None
527 context.window_manager.archimesh_run_opengl = False
529 # ------------------------------
530 # Execute button action
531 # ------------------------------
532 def execute(self, context):
533 if context.area.type == 'VIEW_3D':
534 if context.window_manager.archimesh_run_opengl is False:
535 self.handle_add(self, context)
536 context.area.tag_redraw()
537 else:
538 self.handle_remove(self, context)
539 context.area.tag_redraw()
541 return {'FINISHED'}
542 else:
543 self.report({'WARNING'},
544 "View3D not found, cannot run operator")
546 return {'CANCELLED'}
549 # -------------------------------------------------------------
550 # Handler for drawing OpenGl
551 # -------------------------------------------------------------
552 # noinspection PyUnusedLocal
553 def draw_callback_px(self, context):
554 draw_main(context)