mirror of
https://github.com/blender/blender-addons.git
synced 2025-07-29 12:05:36 +00:00
557 lines
22 KiB
Python
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)
|