Files
blender-addons/archimesh/achm_main_panel.py
Antonio Vazquez d9d24cef3e Archimesh: Register Undo for some missing operators
Two operators were missing the option to enable the Undo registering.
2022-08-16 15:20:48 +02:00

557 lines
22 KiB
Python

# SPDX-License-Identifier: GPL-2.0-or-later
# ----------------------------------------------------------
# Main panel for different Archimesh general actions
# Author: Antonio Vazquez (antonioya)
#
# ----------------------------------------------------------
# noinspection PyUnresolvedReferences
import bpy
# noinspection PyUnresolvedReferences
from bpy.types import Operator, Panel, SpaceView3D
from math import sqrt, fabs, pi, asin
from .achm_tools import *
from .achm_gltools import *
# -----------------------------------------------------
# Verify if boolean already exist
# -----------------------------------------------------
def isboolean(myobject, childobject):
flag = False
for mod in myobject.modifiers:
if mod.type == 'BOOLEAN':
if mod.object == childobject:
flag = True
break
return flag
# ------------------------------------------------------
# Button: Action to link windows and doors
# ------------------------------------------------------
class ARCHIMESH_OT_Hole(Operator):
bl_idname = "object.archimesh_cut_holes"
bl_label = "Auto Holes"
bl_description = "Enable windows and doors holes for any selected object (needs wall thickness)"
bl_category = 'View'
bl_options = {'UNDO', 'REGISTER'}
# ------------------------------
# Execute
# ------------------------------
# noinspection PyMethodMayBeStatic
def execute(self, context):
scene = context.scene
listobj = []
# ---------------------------------------------------------------------
# Save the list of selected objects because the select flag is missed
# only can be windows or doors
# ---------------------------------------------------------------------
for obj in bpy.context.scene.objects:
# noinspection PyBroadException
try:
if obj["archimesh.hole_enable"]:
if obj.select_get() is True or scene.archimesh_select_only is False:
listobj.extend([obj])
except:
continue
# ---------------------------
# Get the baseboard object
# ---------------------------
mybaseboard = None
for child in context.object.children:
# noinspection PyBroadException
try:
if child["archimesh.room_baseboard"]:
mybaseboard = child
except:
continue
# ---------------------------
# Get the shell object
# ---------------------------
myshell = None
for child in context.object.children:
# noinspection PyBroadException
try:
if child["archimesh.room_shell"]:
myshell = child
except:
continue
# -----------------------------
# Remove all empty Boolean modifiers
# -----------------------------
for mod in context.object.modifiers:
if mod.type == 'BOOLEAN':
if mod.object is None:
bpy.ops.object.modifier_remove(modifier=mod.name)
# if thickness is 0, must be > 0
myroom = context.object
if myroom.RoomGenerator[0].wall_width == 0:
self.report({'WARNING'}, "Walls must have thickness for using autohole function. Change it and run again")
# -----------------------------
# Now apply Wall holes
# -----------------------------
for obj in listobj:
parentobj = context.object
# Parent the empty to the room (the parent of frame)
if obj.parent is not None:
bpy.ops.object.select_all(action='DESELECT')
parentobj.select_set(True)
obj.parent.select_set(True) # parent of object
bpy.ops.object.parent_set(type='OBJECT', keep_transform=False)
# ---------------------------------------
# Add the modifier to controller
# and the scale to use the same thickness
# ---------------------------------------
for child in obj.parent.children:
# noinspection PyBroadException
try:
if child["archimesh.ctrl_hole"]:
# apply scale
t = parentobj.RoomGenerator[0].wall_width
if t > 0:
child.scale.y = (t + 0.45) / (child.dimensions.y / child.scale.y) # Add some gap
else:
child.scale.y = 1
# add boolean modifier
if isboolean(myroom, child) is False:
set_modifier_boolean(myroom, child)
except:
# print("Unexpected error:" + str(sys.exc_info()))
pass
# ---------------------------------------
# Now add the modifiers to baseboard
# ---------------------------------------
if mybaseboard is not None:
for obj in bpy.context.scene.objects:
# noinspection PyBroadException
try:
if obj["archimesh.ctrl_base"]:
if obj.select_get() is True or scene.archimesh_select_only is False:
# add boolean modifier
if isboolean(mybaseboard, obj) is False:
set_modifier_boolean(mybaseboard, obj)
except:
pass
# ---------------------------------------
# Now add the modifiers to shell
# ---------------------------------------
if myshell is not None:
# Remove all empty Boolean modifiers
for mod in myshell.modifiers:
if mod.type == 'BOOLEAN':
if mod.object is None:
bpy.ops.object.modifier_remove(modifier=mod.name)
for obj in bpy.context.scene.objects:
# noinspection PyBroadException
try:
if obj["archimesh.ctrl_hole"]:
if obj.select_get() is True or scene.archimesh_select_only is False:
# add boolean modifier
if isboolean(myshell, obj) is False:
set_modifier_boolean(myshell, obj)
except:
pass
return {'FINISHED'}
# ------------------------------------------------------
# Button: Action to create room from grease pencil
# ------------------------------------------------------
class ARCHIMESH_OT_Pencil(Operator):
bl_idname = "object.archimesh_pencil_room"
bl_label = "Room from Draw"
bl_description = "Create a room base on grease pencil strokes (draw from top view (7 key))"
bl_category = 'View'
bl_options = {'UNDO', 'REGISTER'}
# ------------------------------
# Execute
# ------------------------------
def execute(self, context):
# Enable for debugging code
debugmode = False
scene = context.scene
mypoints = None
clearangles = None
if debugmode is True:
print("======================================================================")
print("== ==")
print("== Grease pencil strokes analysis ==")
print("== ==")
print("======================================================================")
# -----------------------------------
# Get grease pencil points
# -----------------------------------
# noinspection PyBroadException
try:
# noinspection PyBroadException
try:
pencil = bpy.context.object.grease_pencil.layers.active
except:
pencil = bpy.context.scene.grease_pencil.layers.active
if pencil.active_frame is not None:
for i, stroke in enumerate(pencil.active_frame.strokes):
stroke_points = pencil.active_frame.strokes[i].points
allpoints = [(point.co.x, point.co.y)
for point in stroke_points]
mypoints = []
idx = 0
x = 0
y = 0
orientation = None
old_orientation = None
for point in allpoints:
if idx == 0:
x = point[0]
y = point[1]
else:
abs_x = abs(point[0] - x)
abs_y = abs(point[1] - y)
if abs_y > abs_x:
orientation = "V"
else:
orientation = "H"
if old_orientation == orientation:
x = point[0]
y = point[1]
else:
mypoints.extend([(x, y)])
x = point[0]
y = point[1]
old_orientation = orientation
idx += 1
# Last point
mypoints.extend([(x, y)])
if debugmode is True:
print("\nPoints\n====================")
i = 0
for p in mypoints:
print(str(i) + ":" + str(p))
i += 1
# -----------------------------------
# Calculate distance between points
# -----------------------------------
if debugmode is True:
print("\nDistance\n====================")
i = len(mypoints)
distlist = []
for e in range(1, i):
d = sqrt(
((mypoints[e][0] - mypoints[e - 1][0]) ** 2) + ((mypoints[e][1] - mypoints[e - 1][1]) ** 2))
# Imperial units if needed
if bpy.context.scene.unit_settings.system == "IMPERIAL":
d *= 3.2808399
distlist.extend([d])
if debugmode is True:
print(str(e - 1) + ":" + str(d))
# -----------------------------------
# Calculate angle of walls
# clamped to right angles
# -----------------------------------
if debugmode is True:
print("\nAngle\n====================")
i = len(mypoints)
anglelist = []
for e in range(1, i):
sinv = (mypoints[e][1] - mypoints[e - 1][1]) / sqrt(
((mypoints[e][0] - mypoints[e - 1][0]) ** 2) + ((mypoints[e][1] - mypoints[e - 1][1]) ** 2))
a = asin(sinv)
# Clamp to 90 or 0 degrees
if fabs(a) > pi / 4:
b = pi / 2
else:
b = 0
anglelist.extend([b])
# Reverse de distance using angles (inverse angle to axis) for Vertical lines
if a < 0.0 and b != 0:
distlist[e - 1] *= -1 # reverse distance
# Reverse de distance for horizontal lines
if b == 0:
if mypoints[e - 1][0] > mypoints[e][0]:
distlist[e - 1] *= -1 # reverse distance
if debugmode is True:
print(str(e - 1) + ":" + str((a * 180) / pi) + "...:" + str(
(b * 180) / pi) + "--->" + str(distlist[e - 1]))
# ---------------------------------------
# Verify duplications and reduce noise
# ---------------------------------------
if len(anglelist) >= 1:
clearangles = []
cleardistan = []
i = len(anglelist)
oldangle = anglelist[0]
olddist = 0
for e in range(0, i):
if oldangle != anglelist[e]:
clearangles.extend([oldangle])
cleardistan.extend([olddist])
oldangle = anglelist[e]
olddist = distlist[e]
else:
olddist += distlist[e]
# last
clearangles.extend([oldangle])
cleardistan.extend([olddist])
# ----------------------------
# Create the room
# ----------------------------
if len(mypoints) > 1 and len(clearangles) > 0:
# Move cursor
bpy.context.scene.cursor.location.x = mypoints[0][0]
bpy.context.scene.cursor.location.y = mypoints[0][1]
bpy.context.scene.cursor.location.z = 0 # always on grid floor
# Add room mesh
bpy.ops.mesh.archimesh_room()
myroom = context.object
mydata = myroom.RoomGenerator[0]
# Number of walls
mydata.wall_num = len(mypoints) - 1
mydata.ceiling = scene.archimesh_ceiling
mydata.floor = scene.archimesh_floor
mydata.merge = scene.archimesh_merge
i = len(mypoints)
for e in range(0, i - 1):
if clearangles[e] == pi / 2:
if cleardistan[e] > 0:
mydata.walls[e].w = round(fabs(cleardistan[e]), 2)
mydata.walls[e].r = (fabs(clearangles[e]) * 180) / pi # from radians
else:
mydata.walls[e].w = round(fabs(cleardistan[e]), 2)
mydata.walls[e].r = (fabs(clearangles[e]) * 180 * -1) / pi # from radians
else:
mydata.walls[e].w = round(cleardistan[e], 2)
mydata.walls[e].r = (fabs(clearangles[e]) * 180) / pi # from radians
# Remove Grease pencil
if pencil is not None:
for frame in pencil.frames:
pencil.frames.remove(frame)
self.report({'INFO'}, "Archimesh: Room created from grease pencil strokes")
else:
self.report({'WARNING'}, "Archimesh: Not enough grease pencil strokes for creating room.")
return {'FINISHED'}
except:
self.report({'WARNING'}, "Archimesh: No grease pencil strokes. Do strokes in top view before creating room")
return {'CANCELLED'}
# ------------------------------------------------------------------
# Define panel class for main functions.
# ------------------------------------------------------------------
class ARCHIMESH_PT_Main(Panel):
bl_idname = "ARCHIMESH_PT_main"
bl_label = "Archimesh"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Create"
bl_context = "objectmode"
bl_options = {'DEFAULT_CLOSED'}
# ------------------------------
# Draw UI
# ------------------------------
def draw(self, context):
layout = self.layout
scene = context.scene
myobj = context.object
# -------------------------------------------------------------------------
# If the selected object didn't be created with the group 'RoomGenerator',
# this button is not created.
# -------------------------------------------------------------------------
# noinspection PyBroadException
try:
if 'RoomGenerator' in myobj:
box = layout.box()
box.label(text="Room Tools", icon='MODIFIER')
row = box.row(align=False)
row.operator("object.archimesh_cut_holes", icon='GRID')
row.prop(scene, "archimesh_select_only")
# Export/Import
row = box.row(align=False)
row.operator("io_import.roomdata", text="Import", icon='COPYDOWN')
row.operator("io_export.roomdata", text="Export", icon='PASTEDOWN')
except:
pass
# -------------------------------------------------------------------------
# If the selected object isn't a kitchen
# this button is not created.
# -------------------------------------------------------------------------
# noinspection PyBroadException
try:
if myobj["archimesh.sku"] is not None:
box = layout.box()
box.label(text="Kitchen Tools", icon='MODIFIER')
# Export
row = box.row(align=False)
row.operator("io_export.kitchen_inventory", text="Export inventory", icon='PASTEDOWN')
except:
pass
# ------------------------------
# Elements Buttons
# ------------------------------
box = layout.box()
box.label(text="Elements", icon='GROUP')
row = box.row()
row.operator("mesh.archimesh_room")
row.operator("mesh.archimesh_column")
row = box.row()
row.operator("mesh.archimesh_door")
row = box.row()
row.operator("mesh.archimesh_window")
row.operator("mesh.archimesh_winpanel")
row = box.row()
row.operator("mesh.archimesh_kitchen")
row.operator("mesh.archimesh_shelves")
row = box.row()
row.operator("mesh.archimesh_stairs")
row.operator("mesh.archimesh_roof")
# ------------------------------
# Prop Buttons
# ------------------------------
box = layout.box()
box.label(text="Props", icon='LIGHT_DATA')
row = box.row()
row.operator("mesh.archimesh_books")
row.operator("mesh.archimesh_light")
row = box.row()
row.operator("mesh.archimesh_venetian")
row.operator("mesh.archimesh_roller")
row = box.row()
row.operator("mesh.archimesh_japan")
# ------------------------------
# OpenGL Buttons
# ------------------------------
box = layout.box()
box.label(text="Display hints", icon='QUESTION')
row = box.row()
if context.window_manager.archimesh_run_opengl is False:
icon = 'PLAY'
txt = 'Show'
else:
icon = "PAUSE"
txt = 'Hide'
row.operator("archimesh.runopenglbutton", text=txt, icon=icon)
row = box.row()
row.prop(scene, "archimesh_gl_measure", toggle=True, icon="ALIGN_CENTER")
row.prop(scene, "archimesh_gl_name", toggle=True, icon="OUTLINER_OB_FONT")
row.prop(scene, "archimesh_gl_ghost", icon='GHOST_ENABLED')
row = box.row()
row.prop(scene, "archimesh_text_color", text="")
row.prop(scene, "archimesh_walltext_color", text="")
row = box.row()
row.prop(scene, "archimesh_font_size")
row.prop(scene, "archimesh_wfont_size")
row = box.row()
row.prop(scene, "archimesh_hint_space")
# ------------------------------
# Grease pencil tools
# ------------------------------
box = layout.box()
box.label(text="Pencil Tools", icon='MODIFIER')
row = box.row(align=False)
row.operator("object.archimesh_pencil_room", icon='GREASEPENCIL')
row = box.row(align=False)
row.prop(scene, "archimesh_ceiling")
row.prop(scene, "archimesh_floor")
row.prop(scene, "archimesh_merge")
# -------------------------------------------------------------
# Defines button for enable/disable the tip display
#
# -------------------------------------------------------------
class ARCHIMESH_OT_HintDisplay(Operator):
bl_idname = "archimesh.runopenglbutton"
bl_label = "Display hint data manager"
bl_description = "Display additional information in the viewport"
bl_category = 'View'
_handle = None # keep function handler
# ----------------------------------
# Enable gl drawing adding handler
# ----------------------------------
@staticmethod
def handle_add(self, context):
if ARCHIMESH_OT_HintDisplay._handle is None:
ARCHIMESH_OT_HintDisplay._handle = SpaceView3D.draw_handler_add(draw_callback_px, (self, context),
'WINDOW',
'POST_PIXEL')
context.window_manager.archimesh_run_opengl = True
# ------------------------------------
# Disable gl drawing removing handler
# ------------------------------------
# noinspection PyUnusedLocal
@staticmethod
def handle_remove(self, context):
if ARCHIMESH_OT_HintDisplay._handle is not None:
SpaceView3D.draw_handler_remove(ARCHIMESH_OT_HintDisplay._handle, 'WINDOW')
ARCHIMESH_OT_HintDisplay._handle = None
context.window_manager.archimesh_run_opengl = False
# ------------------------------
# Execute button action
# ------------------------------
def execute(self, context):
if context.area.type == 'VIEW_3D':
if context.window_manager.archimesh_run_opengl is False:
self.handle_add(self, context)
context.area.tag_redraw()
else:
self.handle_remove(self, context)
context.area.tag_redraw()
return {'FINISHED'}
else:
self.report({'WARNING'},
"View3D not found, cannot run operator")
return {'CANCELLED'}
# -------------------------------------------------------------
# Handler for drawing OpenGl
# -------------------------------------------------------------
# noinspection PyUnusedLocal
def draw_callback_px(self, context):
draw_main(context)