mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-11 22:31:55 +00:00
Refactor PI picker to use Bonsai polyline system
Replace custom SAIKEI_OT_pick_pi_from_viewport modal with a PolylineOperator subclass, reusing Bonsai's proven polyline infrastructure (same base class as wall/slab/profile drawing). Gains: snapping, numeric D/A/X/Y input, axis locking, angle locking, measurement display, undo-last-point, status bar hints. Remove PIPickerDecorator (replaced by PolylineDecorator). Fix EN string formatting in UI list display rows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
"""Alignment module decorators for GPU visualization.
|
"""Alignment module decorators for GPU visualization.
|
||||||
|
|
||||||
This module contains decorators for rendering visual feedback during
|
This module contains decorators for rendering visual feedback during
|
||||||
alignment-related operations, such as PI picking.
|
alignment-related operations, such as PI editing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@@ -27,217 +27,7 @@ import blf
|
|||||||
import gpu
|
import gpu
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
from bpy.types import SpaceView3D
|
from bpy.types import SpaceView3D
|
||||||
from mathutils import Vector
|
|
||||||
from gpu_extras.batch import batch_for_shader
|
from gpu_extras.batch import batch_for_shader
|
||||||
from gpu_extras.presets import draw_circle_2d
|
|
||||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
|
||||||
|
|
||||||
|
|
||||||
class PIPickerDecorator:
|
|
||||||
"""Decorator for visualizing PI placement during modal picking.
|
|
||||||
|
|
||||||
This decorator draws visual feedback while the user is placing
|
|
||||||
PI (Point of Intersection) points in the viewport:
|
|
||||||
- Yellow lines connecting placed PIs (tangent preview)
|
|
||||||
- Rubber band line from last PI to cursor
|
|
||||||
- Green circles at PI marker positions
|
|
||||||
- HUD text showing instructions and PI count
|
|
||||||
|
|
||||||
All drawings are ephemeral - they disappear when the modal ends.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Class-level state (cleared on uninstall)
|
|
||||||
is_installed = False
|
|
||||||
handlers = []
|
|
||||||
|
|
||||||
# PI points in Blender coordinates (list of Vector)
|
|
||||||
pi_points = []
|
|
||||||
|
|
||||||
# Current mouse position in 3D (Vector or None)
|
|
||||||
mouse_3d = None
|
|
||||||
|
|
||||||
# Reference to the active region/rv3d for coordinate conversion
|
|
||||||
region = None
|
|
||||||
rv3d = None
|
|
||||||
|
|
||||||
# Colors (matching reference document)
|
|
||||||
COLOR_TANGENT_LINE = (1.0, 0.9, 0.2, 1.0) # Yellow for tangent lines
|
|
||||||
COLOR_RUBBER_BAND = (1.0, 0.9, 0.2, 0.5) # Yellow with alpha for rubber band
|
|
||||||
COLOR_PI_MARKER = (0.3, 1.0, 0.4, 1.0) # Green for PI markers
|
|
||||||
COLOR_HUD_TEXT = (1.0, 1.0, 1.0, 1.0) # White for HUD text
|
|
||||||
|
|
||||||
# Drawing parameters
|
|
||||||
PI_MARKER_RADIUS = 8 # pixels
|
|
||||||
LINE_WIDTH = 2.5
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def install(cls, context, region, rv3d):
|
|
||||||
"""Install decorator handlers for PI visualization.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: Blender context
|
|
||||||
region: The 3D viewport region for coordinate conversion
|
|
||||||
rv3d: The region's 3D data (RegionView3D)
|
|
||||||
"""
|
|
||||||
if cls.is_installed:
|
|
||||||
cls.uninstall()
|
|
||||||
|
|
||||||
# Store region references for 3D->2D conversion
|
|
||||||
cls.region = region
|
|
||||||
cls.rv3d = rv3d
|
|
||||||
|
|
||||||
# Clear any stale state
|
|
||||||
cls.pi_points = []
|
|
||||||
cls.mouse_3d = None
|
|
||||||
|
|
||||||
handler = cls()
|
|
||||||
# POST_PIXEL for 2D screen-space drawing (more efficient)
|
|
||||||
cls.handlers.append(
|
|
||||||
SpaceView3D.draw_handler_add(handler.draw_tangent_lines, (context,), "WINDOW", "POST_PIXEL")
|
|
||||||
)
|
|
||||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_pi_markers, (context,), "WINDOW", "POST_PIXEL"))
|
|
||||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_hud, (context,), "WINDOW", "POST_PIXEL"))
|
|
||||||
cls.is_installed = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def uninstall(cls):
|
|
||||||
"""Remove all handlers and clear state."""
|
|
||||||
for handler in cls.handlers:
|
|
||||||
try:
|
|
||||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
cls.handlers = []
|
|
||||||
cls.is_installed = False
|
|
||||||
cls.pi_points = []
|
|
||||||
cls.mouse_3d = None
|
|
||||||
cls.region = None
|
|
||||||
cls.rv3d = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def update(cls, pi_points_blender, mouse_3d):
|
|
||||||
"""Update decorator state from modal operator.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pi_points_blender: List of Vector - PI positions in Blender coords
|
|
||||||
mouse_3d: Vector or None - Current mouse position on ground plane
|
|
||||||
"""
|
|
||||||
cls.pi_points = pi_points_blender
|
|
||||||
cls.mouse_3d = mouse_3d
|
|
||||||
|
|
||||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
|
||||||
"""Draw a batch of primitives using GPU shader.
|
|
||||||
|
|
||||||
This follows the established Bonsai decorator pattern.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
shader_type: Type of primitive ("LINES", "POINTS", etc.)
|
|
||||||
content_pos: List of vertex positions
|
|
||||||
color: RGBA color tuple
|
|
||||||
indices: Optional list of index pairs for lines
|
|
||||||
"""
|
|
||||||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
|
||||||
return
|
|
||||||
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_tangent_lines(self, context):
|
|
||||||
"""Draw yellow tangent lines connecting PIs and rubber band to cursor."""
|
|
||||||
region = self.region or context.region
|
|
||||||
rv3d = self.rv3d or context.region_data
|
|
||||||
|
|
||||||
if not region or not rv3d:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Convert 3D points to 2D screen coordinates
|
|
||||||
screen_points = []
|
|
||||||
for pt in self.pi_points:
|
|
||||||
screen_pt = location_3d_to_region_2d(region, rv3d, pt)
|
|
||||||
if screen_pt:
|
|
||||||
screen_points.append(screen_pt)
|
|
||||||
|
|
||||||
# Nothing to draw if no points
|
|
||||||
if not screen_points and not self.mouse_3d:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Setup shaders
|
|
||||||
gpu.state.blend_set("ALPHA")
|
|
||||||
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
|
|
||||||
self.line_shader.bind()
|
|
||||||
self.line_shader.uniform_float("viewportSize", (region.width, region.height))
|
|
||||||
self.line_shader.uniform_float("lineWidth", self.LINE_WIDTH)
|
|
||||||
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
|
||||||
|
|
||||||
# Draw lines between placed PIs (solid yellow)
|
|
||||||
if len(screen_points) >= 2:
|
|
||||||
verts = [(p.x, p.y) for p in screen_points]
|
|
||||||
edges = [[i, i + 1] for i in range(len(screen_points) - 1)]
|
|
||||||
self.draw_batch("LINES", verts, self.COLOR_TANGENT_LINE, edges)
|
|
||||||
|
|
||||||
# Draw rubber band from last PI to cursor (semi-transparent)
|
|
||||||
if screen_points and self.mouse_3d:
|
|
||||||
mouse_2d = location_3d_to_region_2d(region, rv3d, self.mouse_3d)
|
|
||||||
if mouse_2d:
|
|
||||||
last_pt = screen_points[-1]
|
|
||||||
verts = [(last_pt.x, last_pt.y), (mouse_2d.x, mouse_2d.y)]
|
|
||||||
self.draw_batch("LINES", verts, self.COLOR_RUBBER_BAND, [[0, 1]])
|
|
||||||
|
|
||||||
gpu.state.blend_set("NONE")
|
|
||||||
|
|
||||||
def draw_pi_markers(self, context):
|
|
||||||
"""Draw green circles at each PI location."""
|
|
||||||
if not self.pi_points:
|
|
||||||
return
|
|
||||||
|
|
||||||
region = self.region or context.region
|
|
||||||
rv3d = self.rv3d or context.region_data
|
|
||||||
|
|
||||||
if not region or not rv3d:
|
|
||||||
return
|
|
||||||
|
|
||||||
gpu.state.blend_set("ALPHA")
|
|
||||||
|
|
||||||
for pt in self.pi_points:
|
|
||||||
screen_pt = location_3d_to_region_2d(region, rv3d, pt)
|
|
||||||
if screen_pt:
|
|
||||||
draw_circle_2d(screen_pt, self.COLOR_PI_MARKER, self.PI_MARKER_RADIUS)
|
|
||||||
|
|
||||||
gpu.state.blend_set("NONE")
|
|
||||||
|
|
||||||
def draw_hud(self, context):
|
|
||||||
"""Draw HUD text with instructions and PI count."""
|
|
||||||
region = self.region or context.region
|
|
||||||
if not region:
|
|
||||||
return
|
|
||||||
|
|
||||||
font_id = 0
|
|
||||||
font_size = tool.Blender.scale_font_size(14)
|
|
||||||
blf.size(font_id, font_size)
|
|
||||||
blf.enable(font_id, blf.SHADOW)
|
|
||||||
blf.shadow(font_id, 6, 0, 0, 0, 1) # Black shadow for readability
|
|
||||||
blf.color(font_id, *self.COLOR_HUD_TEXT)
|
|
||||||
|
|
||||||
# Position in top-left of viewport
|
|
||||||
margin = 20
|
|
||||||
line_height = 22
|
|
||||||
y_pos = region.height - margin
|
|
||||||
|
|
||||||
# Instructions
|
|
||||||
instructions = [
|
|
||||||
"PI Picker Mode",
|
|
||||||
f"PIs placed: {len(self.pi_points)}",
|
|
||||||
"",
|
|
||||||
"LMB: Place PI",
|
|
||||||
"RMB/ESC: Finish",
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, line in enumerate(instructions):
|
|
||||||
blf.position(font_id, margin, y_pos - (i * line_height), 0)
|
|
||||||
blf.draw(font_id, line)
|
|
||||||
|
|
||||||
blf.disable(font_id, blf.SHADOW)
|
|
||||||
|
|
||||||
|
|
||||||
class PIEditDecorator:
|
class PIEditDecorator:
|
||||||
@@ -259,7 +49,7 @@ class PIEditDecorator:
|
|||||||
# References to PI empty objects
|
# References to PI empty objects
|
||||||
pi_empties = []
|
pi_empties = []
|
||||||
|
|
||||||
# Colors (matching PIPickerDecorator)
|
# Colors
|
||||||
COLOR_TANGENT_LINE = (1.0, 0.9, 0.2, 1.0) # Yellow for tangent lines
|
COLOR_TANGENT_LINE = (1.0, 0.9, 0.2, 1.0) # Yellow for tangent lines
|
||||||
COLOR_HUD_TEXT = (1.0, 1.0, 1.0, 1.0) # White for HUD text
|
COLOR_HUD_TEXT = (1.0, 1.0, 1.0, 1.0) # White for HUD text
|
||||||
COLOR_EDIT_MODE_BG = (0.2, 0.4, 0.8, 0.8) # Blue tint for edit mode indicator
|
COLOR_EDIT_MODE_BG = (0.2, 0.4, 0.8, 0.8) # Blue tint for edit mode indicator
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ from bpy.types import Operator
|
|||||||
from bpy.props import StringProperty, FloatProperty, IntProperty
|
from bpy.props import StringProperty, FloatProperty, IntProperty
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
from . import decorator as alignment_decorator
|
from . import decorator as alignment_decorator
|
||||||
|
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||||
|
from bonsai.bim.module.model.decorator import PolylineDecorator
|
||||||
|
from bonsai.bim.ifc import IfcStore
|
||||||
|
|
||||||
|
|
||||||
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
||||||
@@ -787,180 +790,168 @@ class SAIKEI_OT_remove_pi(Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class SAIKEI_OT_pick_pi_from_viewport(Operator):
|
class SAIKEI_OT_pick_pi_from_viewport(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||||
"""Add PI points by clicking in the 3D viewport"""
|
"""Add PI points by clicking in the 3D viewport using polyline tools"""
|
||||||
|
|
||||||
bl_idname = "saikei.pick_pi_from_viewport"
|
bl_idname = "saikei.pick_pi_from_viewport"
|
||||||
bl_label = "Pick PI from Viewport"
|
bl_label = "Pick PI from Viewport"
|
||||||
bl_description = "Click in the viewport to add PI points. Right-click or Escape to finish."
|
bl_description = "Click in the viewport to add PI points with snapping and numeric input. RMB/Enter to finish, ESC to cancel."
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
# Store reference to 3D view for modal
|
|
||||||
_area = None
|
|
||||||
_region = None
|
|
||||||
_rv3d = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return poll_ifc4x3(cls, context)
|
return poll_ifc4x3(cls, context)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
bpy.types.Operator.__init__(self, *args, **kwargs)
|
||||||
|
PolylineOperator.__init__(self)
|
||||||
|
# Remove instructions that don't apply to alignments
|
||||||
|
self.instructions.pop("Close Polyline", None)
|
||||||
|
self.instructions.pop("Offset", None)
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
# Find the 3D viewport area, region, and region_data
|
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
|
||||||
|
|
||||||
|
def _invoke(self, context, event):
|
||||||
|
# Find the 3D viewport — the operator is invoked from the Properties
|
||||||
|
# panel, so we need to override context for PolylineOperator.invoke()
|
||||||
|
# which requires bpy.context.space_data to be SpaceView3D.
|
||||||
|
area_3d = None
|
||||||
|
region_3d = None
|
||||||
for area in context.screen.areas:
|
for area in context.screen.areas:
|
||||||
if area.type == "VIEW_3D":
|
if area.type == "VIEW_3D":
|
||||||
self._area = area
|
area_3d = area
|
||||||
for region in area.regions:
|
for region in area.regions:
|
||||||
if region.type == "WINDOW":
|
if region.type == "WINDOW":
|
||||||
self._region = region
|
region_3d = region
|
||||||
break
|
|
||||||
for space in area.spaces:
|
|
||||||
if space.type == "VIEW_3D":
|
|
||||||
self._rv3d = space.region_3d
|
|
||||||
break
|
break
|
||||||
break
|
break
|
||||||
|
|
||||||
if not self._region or not self._rv3d:
|
if not area_3d or not region_3d:
|
||||||
self.report({"ERROR"}, "No 3D Viewport found")
|
self.report({"ERROR"}, "No 3D Viewport found")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
# Install the PI picker decorator for visual feedback
|
with context.temp_override(area=area_3d, region=region_3d):
|
||||||
alignment_decorator.PIPickerDecorator.install(context, self._region, self._rv3d)
|
super().invoke(context, event)
|
||||||
|
|
||||||
# Initialize decorator with any existing PIs
|
self.tool_state.use_default_container = False
|
||||||
alignment_decorator.PIPickerDecorator.update(
|
self.tool_state.plane_method = "XY"
|
||||||
pi_points_blender=self._get_pi_points_blender(context),
|
|
||||||
mouse_3d=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
context.window.cursor_set("CROSSHAIR")
|
|
||||||
context.window_manager.modal_handler_add(self)
|
|
||||||
self.report({"INFO"}, "Click to add PIs. Right-click or Escape to finish.")
|
|
||||||
return {"RUNNING_MODAL"}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
def modal(self, context, event):
|
def modal(self, context, event):
|
||||||
# Update rubber band position on mouse move
|
return IfcStore.execute_ifc_operator(self, context, event, method="MODAL")
|
||||||
if event.type == "MOUSEMOVE":
|
|
||||||
coord = self.get_ground_intersection(context, event)
|
|
||||||
if coord:
|
|
||||||
mouse_3d = Vector((coord[0], coord[1], 0.0))
|
|
||||||
alignment_decorator.PIPickerDecorator.update(
|
|
||||||
pi_points_blender=self._get_pi_points_blender(context),
|
|
||||||
mouse_3d=mouse_3d,
|
|
||||||
)
|
|
||||||
if self._area:
|
|
||||||
self._area.tag_redraw()
|
|
||||||
return {"RUNNING_MODAL"}
|
|
||||||
|
|
||||||
if event.type == "LEFTMOUSE" and event.value == "PRESS":
|
def _modal(self, context, event):
|
||||||
# Raycast to ground plane (Z=0)
|
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
||||||
coord = self.get_ground_intersection(context, event)
|
tool.Blender.update_viewport()
|
||||||
if coord:
|
|
||||||
self.add_pi_at_location(context, coord)
|
|
||||||
# Update decorator with new PI
|
|
||||||
mouse_3d = Vector((coord[0], coord[1], 0.0))
|
|
||||||
alignment_decorator.PIPickerDecorator.update(
|
|
||||||
pi_points_blender=self._get_pi_points_blender(context),
|
|
||||||
mouse_3d=mouse_3d,
|
|
||||||
)
|
|
||||||
if self._area:
|
|
||||||
self._area.tag_redraw()
|
|
||||||
return {"RUNNING_MODAL"}
|
|
||||||
|
|
||||||
elif event.type in {"RIGHTMOUSE", "ESC"}:
|
self.handle_lock_axis(context, event)
|
||||||
self._finish_modal(context)
|
|
||||||
|
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
|
||||||
|
self.handle_mouse_move(context, event)
|
||||||
|
return {"PASS_THROUGH"}
|
||||||
|
|
||||||
|
self.handle_instructions(context)
|
||||||
|
self.handle_mouse_move(context, event, should_round=True)
|
||||||
|
self.choose_axis(event)
|
||||||
|
self.handle_snap_selection(context, event)
|
||||||
|
self.handle_keyboard_input(context, event)
|
||||||
|
self._handle_inserting_polyline_no_close(context, event)
|
||||||
|
|
||||||
|
# Finish: transfer polyline points to PI table
|
||||||
|
if (
|
||||||
|
not self.tool_state.is_input_on
|
||||||
|
and event.value == "RELEASE"
|
||||||
|
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
|
||||||
|
):
|
||||||
|
self._transfer_polyline_to_pis(context)
|
||||||
|
context.workspace.status_text_set(text=None)
|
||||||
|
PolylineDecorator.uninstall()
|
||||||
|
tool.Polyline.clear_polyline()
|
||||||
|
tool.Blender.update_viewport()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
# Allow viewport navigation
|
cancel = self.handle_cancelation(context, event)
|
||||||
elif event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
|
if cancel is not None:
|
||||||
return {"PASS_THROUGH"}
|
return cancel
|
||||||
|
|
||||||
return {"RUNNING_MODAL"}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
def get_ground_intersection(self, context, event):
|
def _handle_inserting_polyline_no_close(self, context, event):
|
||||||
"""Raycast from mouse to Z=0 ground plane"""
|
"""Insert polyline points without close-polyline (C key) behavior.
|
||||||
from bpy_extras.view3d_utils import region_2d_to_origin_3d, region_2d_to_vector_3d
|
|
||||||
|
|
||||||
region = self._region
|
Alignments are open curves, so the C key (close polyline) is suppressed.
|
||||||
rv3d = self._rv3d
|
All other insertion behavior is preserved: LEFTMOUSE, BACKSPACE, and
|
||||||
|
RET/ENTER with numeric input active.
|
||||||
|
"""
|
||||||
|
# LEFTMOUSE: insert point at current snap/cursor position
|
||||||
|
if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type == "LEFTMOUSE":
|
||||||
|
result = tool.Polyline.insert_polyline_point(self.input_ui, self.tool_state)
|
||||||
|
if result:
|
||||||
|
self.report({"WARNING"}, result)
|
||||||
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
# Guard against None context
|
# RET/ENTER with numeric input: validate and insert
|
||||||
if region is None or rv3d is None:
|
if (
|
||||||
return None
|
self.tool_state.is_input_on
|
||||||
|
and event.value == "RELEASE"
|
||||||
|
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
|
||||||
|
):
|
||||||
|
is_valid = self.recalculate_inputs(context)
|
||||||
|
if is_valid:
|
||||||
|
result = tool.Polyline.insert_polyline_point(self.input_ui, self.tool_state)
|
||||||
|
if result:
|
||||||
|
self.report({"WARNING"}, result)
|
||||||
|
|
||||||
# Use absolute mouse coordinates and convert to the 3D viewport region's local coords
|
self.tool_state.mode = "Mouse"
|
||||||
# event.mouse_region_x/y are relative to whatever region received the event,
|
self.tool_state.is_input_on = False
|
||||||
# which may not be the 3D viewport region we stored
|
self.input_type = None
|
||||||
region_x = event.mouse_x - region.x
|
self.tool_state.input_type = None
|
||||||
region_y = event.mouse_y - region.y
|
self.number_input = []
|
||||||
coord = (region_x, region_y)
|
self.number_output = ""
|
||||||
|
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
||||||
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
origin = region_2d_to_origin_3d(region, rv3d, coord)
|
# BACKSPACE: remove last point (when not typing numeric input)
|
||||||
direction = region_2d_to_vector_3d(region, rv3d, coord)
|
if not self.tool_state.is_input_on:
|
||||||
|
if event.value == "RELEASE" and event.type == "BACK_SPACE":
|
||||||
|
tool.Polyline.remove_last_polyline_point()
|
||||||
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
# Intersect with Z=0 plane
|
def _transfer_polyline_to_pis(self, context):
|
||||||
if direction.z != 0:
|
"""Transfer collected polyline points to the PI Editor table.
|
||||||
t = -origin.z / direction.z
|
|
||||||
if t > 0: # In front of camera
|
|
||||||
hit = origin + direction * t
|
|
||||||
return (hit.x, hit.y)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_pi_points_blender(self, context):
|
Polyline points are in Blender coordinate space. This method converts
|
||||||
"""Get all PI points converted to Blender coordinates.
|
each point to IFC coordinate space before storing in props.pis.
|
||||||
|
|
||||||
PI coordinates are stored in IFC space; this converts them
|
|
||||||
to Blender local coordinates for visualization.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of Vector - PI positions in Blender coordinates
|
|
||||||
"""
|
"""
|
||||||
props = context.scene.SaikeiAlignmentProperties
|
props = context.scene.SaikeiAlignmentProperties
|
||||||
points = []
|
polyline_props = tool.Model.get_polyline_props()
|
||||||
for pi in props.pis:
|
polyline_data = polyline_props.insertion_polyline
|
||||||
# Convert from IFC to Blender coordinates
|
if not polyline_data:
|
||||||
blender_coord = tool.Georeference.enh2xyz((float(pi.e), float(pi.n), 0.0))
|
return
|
||||||
print(repr(blender_coord))
|
|
||||||
points.append(Vector(blender_coord))
|
|
||||||
return points
|
|
||||||
|
|
||||||
def _finish_modal(self, context):
|
polyline_points = polyline_data[0].polyline_points
|
||||||
"""Clean up modal state and uninstall decorator."""
|
if not polyline_points:
|
||||||
context.window.cursor_set("DEFAULT")
|
return
|
||||||
alignment_decorator.PIPickerDecorator.uninstall()
|
|
||||||
if self._area:
|
|
||||||
self._area.tag_redraw()
|
|
||||||
self.report({"INFO"}, "Finished adding PIs")
|
|
||||||
|
|
||||||
def add_pi_at_location(self, context, coord):
|
num_points = len(polyline_points)
|
||||||
"""Add a new PI at the given (x, y) coordinate.
|
for i, point in enumerate(polyline_points):
|
||||||
|
# Convert Blender space -> IFC easting/northing
|
||||||
|
ifc_coord = tool.Georeference.xyz2enh((point.x, point.y, 0.0))
|
||||||
|
|
||||||
The coordinate is in Blender world space. If there's a Blender offset
|
pi = props.pis.add()
|
||||||
configured (for geospatial coordinates), we convert to IFC global
|
pi.e = str(ifc_coord[0])
|
||||||
coordinates before storing.
|
pi.n = str(ifc_coord[1])
|
||||||
"""
|
|
||||||
props = context.scene.SaikeiAlignmentProperties
|
|
||||||
|
|
||||||
# Convert Blender coordinates to IFC coordinates
|
# Determine PI type based on position
|
||||||
# PIs are stored in IFC coordinate space (global/map coordinates)
|
if i == 0 or i == num_points - 1:
|
||||||
ifc_coord = tool.Georeference.xyz2enh((coord[0], coord[1], 0.0))
|
pi.pi_type = "ENDPOINT"
|
||||||
|
else:
|
||||||
pi = props.pis.add()
|
pi.pi_type = "TANGENT"
|
||||||
pi.e = str(ifc_coord[0])
|
|
||||||
pi.n = str(ifc_coord[1])
|
|
||||||
|
|
||||||
# Determine PI type based on position in list
|
|
||||||
if len(props.pis) == 1:
|
|
||||||
pi.pi_type = "ENDPOINT"
|
|
||||||
elif len(props.pis) == 2:
|
|
||||||
pi.pi_type = "ENDPOINT"
|
|
||||||
else:
|
|
||||||
pi.pi_type = "TANGENT"
|
|
||||||
# Previous endpoint becomes tangent
|
|
||||||
if len(props.pis) >= 2:
|
|
||||||
props.pis[-2].pi_type = "TANGENT"
|
|
||||||
|
|
||||||
props.active_pi_index = len(props.pis) - 1
|
props.active_pi_index = len(props.pis) - 1
|
||||||
recalculate_pi_geometry(props)
|
recalculate_pi_geometry(props)
|
||||||
|
rebuild_display_rows(props)
|
||||||
|
|
||||||
|
|
||||||
class SAIKEI_OT_recalculate_pis(Operator):
|
class SAIKEI_OT_recalculate_pis(Operator):
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ class SAIKEI_UL_alignment_pis(UIList):
|
|||||||
sub.prop(pi, "e", text="")
|
sub.prop(pi, "e", text="")
|
||||||
sub.prop(pi, "n", text="")
|
sub.prop(pi, "n", text="")
|
||||||
else:
|
else:
|
||||||
row.label(text=f"{item.e:.2f}")
|
row.label(text=f"{float(item.e):.2f}")
|
||||||
row.label(text=f"{item.n:.2f}")
|
row.label(text=f"{float(item.n):.2f}")
|
||||||
|
|
||||||
# Length column - empty for point rows
|
# Length column - empty for point rows
|
||||||
row.label(text="")
|
row.label(text="")
|
||||||
@@ -86,8 +86,8 @@ class SAIKEI_UL_alignment_pis(UIList):
|
|||||||
row.label(text="Curve", icon="SPHERECURVE")
|
row.label(text="Curve", icon="SPHERECURVE")
|
||||||
|
|
||||||
# Show PI coordinates on curve row
|
# Show PI coordinates on curve row
|
||||||
row.label(text=f"{item.e:.2f}")
|
row.label(text=f"{float(item.e):.2f}")
|
||||||
row.label(text=f"{item.n:.2f}")
|
row.label(text=f"{float(item.n):.2f}")
|
||||||
|
|
||||||
# Arc length
|
# Arc length
|
||||||
row.label(text=f"{item.arc_length:.2f}")
|
row.label(text=f"{item.arc_length:.2f}")
|
||||||
|
|||||||
Reference in New Issue
Block a user