mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Measure tool persists on screen and can be snapped.
The user has an option to clean previous measurements polylines by pressing "E" while using the measure tool.
This commit is contained in:
@@ -120,7 +120,7 @@ classes = (
|
||||
mep.RegenerateDistributionElement,
|
||||
prop.SnapMousePoint,
|
||||
prop.PolylinePoint,
|
||||
prop.PolylineMeasurement,
|
||||
prop.MeasurePolyline,
|
||||
prop.BIMModelProperties,
|
||||
prop.BIMArrayProperties,
|
||||
prop.BIMStairProperties,
|
||||
|
||||
@@ -454,7 +454,7 @@ class PolylineDecorator:
|
||||
def draw_measurements(self, context):
|
||||
region = context.region
|
||||
rv3d = region.data
|
||||
measurement_prop = context.scene.BIMPolylineProperties.polyline_measurement
|
||||
measurement_prop = context.scene.BIMPolylineProperties.polyline_point
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.font_id = 1
|
||||
|
||||
@@ -774,17 +774,16 @@ class PolylinePoint(PropertyGroup):
|
||||
x: bpy.props.FloatProperty(name="X")
|
||||
y: bpy.props.FloatProperty(name="Y")
|
||||
z: bpy.props.FloatProperty(name="Z")
|
||||
|
||||
|
||||
class PolylineMeasurement(PropertyGroup):
|
||||
dim: bpy.props.StringProperty(name="Dimension")
|
||||
angle: bpy.props.StringProperty(name="Angle")
|
||||
position: bpy.props.FloatVectorProperty(name="Decorator Position", size=3)
|
||||
|
||||
class MeasurePolyline(PropertyGroup):
|
||||
polyline_point: bpy.props.CollectionProperty(type=PolylinePoint)
|
||||
|
||||
class BIMPolylineProperties(PropertyGroup):
|
||||
snap_mouse_point: bpy.props.CollectionProperty(type=SnapMousePoint)
|
||||
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
|
||||
polyline_point: bpy.props.CollectionProperty(type=PolylinePoint)
|
||||
polyline_measurement: bpy.props.CollectionProperty(type=PolylineMeasurement)
|
||||
product_preview: bpy.props.CollectionProperty(type=PolylinePoint)
|
||||
measure_polyline: bpy.props.CollectionProperty(type=MeasurePolyline)
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import gpu
|
||||
import blf
|
||||
import bpy
|
||||
import bmesh
|
||||
import bonsai.tool as tool
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from bpy.app.handlers import persistent
|
||||
@@ -201,3 +203,133 @@ class ClippingPlaneDecorator:
|
||||
if selected_edges:
|
||||
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
|
||||
|
||||
|
||||
class MeasureDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL"))
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
except ValueError:
|
||||
pass
|
||||
cls.is_installed = False
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
shader = self.line_shader if shader_type == "LINES" else self.shader
|
||||
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_text_background(self, context, coords_dim, text_dim):
|
||||
padding = 5
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
color = (*theme.user_interface.wcol_menu_back.inner[:3], 0.5) # unwrap color values and adds alpha
|
||||
top_left = (coords_dim[0] - padding, coords_dim[1] + text_dim[1] + padding)
|
||||
bottom_left = (coords_dim[0] - padding, coords_dim[1] - padding)
|
||||
top_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] + text_dim[1] + padding)
|
||||
bottom_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] - padding)
|
||||
|
||||
verts = [top_left, bottom_left, top_right, bottom_right]
|
||||
gpu.state.blend_set("ALPHA")
|
||||
self.draw_batch("TRIS", verts, color, [(0, 1, 2), (1, 2, 3)])
|
||||
|
||||
def draw_measurements(self, context):
|
||||
region = context.region
|
||||
rv3d = region.data
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.font_id = 1
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
font_size = tool.Blender.scale_font_size(12)
|
||||
blf.size(self.font_id, font_size)
|
||||
blf.enable(self.font_id, blf.SHADOW)
|
||||
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
|
||||
color = self.addon_prefs.decorations_colour
|
||||
blf.color(self.font_id, *color)
|
||||
|
||||
measure_data = context.scene.BIMPolylineProperties.measure_polyline
|
||||
for data in measure_data:
|
||||
measurement_prop = data.polyline_point
|
||||
|
||||
for i in range(len(measurement_prop)):
|
||||
if i == 0:
|
||||
continue
|
||||
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
|
||||
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
|
||||
|
||||
formatted_value = measurement_prop[i].dim
|
||||
|
||||
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
|
||||
text = "d: " + formatted_value
|
||||
text_dim = blf.dimensions(self.font_id, text)
|
||||
self.draw_text_background(context, coords_dim, text_dim)
|
||||
blf.draw(self.font_id, text)
|
||||
|
||||
if i == 1:
|
||||
continue
|
||||
pos_angle = measurement_prop[i - 1].position
|
||||
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
|
||||
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
|
||||
text = "a: " + measurement_prop[i].angle
|
||||
text_dim = blf.dimensions(self.font_id, text)
|
||||
self.draw_text_background(context, coords_angle, text_dim)
|
||||
blf.draw(self.font_id, text)
|
||||
blf.disable(self.font_id, blf.SHADOW)
|
||||
|
||||
def __call__(self, context):
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
decorator_color = self.addon_prefs.decorations_colour
|
||||
decorator_color_special = self.addon_prefs.decorator_color_special
|
||||
decorator_color_selected = self.addon_prefs.decorator_color_selected
|
||||
decorator_color_error = self.addon_prefs.decorator_color_error
|
||||
decorator_color_unselected = self.addon_prefs.decorator_color_unselected
|
||||
decorator_color_background = self.addon_prefs.decorator_color_background
|
||||
theme = context.preferences.themes.items()[0][1]
|
||||
decorator_color_object_active = (*theme.view_3d.object_active, 1) # unwrap color values and adds alpha=1
|
||||
decorator_color_x_axis = (*theme.user_interface.axis_x, 1)
|
||||
decorator_color_y_axis = (*theme.user_interface.axis_y, 1)
|
||||
decorator_color_z_axis = (*theme.user_interface.axis_z, 1)
|
||||
|
||||
gpu.state.blend_set("ALPHA")
|
||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
||||
self.line_shader.bind() # required to be able to change uniforms of the shader
|
||||
# POLYLINE_UNIFORM_COLOR specific uniforms
|
||||
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
|
||||
|
||||
# general shader
|
||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
gpu.state.point_size_set(6)
|
||||
|
||||
|
||||
measure_data = context.scene.BIMPolylineProperties.measure_polyline
|
||||
for data in measure_data:
|
||||
polyline_data = data.polyline_point
|
||||
|
||||
polyline_points = []
|
||||
polyline_edges = []
|
||||
for point_prop in polyline_data:
|
||||
point = Vector((point_prop.x, point_prop.y, point_prop.z))
|
||||
polyline_points.append(point)
|
||||
|
||||
for i in range(len(polyline_points) - 1):
|
||||
polyline_edges.append([i, i + 1])
|
||||
|
||||
|
||||
# Draw polyline with selected points
|
||||
self.line_shader.uniform_float("lineWidth", 2.0)
|
||||
self.draw_batch("POINTS", polyline_points, decorator_color_unselected)
|
||||
if len(polyline_points) > 1:
|
||||
self.draw_batch("LINES", polyline_points, decorator_color_unselected, polyline_edges)
|
||||
|
||||
@@ -51,7 +51,7 @@ from mathutils import Vector, Matrix
|
||||
from bpy.app.handlers import persistent
|
||||
from ifcopenshell.geom import ShapeElementType
|
||||
from bonsai.bim.module.project.data import LinksData
|
||||
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
|
||||
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator, MeasureDecorator
|
||||
from bonsai.bim.module.model.decorator import PolylineDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
from typing import Union
|
||||
@@ -2340,6 +2340,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
|
||||
A: Angle Input
|
||||
M: Modify Snap Point
|
||||
C: Close Polyline
|
||||
E: Erase previous polylines
|
||||
BACKSPACE: Remove Point
|
||||
X, Y, Z: Choose Axis
|
||||
S-X, S-Y, S-Z: Choose Plane
|
||||
@@ -2366,7 +2367,9 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
|
||||
):
|
||||
context.workspace.status_text_set(text=None)
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Snap.move_polyline_to_measure()
|
||||
tool.Snap.clear_polyline()
|
||||
MeasureDecorator.install(context)
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -2374,6 +2377,11 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
if event.type == "E":
|
||||
context.scene.BIMPolylineProperties.measure_polyline.clear()
|
||||
MeasureDecorator.uninstall()
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
result = self.handle_cancelation(context, event)
|
||||
if result is not None:
|
||||
return result
|
||||
@@ -2383,3 +2391,4 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
|
||||
def invoke(self, context, event):
|
||||
super().invoke(context, event)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
return None, None, None
|
||||
|
||||
@classmethod
|
||||
def ray_cast_by_proximity(cls, context, event, obj, face=None):
|
||||
def ray_cast_by_proximity(cls, context, event, obj, face=None, custom_bmesh=None):
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
mouse_pos = event.mouse_region_x, event.mouse_region_y
|
||||
@@ -149,35 +149,42 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
except:
|
||||
loc = Vector((0, 0, 0))
|
||||
|
||||
bm = bmesh.new()
|
||||
if face is None: # Object with faces
|
||||
bm.from_mesh(obj.data)
|
||||
else: # Object without faces
|
||||
verts = [bm.verts.new(obj.data.vertices[i].co) for i in face.vertices]
|
||||
bm.faces.new(verts)
|
||||
if not custom_bmesh:
|
||||
bm = bmesh.new()
|
||||
if face is None: # Object without faces
|
||||
bm.from_mesh(obj.data)
|
||||
else: # Object with faces
|
||||
verts = [bm.verts.new(obj.data.vertices[i].co) for i in face.vertices]
|
||||
bm.faces.new(verts)
|
||||
else:
|
||||
# Measure polylines
|
||||
bm = custom_bmesh
|
||||
|
||||
for vertex in bm.verts:
|
||||
world_vertex = obj.matrix_world.copy() @ vertex.co
|
||||
intersection = tool.Cad.point_on_edge(world_vertex, (ray_target, loc))
|
||||
distance = (world_vertex - intersection).length
|
||||
v = vertex.co
|
||||
if obj:
|
||||
v = obj.matrix_world.copy() @ v
|
||||
intersection = tool.Cad.point_on_edge(v, (ray_target, loc))
|
||||
distance = (v - intersection).length
|
||||
if distance < 0.2:
|
||||
points.append([distance - stick_factor, (world_vertex, "Vertex")])
|
||||
points.append([distance - stick_factor, (v, "Vertex")])
|
||||
|
||||
for edge in bm.edges:
|
||||
v1 = edge.verts[0].co
|
||||
v2 = edge.verts[1].co
|
||||
world_v1 = obj.matrix_world.copy() @ v1
|
||||
world_v2 = obj.matrix_world.copy() @ v2
|
||||
division_point = (world_v1 + world_v2) / 2 # TODO Make it work for different divisions
|
||||
if obj:
|
||||
v1 = obj.matrix_world.copy() @ v1
|
||||
v2 = obj.matrix_world.copy() @ v2
|
||||
division_point = (v1 + v2) / 2 # TODO Make it work for different divisions
|
||||
|
||||
intersection = tool.Cad.point_on_edge(division_point, (ray_target, loc))
|
||||
distance = (division_point - intersection).length
|
||||
if distance < 0.2:
|
||||
points.append([distance, (division_point, "Edge Center")])
|
||||
|
||||
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), (world_v1, world_v2))
|
||||
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), (v1, v2))
|
||||
if intersection[0]:
|
||||
if tool.Cad.is_point_on_edge(intersection[1], (world_v1, world_v2)):
|
||||
if tool.Cad.is_point_on_edge(intersection[1], (v1, v2)):
|
||||
distance = (intersection[1] - intersection[0]).length
|
||||
if distance < 0.8:
|
||||
points.append([distance + 4 * stick_factor, (intersection[1], "Edge")])
|
||||
@@ -217,6 +224,24 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
|
||||
return polyline_points
|
||||
|
||||
@classmethod
|
||||
def ray_cast_to_measure(cls, context, event, points):
|
||||
bm = bmesh.new()
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
indices = list(range(len(points)-1))
|
||||
edges = [(i, i+1) for i in range(len(points)-1)]
|
||||
new_verts = [bm.verts.new(Vector((point.x, point.y, point.z))) for point in points]
|
||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in edges]
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
snapping_points = cls.ray_cast_by_proximity(context, event, None, custom_bmesh=bm)
|
||||
bm.free()
|
||||
return snapping_points
|
||||
|
||||
|
||||
@classmethod
|
||||
def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal):
|
||||
region = context.region
|
||||
|
||||
@@ -60,28 +60,6 @@ class Snap(bonsai.core.tool.Snap):
|
||||
|
||||
return snap_point
|
||||
|
||||
# TODO Remove this function
|
||||
@classmethod
|
||||
def select_snap_point(cls, snap_points, hit, threshold):
|
||||
shortest_distance = None
|
||||
snap_point = None
|
||||
for point, snap_type in snap_points.items():
|
||||
point = Vector(point)
|
||||
distance = (point - hit).length
|
||||
if distance > threshold:
|
||||
continue
|
||||
if shortest_distance and distance < shortest_distance:
|
||||
shortest_distance = distance
|
||||
snap_point = (point, snap_type)
|
||||
|
||||
elif not shortest_distance:
|
||||
shortest_distance = distance
|
||||
snap_point = (point, snap_type)
|
||||
else:
|
||||
pass
|
||||
|
||||
return snap_point
|
||||
|
||||
@classmethod
|
||||
def update_snapping_point(cls, snap_point, snap_type):
|
||||
try:
|
||||
@@ -140,6 +118,7 @@ class Snap(bonsai.core.tool.Snap):
|
||||
for point in polyline_data[1:]: # The first can be repeated to form a wall loop
|
||||
if (x, y, z) == (point.x, point.y, point.z):
|
||||
return "Cannot create two points at the same location"
|
||||
# TODO move this limitation to be Wall tool specific. Right now it also affects Measure tool
|
||||
# Avoids creating segments smaller then 0.1. This is a limitation from create_wall_from_2_points
|
||||
length = (
|
||||
Vector((x, y, z)) - Vector((polyline_data[-1].x, polyline_data[-1].y, polyline_data[-1].z))
|
||||
@@ -152,10 +131,9 @@ class Snap(bonsai.core.tool.Snap):
|
||||
polyline_point.y = y
|
||||
polyline_point.z = z
|
||||
|
||||
polyline_measurement = bpy.context.scene.BIMPolylineProperties.polyline_measurement.add()
|
||||
polyline_measurement.dim = d
|
||||
polyline_measurement.angle = a
|
||||
polyline_measurement.position = Vector((x, y, z))
|
||||
polyline_point.dim = d
|
||||
polyline_point.angle = a
|
||||
polyline_point.position = Vector((x, y, z))
|
||||
|
||||
@classmethod
|
||||
def close_polyline(cls):
|
||||
@@ -172,15 +150,25 @@ class Snap(bonsai.core.tool.Snap):
|
||||
@classmethod
|
||||
def clear_polyline(cls):
|
||||
bpy.context.scene.BIMPolylineProperties.polyline_point.clear()
|
||||
bpy.context.scene.BIMPolylineProperties.polyline_measurement.clear()
|
||||
|
||||
@classmethod
|
||||
def remove_last_polyline_point(cls):
|
||||
polyline_data = bpy.context.scene.BIMPolylineProperties.polyline_point
|
||||
polyline_data.remove(len(polyline_data) - 1)
|
||||
polyline_measurement = bpy.context.scene.BIMPolylineProperties.polyline_measurement
|
||||
polyline_measurement.remove(len(polyline_measurement) - 1)
|
||||
|
||||
@classmethod
|
||||
def move_polyline_to_measure(cls):
|
||||
polyline_data = bpy.context.scene.BIMPolylineProperties.polyline_point
|
||||
measure_data = bpy.context.scene.BIMPolylineProperties.measure_polyline.add()
|
||||
for point in polyline_data:
|
||||
measure_point = measure_data.polyline_point.add()
|
||||
measure_point.x = point.x
|
||||
measure_point.y = point.y
|
||||
measure_point.z = point.z
|
||||
measure_point.dim = point.dim
|
||||
measure_point.angle = point.angle
|
||||
measure_point.position = point.position
|
||||
|
||||
@classmethod
|
||||
def snap_on_axis(cls, intersection, tool_state, lock_angle=False):
|
||||
|
||||
@@ -399,10 +387,20 @@ class Snap(bonsai.core.tool.Snap):
|
||||
last_polyline_point = polyline_data[len(polyline_data) - 1]
|
||||
except:
|
||||
last_polyline_point = None
|
||||
snap_points = tool.Raycast.ray_cast_to_polyline(context, event)
|
||||
# snap_point = cls.select_snap_point(snap_points, intersection, snap_threshold)
|
||||
if snap_points:
|
||||
detected_snaps.append({"Polyline": snap_points})
|
||||
if polyline_data:
|
||||
snap_points = tool.Raycast.ray_cast_to_polyline(context, event)
|
||||
if snap_points:
|
||||
detected_snaps.append({"Polyline": snap_points})
|
||||
|
||||
# Measure
|
||||
measure_data = context.scene.BIMPolylineProperties.measure_polyline
|
||||
for measure in measure_data:
|
||||
measure_points = measure.polyline_point
|
||||
print('points', measure_points)
|
||||
snap_points = tool.Raycast.ray_cast_to_measure(context, event, measure_points)
|
||||
if snap_points:
|
||||
detected_snaps.append({"Polyline": snap_points})
|
||||
|
||||
|
||||
# Axis and Plane
|
||||
elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
|
||||
|
||||
Reference in New Issue
Block a user