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
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
):
24 for mod
in myobject
.modifiers
:
25 if mod
.type == 'BOOLEAN':
26 if mod
.object == childobject
:
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)"
40 bl_options
= {'UNDO', 'REGISTER'}
42 # ------------------------------
44 # ------------------------------
45 # noinspection PyMethodMayBeStatic
46 def execute(self
, context
):
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
56 if obj
["archimesh.hole_enable"]:
57 if obj
.select_get() is True or scene
.archimesh_select_only
is False:
61 # ---------------------------
62 # Get the baseboard object
63 # ---------------------------
65 for child
in context
.object.children
:
66 # noinspection PyBroadException
68 if child
["archimesh.room_baseboard"]:
72 # ---------------------------
73 # Get the shell object
74 # ---------------------------
76 for child
in context
.object.children
:
77 # noinspection PyBroadException
79 if child
["archimesh.room_shell"]:
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 # -----------------------------
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"]:
115 t
= parentobj
.RoomGenerator
[0].wall_width
117 child
.scale
.y
= (t
+ 0.45) / (child
.dimensions
.y
/ child
.scale
.y
) # Add some gap
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
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
)
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
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
)
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))"
171 bl_options
= {'UNDO', 'REGISTER'}
173 # ------------------------------
175 # ------------------------------
176 def execute(self
, context
):
177 # Enable for debugging code
180 scene
= context
.scene
184 if debugmode
is True:
185 print("======================================================================")
187 print("== Grease pencil strokes analysis ==")
189 print("======================================================================")
191 # -----------------------------------
192 # Get grease pencil points
193 # -----------------------------------
194 # noinspection PyBroadException
197 # noinspection PyBroadException
199 pencil
= bpy
.context
.object.grease_pencil
.layers
.active
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
]
214 old_orientation
= None
216 for point
in allpoints
:
221 abs_x
= abs(point
[0] - x
)
222 abs_y
= abs(point
[1] - y
)
229 if old_orientation
== orientation
:
233 mypoints
.extend([(x
, y
)])
236 old_orientation
= orientation
240 mypoints
.extend([(x
, y
)])
242 if debugmode
is True:
243 print("\nPoints\n====================")
246 print(str(i
) + ":" + str(p
))
248 # -----------------------------------
249 # Calculate distance between points
250 # -----------------------------------
251 if debugmode
is True:
252 print("\nDistance\n====================")
255 for e
in range(1, i
):
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":
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====================")
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))
279 # Clamp to 90 or 0 degrees
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
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:
306 oldangle
= anglelist
[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
]
315 olddist
+= distlist
[e
]
317 clearangles
.extend([oldangle
])
318 cleardistan
.extend([olddist
])
320 # ----------------------------
322 # ----------------------------
323 if len(mypoints
) > 1 and len(clearangles
) > 0:
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
330 bpy
.ops
.mesh
.archimesh_room()
331 myroom
= context
.object
332 mydata
= myroom
.RoomGenerator
[0]
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
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
346 mydata
.walls
[e
].w
= round(fabs(cleardistan
[e
]), 2)
347 mydata
.walls
[e
].r
= (fabs(clearangles
[e
]) * 180 * -1) / pi
# from radians
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")
360 self
.report({'WARNING'}, "Archimesh: Not enough grease pencil strokes for creating room.")
364 self
.report({'WARNING'}, "Archimesh: No grease pencil strokes. Do strokes in top view before creating room")
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 # ------------------------------
382 # ------------------------------
383 def draw(self
, context
):
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
394 if 'RoomGenerator' in myobj
:
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")
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')
408 # -------------------------------------------------------------------------
409 # If the selected object isn't a kitchen
410 # this button is not created.
411 # -------------------------------------------------------------------------
412 # noinspection PyBroadException
414 if myobj
["archimesh.sku"] is not None:
416 box
.label(text
="Kitchen Tools", icon
='MODIFIER')
418 row
= box
.row(align
=False)
419 row
.operator("io_export.kitchen_inventory", text
="Export inventory", icon
='PASTEDOWN')
423 # ------------------------------
425 # ------------------------------
427 box
.label(text
="Elements", icon
='GROUP')
429 row
.operator("mesh.archimesh_room")
430 row
.operator("mesh.archimesh_column")
432 row
.operator("mesh.archimesh_door")
434 row
.operator("mesh.archimesh_window")
435 row
.operator("mesh.archimesh_winpanel")
437 row
.operator("mesh.archimesh_kitchen")
438 row
.operator("mesh.archimesh_shelves")
440 row
.operator("mesh.archimesh_stairs")
441 row
.operator("mesh.archimesh_roof")
443 # ------------------------------
445 # ------------------------------
447 box
.label(text
="Props", icon
='LIGHT_DATA')
449 row
.operator("mesh.archimesh_books")
450 row
.operator("mesh.archimesh_light")
452 row
.operator("mesh.archimesh_venetian")
453 row
.operator("mesh.archimesh_roller")
455 row
.operator("mesh.archimesh_japan")
457 # ------------------------------
459 # ------------------------------
461 box
.label(text
="Display hints", icon
='QUESTION')
463 if context
.window_manager
.archimesh_run_opengl
is False:
469 row
.operator("archimesh.runopenglbutton", text
=txt
, icon
=icon
)
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')
475 row
.prop(scene
, "archimesh_text_color", text
="")
476 row
.prop(scene
, "archimesh_walltext_color", text
="")
478 row
.prop(scene
, "archimesh_font_size")
479 row
.prop(scene
, "archimesh_wfont_size")
481 row
.prop(scene
, "archimesh_hint_space")
482 # ------------------------------
483 # Grease pencil tools
484 # ------------------------------
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"
505 _handle
= None # keep function handler
507 # ----------------------------------
508 # Enable gl drawing adding handler
509 # ----------------------------------
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
),
516 context
.window_manager
.archimesh_run_opengl
= True
518 # ------------------------------------
519 # Disable gl drawing removing handler
520 # ------------------------------------
521 # noinspection PyUnusedLocal
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()
538 self
.handle_remove(self
, context
)
539 context
.area
.tag_redraw()
543 self
.report({'WARNING'},
544 "View3D not found, cannot run operator")
549 # -------------------------------------------------------------
550 # Handler for drawing OpenGl
551 # -------------------------------------------------------------
552 # noinspection PyUnusedLocal
553 def draw_callback_px(self
, context
):