''' Copyright (C) 2023 CG Cookie http://cgcookie.com hello@cgcookie.com Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import math import time from ..rftool import RFTool from ..rfwidget import RFWidget from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory from ..rfwidgets.rfwidget_selectbox import RFWidget_SelectBox_Factory from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory from ...addon_common.common.maths import ( Vec, Vec2D, Point, Point2D, Direction, Color, closest_point_segment, ) from ...addon_common.common.fsm import FSM from ...addon_common.common.boundvar import BoundBool, BoundInt, BoundFloat, BoundString from ...addon_common.common.maths import segment2D_intersection, Point2D, triangle2D_overlap from ...addon_common.common.profiler import profiler from ...addon_common.common.utils import iter_pairs, delay_exec, Dict from ...config.options import options, themes class Select(RFTool): name = 'Select' description = 'Select geometry' icon = 'select-icon.png' help = 'select.md' shortcut = 'select tool' quick_shortcut = 'select quick' statusbar = '{{select box}} Select\t{{select box del}}: Remove selection\t{{select box add}}: Add selection' ui_config = 'select_options.html' RFWidget_Default = RFWidget_Default_Factory.create() RFWidget_SelectBox = RFWidget_SelectBox_Factory.create('Select: Box') RFWidget_Hidden = RFWidget_Hidden_Factory.create() @RFTool.on_init def init(self): self.rfwidgets = { 'default': self.RFWidget_Default(self), 'selectbox': self.RFWidget_SelectBox(self), # circle select???? 'hidden': self.RFWidget_Hidden(self), } self.rfwidget = None @RFTool.on_quickselect_start def quickselect_start(self): self.rfwidgets['selectbox'].quickselect_start() @FSM.on_state('main') def main(self): self.set_widget('selectbox') if self.actions.pressed({'select single', 'select single add'}, unpress=False): sel_only = self.actions.pressed('select single') self.actions.unpress() bmv,_ = self.rfcontext.accel_nearest2D_vert(max_dist=options['select dist']) bme,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['select dist']) bmf,_ = self.rfcontext.accel_nearest2D_face(max_dist=options['select dist']) sel = bmv or bme or bmf if not sel_only and not sel: return self.rfcontext.undo_push('select') if sel_only: self.rfcontext.deselect_all() if not sel: return if sel.select: self.rfcontext.deselect(sel, subparts=False) else: self.rfcontext.select(sel, supparts=False, only=sel_only) return if self.actions.pressed('grab'): self.rfcontext.undo_push('move grabbed') return 'move' def select_linked(self): self.rfcontext.undo_push('select linked') self.rfcontext.select_linked() def deselect_all(self): self.rfcontext.undo_push('deselect all') self.rfcontext.deselect_all() def select_invert(self): self.rfcontext.undo_push('invert selection') self.rfcontext.select_invert() @RFWidget.on_action('Select: Box') def selectbox(self): box = self.rfwidgets['selectbox'] p0, p1 = box.box2D if not p0 or not p1: return (x0, y0), (x1, y1) = p0, p1 left, right = min(x0, x1), max(x0, x1) bottom, top = min(y0, y1), max(y0, y1) c0, c1, c2, c3 = Point2D((left, top)), Point2D((left, bottom)), Point2D((right, bottom)), Point2D((right, top)) tri0, tri1 = (c0, c1, c2), (c0, c2, c3) get_point2D = self.rfcontext.get_point2D def vert_inside(vert): p = get_point2D(vert.co) return left <= p.x <= right and bottom <= p.y <= top def edge_inside(edge): v0, v1 = edge.verts if vert_inside(v0) or vert_inside(v1): return True p0, p1 = get_point2D(v0.co), get_point2D(v1.co) return any(( segment2D_intersection(c0, c1, p0, p1), segment2D_intersection(c1, c2, p0, p1), segment2D_intersection(c1, c3, p0, p1), segment2D_intersection(c3, c0, p0, p1), )) def face_inside(face): points = [get_point2D(v.co) for v in face.verts] p0 = points[0] return any(( triangle2D_overlap((p0, p1, p2), tri0) or triangle2D_overlap((p0, p1, p2), tri1) for p1, p2 in zip(points[1:-1], points[2:]) )) match options['select geometry']: case 'Verts': verts = { vert for vert in self.rfcontext.get_vis_verts() if vert_inside(vert) } case 'Edges': verts = { vert for edge in self.rfcontext.get_vis_edges() if edge_inside(edge) for vert in edge.verts } case 'Faces': verts = { vert for face in self.rfcontext.get_vis_faces() if face_inside(face) for vert in face.verts } self.rfcontext.undo_push('select box') if box.mods['ctrl']: self.rfcontext.select(self.rfcontext.get_selected_verts() - verts, only=True) # del verts from selection elif box.mods['shift']: self.rfcontext.select(verts, only=False) # add vert to selection else: self.rfcontext.select(verts, only=True) # replace selection @FSM.on_state('move', 'enter') def move_enter(self): self.move_data = Dict() Point_to_Point2D = self.rfcontext.Point_to_Point2D self.move_data.bmverts_xys = [ (bmv, xy) for bmv in self.rfcontext.get_selected_verts() if bmv and bmv.is_valid and (xy := self.rfcontext.Point_to_Point2D(bmv.co)) ] self.move_data.bmverts = [ bmv for (bmv, _) in self.move_data.bmverts_xys ] self.move_data.mousedown = self.actions.mouse self.move_data.last_delta = None if options['select automerge']: self.move_data.vis_accel = self.rfcontext.get_custom_vis_accel( selection_only=False, include_edges=False, include_faces=False, symmetry=False, ) self.rfcontext.split_target_visualization_selected() self.rfcontext.fast_update_timer.start() self.rfcontext.set_accel_defer(True) if options['hide cursor on tweak']: self.set_widget('hidden') @FSM.on_state('move') def modal_move(self): if self.actions.pressed(['confirm', 'confirm drag']): if options['select automerge']: self.rfcontext.merge_verts_by_dist(self.move_data.bmverts, options['select merge dist']) return 'main' if self.actions.pressed('cancel'): self.rfcontext.undo_cancel() return 'main' @RFTool.on_mouse_move @RFTool.once_per_frame @FSM.onlyinstate('move') def modal_move_update(self): delta = Vec2D(self.actions.mouse - self.move_data.mousedown) if delta == self.move_data.last_delta: return self.move_data.last_delta = delta set2D_vert = self.rfcontext.set2D_vert for bmv,xy in self.move_data.bmverts_xys: if not xy: continue xy_updated = xy + delta if options['select automerge']: # snap xy_updated to any visible verts close enough to current xy_updated (in image plane) bmv1, _ = self.rfcontext.accel_nearest2D_vert(point=xy_updated, vis_accel=self.move_data.vis_accel, max_dist=options['select merge dist']) xy1 = self.rfcontext.Point_to_Point2D(bmv1.co) if bmv1 else None if xy1: xy_updated = xy1 set2D_vert(bmv, xy_updated) self.rfcontext.update_verts_faces(self.move_data.bmverts) self.rfcontext.dirty() @FSM.on_state('move', 'exit') def move_exit(self): self.rfcontext.set_accel_defer(False) self.rfcontext.fast_update_timer.stop() self.rfcontext.clear_split_target_visualization()