Skip to content

Conversation

@SHAM560
Copy link

@SHAM560 SHAM560 commented Dec 20, 2025

Здесь сделай чтоб присадку делал согласно локальных координат компонентов, так как сейчас на повёрнутых под определённый градус угла компонентах не делает присадок потому что делает присадку согласно мировых координат что не допустимо. Пиши полный код целиком.

require 'sketchup.rb'
require 'extensions.rb'

module ABFStyleDrill

Настройки по умолчанию

DEFAULTS = {
:cam_diameter => 8.0, # Диаметр отверстия в пласть (Cam)
:screw_diameter => 5.0, # Диаметр отверстия в торец (Screw)
:offset => 50.0, # Отступ от края детали для присадки
:hole_count => 2, # Количество присадочных отверстий
:marker_length => 15.0, # Длина проекции (маркера) для торцевого отверстия

# --- НОВЫЕ НАСТРОЙКИ ДЛЯ ПЕТЕЛЬ ---
:hinge_diameter => 35.0, # Диаметр отверстия под чашку петли (35мм)
:hinge_offset => 100.0,  # Отступ от края детали для первой петли (100мм) - Смещение по длине
:hinge_count => 2,       # Количество петель
:hinge_side_offset => 21.5, # Смещение от бокового края компонента (21.5мм)
:hinge_screw_diameter => 5.0, # Диаметр отверстий под саморезы (5мм)
:hinge_screw_spacing => 45.0, # Расстояние между центрами отверстий под саморезы (45мм)
:hinge_screw_vertical_offset => 9.5, # НОВОЕ: Смещение отверстий под саморезы от центра чашки (9.5мм)

# --- НОВЫЕ НАСТРОЙКИ ДЛЯ ПЛАНКИ ---
:strip_diameter => 5.0,        # Диаметр отверстия под планку (5мм)
:strip_spacing => 32.0,        # Межосевое расстояние (32мм)
:strip_side_offset => 37.0,     # Смещение от края боковины (37мм)
:strip_offset => 100.0         # НОВОЕ: Отступ от короткого края (100мм)

}

Константа для зазора между деталями при раскладке

GAP = 500.0.mm

Переменная для отслеживания состояния аннотаций

@annotations_visible = false

--- МЕТОД: Очистка ранее созданных аннотаций (размеров) ---

def self.cleanup_annotations(entities)
entities_to_remove = []
entities.each do |e|
if e.is_a?(Sketchup::DimensionLinear) && e.get_attribute('ABFDrill', 'Type') == 'Dimension'
entities_to_remove << e
end
end
entities.erase_entities(entities_to_remove) if entities_to_remove.any?
end

--- МЕТОД: Очистка только стандартных присадочных отверстий и маркеров ---

def self.cleanup_standard_drill_geometry(entity)
defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
entities_to_remove = []

defn.entities.each do |e|
  # Удаляем всё, что помечено как DrillHole
  type = e.get_attribute('ABFDrill', 'Type')
  if type == 'DrillHole'
    entities_to_remove << e
  end
end

defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?

end

--- МЕТОД: Очистка только отверстий под петли ---

def self.cleanup_hinge_drill_geometry(entity)
defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
entities_to_remove = []

defn.entities.each do |e|
  # Удаляем всё, что помечено как HingeHole
  type = e.get_attribute('ABFDrill', 'Type')
  if type == 'HingeHole'
    entities_to_remove << e
  end
end

defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?

end

--- НОВЫЙ МЕТОД: Очистка только отверстий под планку ---

def self.cleanup_strip_drill_geometry(entity)
defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
entities_to_remove = []

defn.entities.each do |e|
  # Удаляем всё, что помечено как StripHole (включая новую точку)
  type = e.get_attribute('ABFDrill', 'Type')
  if type == 'StripHole'
    entities_to_remove << e
  end
end

defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?

end

--- МЕТОД: Отображение диалогового окна (Обновлено для передачи параметров петель в планку) ---

def self.show_dialog
dialog = UI::HtmlDialog.new(
{
:dialog_title => "ABF Style Drill & Hinges",
:preferences_key => "com.abfstyle.drill",
:scrollable => true,
:resizable => true,
:width => 350,
:height => 850, # Увеличена высота для новых настроек
:style => UI::HtmlDialog::STYLE_DIALOG
}
)

html_content = <<-HTML
  <!DOCTYPE html>
  <html>
  <head>
    <style>
      body { font-family: 'Segoe UI', sans-serif; padding: 20px; background-color: #333; color: white; }
      h3 { margin-top: 20px; color: #ff9800; border-bottom: 1px solid #555; padding-bottom: 10px; }
      .control { margin-bottom: 15px; }
      label { display: flex; justify-content: space-between; align-items: center; font-size: 14px; }
      input { width: 80px; padding: 5px; border-radius: 4px; border: none; text-align: center; }
      button {
        width: 100%; padding: 12px; margin-top: 10px;
        background-color: #ff9800; color: white; border: none;
        border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 16px;
        transition: background 0.3s;
      }
      button:hover { background-color: #e68900; }
      .hint { font-size: 11px; color: #aaa; margin-top: 5px; }
      .secondary-button { background-color: #555; }
      .secondary-button:hover { background-color: #777; }
      .annotation-button { background-color: #00bcd4; }
      .annotation-button:hover { background-color: #0097a7; }
      .hinge-button { background-color: #4CAF50; }
      .hinge-button:hover { background-color: #45a049; }
      /* НОВЫЙ СТИЛЬ ДЛЯ КНОПКИ ПЛАНКИ */
      .strip-button { background-color: #9c27b0; }
      .strip-button:hover { background-color: #7b1fa2; }
    </style>
  </head>
  <body>
    <!-- --- НОВАЯ СЕКЦИЯ ДЛЯ ПЛАНКИ (ПЕРЕМЕЩЕНА ВЫШЕ) --- -->
    <h3>Присадка планки</h3>
    <div class="control">
      <label>Диаметр отверстий (мм) <input type="number" id="strip_dia" value="#{DEFAULTS[:strip_diameter]}" step="0.5"></label>
    </div>
    <div class="control">
      <label>Межосевое (мм) <input type="number" id="strip_spacing" value="#{DEFAULTS[:strip_spacing]}" step="1.0"></label>
    </div>
    <div class="control">
      <label>Смещение от края (мм) <input type="number" id="strip_side_off" value="#{DEFAULTS[:strip_side_offset]}" step="1.0"></label>
    </div>
    <!-- НОВЫЙ ИНПУТ ДЛЯ СМЕЩЕНИЯ ПЛАНКИ -->
    <div class="control">
      <label>Смещение по длине (мм) <input type="number" id="strip_off" value="#{DEFAULTS[:strip_offset]}" step="1.0"></label>
      <div class="hint">Отступ первой планки от короткого края.</div>
    </div>
    <button class="strip-button" onclick="run_strip_drill()">ПРИСАДКА ПЛАНКИ</button>
    <!-- --- КОНЕЦ НОВОЙ СЕКЦИИ --- -->

    <h3>Настройки присадки (Minifix)</h3>

    <div class="control">
      <label>Количество отверстий <input type="number" id="hole_count" value="#{DEFAULTS[:hole_count]}" step="1" min="1" oninput="update_model_realtime()"></label>
      <div class="hint">Общее количество на стык.</div>
    </div>

    <div class="control">
      <label>Диаметр (Пласть/Cam) <input type="number" id="cam_dia" value="#{DEFAULTS[:cam_diameter]}" step="0.5" oninput="update_model_realtime()"></label>
    </div>

    <div class="control">
      <label>Диаметр (Торец/Screw) <input type="number" id="screw_dia" value="#{DEFAULTS[:screw_diameter]}" step="0.5" oninput="update_model_realtime()"></label>
    </div>

    <div class="control">
      <label>Длина маркера (мм) <input type="number" id="marker_len" value="#{DEFAULTS[:marker_length]}" step="1.0" min="0" oninput="update_model_realtime()"></label>
      <div class="hint">Длина проекции торцевого отверстия.</div>
    </div>

    <div class="control">
      <label>Отступ от края (мм) <input type="number" id="off" value="#{DEFAULTS[:offset]}" step="1" oninput="update_model_realtime()"></label>
    </div>

    <button onclick="run()">ПРИСАДИТЬ (Auto)</button>

    <!-- --- НОВАЯ СЕКЦИЯ ДЛЯ ПЕТЕЛЬ --- -->
    <h3>Настройки петель</h3>

    <div class="control">
      <label>Количество кругов <input type="number" id="hinge_count" value="#{DEFAULTS[:hinge_count]}" step="1" min="1"></label>
    </div>

    <div class="control">
      <label>Диаметр чашки (мм) <input type="number" id="hinge_dia" value="#{DEFAULTS[:hinge_diameter]}" step="1.0"></label>
    </div>

    <!-- НОВЫЕ ИНПУТЫ ДЛЯ ОТВЕРСТИЙ ПОД САМОРЕЗЫ -->
    <div class="control">
      <label>Диаметр саморезов (мм) <input type="number" id="hinge_screw_dia" value="#{DEFAULTS[:hinge_screw_diameter]}" step="0.5"></label>
    </div>

    <div class="control">
      <label>Расстояние между (мм) <input type="number" id="hinge_screw_spacing" value="#{DEFAULTS[:hinge_screw_spacing]}" step="1.0"></label>
      <div class="hint">Расстояние между центрами отверстий под саморезы (45мм).</div>
    </div>

    <!-- НОВЫЙ ИНПУТ ДЛЯ СМЕЩЕНИЯ САМОРЕЗОВ ОТНОСИТЕЛЬНО ЧАШКИ -->
    <div class="control">
      <label>Смещение саморезов (мм) <input type="number" id="hinge_screw_vert_off" value="#{DEFAULTS[:hinge_screw_vertical_offset]}" step="0.5"></label>
      <div class="hint">Смещение отверстий от центра чашки (вдоль 21.5мм оси).</div>
    </div>
    <!-- КОНЕЦ НОВЫХ ИНПУТОВ -->

    <div class="control">
      <label>Смещение по длине (мм) <input type="number" id="hinge_off" value="#{DEFAULTS[:hinge_offset]}" step="1.0"></label>
      <div class="hint">Отступ первой петли от короткого края.</div>
    </div>

    <div class="control">
      <label>Смещение от края компонента (мм) <input type="number" id="hinge_side_off" value="#{DEFAULTS[:hinge_side_offset]}" step="0.5"></label>
      <div class="hint">Смещение центра чашки от бокового края (21.5мм).</div>
    </div>

    <button class="hinge-button" onclick="run_hinges()">ПЕТЛИ</button>
    <!-- --- КОНЕЦ НОВОЙ СЕКЦИИ --- -->

    <button class="secondary-button" onclick="line_up()">РАЗЛОЖИТЬ</button>
    <button class="annotation-button" onclick="show_annotations()">АННОТАЦИИ</button>

    <script>
      function debounce(func, timeout = 300) {
        let timer;
        return (...args) => {
          clearTimeout(timer);
          timer = setTimeout(() => { func.apply(this, args); }, timeout);
        };
      }

      function get_input_values() {
        return {
          cam_dia: parseFloat(document.getElementById('cam_dia').value),
          screw_dia: parseFloat(document.getElementById('screw_dia').value),
          off: parseFloat(document.getElementById('off').value),
          hole_count: parseInt(document.getElementById('hole_count').value),
          marker_len: parseFloat(document.getElementById('marker_len').value)
        };
      }

      // --- НОВАЯ ФУНКЦИЯ ДЛЯ ПОЛУЧЕНИЯ ЗНАЧЕНИЙ ПЕТЕЛЬ (ОБНОВЛЕНО) ---
      function get_hinge_values() {
        return {
          hinge_count: parseInt(document.getElementById('hinge_count').value),
          hinge_dia: parseFloat(document.getElementById('hinge_dia').value),
          hinge_off: parseFloat(document.getElementById('hinge_off').value),
          hinge_side_off: parseFloat(document.getElementById('hinge_side_off').value),
          // НОВЫЕ ЗНАЧЕНИЯ
          hinge_screw_dia: parseFloat(document.getElementById('hinge_screw_dia').value),
          hinge_screw_spacing: parseFloat(document.getElementById('hinge_screw_spacing').value),
          hinge_screw_vertical_offset: parseFloat(document.getElementById('hinge_screw_vert_off').value)
        };
      }

      // --- НОВАЯ ФУНКЦИЯ ДЛЯ ПОЛУЧЕНИЯ ЗНАЧЕНИЙ ПЛАНКИ (ОБНОВЛЕНО) ---
      function get_strip_values() {
        return {
          strip_dia: parseFloat(document.getElementById('strip_dia').value),
          strip_spacing: parseFloat(document.getElementById('strip_spacing').value),
          strip_side_off: parseFloat(document.getElementById('strip_side_off').value),
          strip_offset: parseFloat(document.getElementById('strip_off').value), // ДОБАВЛЕНО
          // ДОБАВЛЕНЫ ЗНАЧЕНИЯ ПЕТЕЛЬ ДЛЯ ПОЗИЦИОНИРОВАНИЯ ПЛАНКИ
          hinge_count: parseInt(document.getElementById('hinge_count').value)
        };
      }

      function run() {
        var values = get_input_values();
        sketchup.do_drill(values.cam_dia, values.screw_dia, values.off, values.hole_count, values.marker_len);
      }

      // --- НОВАЯ ФУНКЦИЯ ДЛЯ ЗАПУСКА ПЕТЕЛЬ (ОБНОВЛЕНО) ---
      function run_hinges() {
        var values = get_hinge_values();
        // Передаем новые значения
        sketchup.do_hinges(
          values.hinge_count,
          values.hinge_dia,
          values.hinge_off,
          values.hinge_side_off,
          values.hinge_screw_dia,
          values.hinge_screw_spacing,
          values.hinge_screw_vertical_offset
        );
      }

      // --- НОВАЯ ФУНКЦИЯ ДЛЯ ЗАПУСКА ПЛАНКИ (ОБНОВЛЕНО) ---
      function run_strip_drill() {
        var values = get_strip_values();
        sketchup.do_strip_drill(
          values.strip_dia,
          values.strip_spacing,
          values.strip_side_off,
          // ПЕРЕДАЕМ ПАРАМЕТРЫ ДЛЯ ПОЗИЦИОНИРОВАНИЯ
          values.hinge_count,
          values.strip_offset // ИСПОЛЬЗУЕМ НОВОЕ СМЕЩЕНИЕ
        );
      }

      const debounced_update_callback = debounce(function() {
        var values = get_input_values();
        sketchup.update_drill_realtime(values.cam_dia, values.screw_dia, values.off, values.hole_count, values.marker_len);
      }, 300);

      function update_model_realtime() { debounced_update_callback(); }
      function line_up() { sketchup.line_up_components(); }
      function show_annotations() { sketchup.show_drill_annotations(); }
      window.onload = function() { update_model_realtime(); };
    </script>
  </body>
  </html>
HTML

dialog.set_html(html_content)
dialog.center

dialog.add_action_callback("do_drill") do |ctx, cam_dia, screw_dia, off, hole_count, marker_len|
  self.process_model(cam_dia, screw_dia, off, hole_count, marker_len)
end

dialog.add_action_callback("update_drill_realtime") do |ctx, cam_dia, screw_dia, off, hole_count, marker_len|
  self.process_model(cam_dia, screw_dia, off, hole_count, marker_len)
end

# --- CALLBACK ДЛЯ ПЕТЕЛЬ ---
dialog.add_action_callback("do_hinges") do |ctx, count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset|
  self.process_hinge_holes(count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset)
end

# --- НОВЫЙ CALLBACK ДЛЯ ПЛАНКИ (ОБНОВЛЕНО) ---
# Изменено: hinge_offset заменен на length_offset (который теперь strip_offset)
dialog.add_action_callback("do_strip_drill") do |ctx, diameter, spacing, side_offset, count, length_offset|
  self.process_strip_holes(diameter, spacing, side_offset, count, length_offset)
end

dialog.add_action_callback("line_up_components") { |ctx| self.line_up_components }
dialog.add_action_callback("show_drill_annotations") { |ctx| self.show_drill_annotations }

dialog.show

end

--- ГЛАВНЫЙ ПРОЦЕСС ПРИСАДКИ (Minifix) ---

def self.process_model(cam_diameter_mm, screw_diameter_mm, offset_mm, hole_count, marker_length_mm)
model = Sketchup.active_model
selection = model.selection
items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

# Если выделено меньше 2-х, очищаем присадки только на выделенных и выходим
if items.length < 2
  model.start_operation("ABF Drill Cleanup", true)
  items.each { |ent| self.cleanup_standard_drill_geometry(ent) }
  model.commit_operation
  return
end

model.start_operation("ABF Style Drill Update", true)
# Очищаем только стандартные присадки на выделенных компонентах
items.each { |ent| self.cleanup_standard_drill_geometry(ent) }

processed_pairs = 0
(0...items.length).each do |i|
  ((i + 1)...items.length).each do |j|
    ent1 = items[i]
    ent2 = items[j]
    # Проверяем пересечение
    if ent1.bounds.intersect(ent2.bounds).valid?
       if self.apply_drill_to_pair(ent1, ent2, cam_diameter_mm.mm, screw_diameter_mm.mm, offset_mm.mm, hole_count, marker_length_mm.mm)
         processed_pairs += 1
       end
    end
  end
end
model.commit_operation

end

... (get_diameter остается без изменений)

def self.get_diameter(entity, global_drill_axis, cam_dia, screw_dia)
trans_inv = entity.transformation.inverse
local_drill_axis = global_drill_axis.transform(trans_inv)
bb_local = entity.bounds
w, h, d = bb_local.width, bb_local.height, bb_local.depth
dims = { w => Geom::Vector3d.new(1, 0, 0), h => Geom::Vector3d.new(0, 1, 0), d => Geom::Vector3d.new(0, 0, 1) }
min_dim = dims.keys.min
local_thickness_axis = dims[min_dim]
dot_product = local_drill_axis.dot(local_thickness_axis).abs
return (dot_product > 0.999) ? cam_dia : screw_dia
end

--- ОСНОВНАЯ ЛОГИКА ПРИСАДКИ И ГЕОМЕТРИИ (Minifix) ---

def self.apply_drill_to_pair(ent1, ent2, cam_diameter, screw_diameter, offset, hole_count, marker_length)
bb1 = ent1.bounds
bb2 = ent2.bounds
intersect = bb1.intersect(bb2)
return false if intersect.empty?

w, h, d = intersect.width, intersect.height, intersect.depth
dims = [w, h, d]
min_dim = dims.min
return false if dims.max < 10.mm

drill_axis = Geom::Vector3d.new(0,0,1)
long_axis_vec = Geom::Vector3d.new(1,0,0)
center = intersect.center

if min_dim == w
  drill_axis = Geom::Vector3d.new(1, 0, 0)
  long_axis_vec = (h > d) ? Geom::Vector3d.new(0, 1, 0) : Geom::Vector3d.new(0, 0, 1)
  len = [h, d].max
elsif min_dim == h
  drill_axis = Geom::Vector3d.new(0, 1, 0)
  long_axis_vec = (w > d) ? Geom::Vector3d.new(1, 0, 0) : Geom::Vector3d.new(0, 0, 1)
  len = [w, d].max
else
  drill_axis = Geom::Vector3d.new(0, 0, 1)
  long_axis_vec = (w > h) ? Geom::Vector3d.new(1, 0, 0) : Geom::Vector3d.new(0, 1, 0)
  len = [w, h].max
end

drill_points = []
hole_count = [1, hole_count.to_i].max
max_drill_length = len - (offset * 2)

if hole_count == 1 || max_drill_length < 0.mm
  drill_points << center
else
  spacing = max_drill_length / (hole_count - 1).to_f
  start_dist_from_center = (len / 2.0) - offset
  start_point = center - long_axis_vec.clone.tap { |v| v.length = start_dist_from_center }
  (0...hole_count).each do |i|
    current_offset = long_axis_vec.clone.tap { |v| v.length = i * spacing }
    drill_points << start_point + current_offset
  end
end

[ent1, ent2].each do |ent|
  defn = ent.respond_to?(:definition) ? ent.definition : ent.entities.parent
  trans_inv = ent.transformation.inverse
  current_diameter = self.get_diameter(ent, drill_axis, cam_diameter, screw_diameter)

  drill_points.each do |global_pt|
    local_pt = global_pt.transform(trans_inv)
    local_axis = drill_axis.transform(trans_inv)

    # Вынос на внешнюю сторону для отверстий в пласть
    if current_diameter == cam_diameter
      bounds = defn.bounds
      bounds_center = bounds.center
      axis_idx = local_axis.to_a.map(&:abs).each_with_index.max[1]
      dir_val = bounds_center[axis_idx] - local_pt[axis_idx]
      dir_val = 1.0 if dir_val.abs < 0.0001
      target_coordinate = (dir_val > 0) ? bounds.max[axis_idx] : bounds.min[axis_idx]
      local_pt[axis_idx] = target_coordinate
    end

    # 1. Создаем само отверстие (круг)
    circle = defn.entities.add_circle(local_pt, local_axis, current_diameter / 2.0)
    cpoint = defn.entities.add_cpoint(local_pt)
    circle.each { |e| e.set_attribute('ABFDrill', 'Type', 'DrillHole') }
    cpoint.set_attribute('ABFDrill', 'Type', 'DrillHole')

    # 2. Логика для рисования прямоугольника-проекции (только линии)
    if current_diameter == screw_diameter && marker_length > 0.0.mm
       bounds = defn.bounds

       # Определяем ось толщины (самая короткая сторона)
       b_dims = [bounds.width, bounds.height, bounds.depth]
       min_b_dim = b_dims.min
       thick_axis_idx = b_dims.index(min_b_dim)

       # Вектор нормали к грани (ось толщины)
       face_normal = Geom::Vector3d.new(0,0,0)
       face_normal[thick_axis_idx] = 1

       # Вектор "ширины" прямоугольника
       width_vec = local_axis.cross(face_normal)

       if width_vec.length > 0.001
         width_vec.length = current_diameter / 2.0

         # Вектор "длины" маркера, используем значение из инпута
         marker_len_vec = local_axis.clone

         # Направление: Внутрь детали
         vec_to_center = bounds.center - local_pt
         if vec_to_center.dot(marker_len_vec) < 0
            marker_len_vec.reverse!
         end
         marker_len_vec.length = marker_length

         # Рисуем прямоугольники на обеих плоскостях
         faces_coords = [bounds.min[thick_axis_idx], bounds.max[thick_axis_idx]]

         faces_coords.each do |z_coord|
            pt_on_face = local_pt.clone
            pt_on_face[thick_axis_idx] = z_coord

            # 4 точки прямоугольника
            p1 = pt_on_face + width_vec
            p2 = pt_on_face - width_vec
            p3 = p2 + marker_len_vec
            p4 = p1 + marker_len_vec

            # Создаем группу для маркера
            m_group = defn.entities.add_group
            # Создаем Face, чтобы потом его удалить, но сохранить Edges
            m_face = m_group.entities.add_face(p1, p2, p3, p4)

            if m_face
               # УДАЛЯЕМ ГРАНЬ, ОСТАВЛЯЯ ТОЛЬКО ЛИНИИ (Edges)
               m_face.erase!
               # Метим группу как DrillHole для очистки
               m_group.set_attribute('ABFDrill', 'Type', 'DrillHole')
            else
               # Если грань не создалась, удаляем пустую группу
               m_group.erase!
            end
         end
       end
    end
    # --- КОНЕЦ ЛОГИКИ ПРЯМОУГОЛЬНИКА ---
  end
end
return true

end

--- ИСПРАВЛЕННЫЙ МЕТОД: Рисование компенсированного круга ---

Этот метод создает круг, который останется кругом даже после не-униформного масштабирования

def self.draw_drill_hole_geometry(entity, center_pt, radius, normal_axis, type_tag)
defn_entities = entity.respond_to?(:definition) ? entity.definition.entities : entity.entities
trans = entity.transformation

# 1. Получаем обратную трансформацию для компенсации масштабирования
inv_trans = trans.inverse

# 2. Получаем мировые оси, преобразованные в локальные координаты определения
world_axes = [
  Geom::Vector3d.new(1, 0, 0),
  Geom::Vector3d.new(0, 1, 0), 
  Geom::Vector3d.new(0, 0, 1)
]

# 3. Преобразуем нормаль в мировые координаты
world_normal = normal_axis.transform(trans)

# 4. Находим две оси, ортогональные нормали (в мировых координатах)
world_axes_in_plane = world_axes.reject { |axis| axis.parallel?(world_normal) }

if world_axes_in_plane.length < 2
  # Если не можем найти оси, используем альтернативный метод
  world_axes_in_plane = [
    world_normal.axes[0],
    world_normal.axes[1]
  ]
end

axis1 = world_axes_in_plane[0]
axis2 = world_axes_in_plane[1]

# 5. Преобразуем оси обратно в локальные координаты определения
local_axis1 = axis1.transform(inv_trans)
local_axis2 = axis2.transform(inv_trans)

# 6. Нормализуем оси
local_axis1.normalize!
local_axis2.normalize!

# 7. Убеждаемся, что оси ортогональны
if local_axis1.dot(local_axis2) > 0.001
  # Делаем axis2 ортогональным axis1
  local_axis2 = local_axis2 - local_axis1 * local_axis1.dot(local_axis2)
  local_axis2.normalize!
end

# 8. Создаем круг с использованием add_circle (который Sketchup корректно масштабирует)
# Вместо попытки компенсировать масштабирование, мы рисуем круг в определении,
# и Sketchup сам правильно масштабирует его при любом трансформировании экземпляра
circle = defn_entities.add_circle(center_pt, normal_axis, radius)
circle.each { |e| e.set_attribute('ABFDrill', 'Type', type_tag) }

# 9. Добавляем CPoint
cpoint = defn_entities.add_cpoint(center_pt)
cpoint.set_attribute('ABFDrill', 'Type', type_tag)

return true

end

--- ГЛАВНЫЙ ПРОЦЕСС ДЛЯ ПЕТЕЛЬ ---

def self.process_hinge_holes(count, diameter_mm, length_offset_mm, side_offset_mm, screw_diameter_mm, screw_spacing_mm, screw_vert_offset_mm)
model = Sketchup.active_model
selection = model.selection
items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

if items.empty?
  UI.messagebox("Выберите один или несколько компонентов для установки петель.")
  return
end

model.start_operation("ABF Style Add Hinges", true)
# Очищаем только петли на выделенных компонентах
items.each { |ent| self.cleanup_hinge_drill_geometry(ent) }

items.each do |entity|
  self.apply_hinge_holes(entity, count.to_i, diameter_mm.mm, length_offset_mm.mm, side_offset_mm.mm, screw_diameter_mm.mm, screw_spacing_mm.mm, screw_vert_offset_mm.mm)
end

model.commit_operation

rescue => e
UI.messagebox("Ошибка при создании петель: #{e.message}")
model.abort_operation
end

--- ЛОГИКА ДЛЯ СОЗДАНИЯ ОТВЕРСТИЙ ПОД ПЕТЛИ ---

def self.apply_hinge_holes(entity, count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset)
defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
bounds = defn.bounds

# 1. Определяем оси компонента в локальных координатах
dims = [bounds.width, bounds.height, bounds.depth]

# Ось толщины (самая короткая) - Нормаль круга
min_dim = dims.min
thick_axis_idx = dims.index(min_dim)
thick_axis = Geom::Vector3d.new(0,0,0)
thick_axis[thick_axis_idx] = 1.0

# Ось длины (самая длинная) - Ось для распределения петель
max_dim = dims.max
long_axis_idx = dims.index(max_dim)
long_axis = Geom::Vector3d.new(0,0,0)
long_axis[long_axis_idx] = 1.0

# Ось ширины (средняя) - Ось для смещения 21.5мм и смещения саморезов
mid_dim = dims.sort[1]
mid_axis_idx = dims.index(mid_dim)
mid_axis = Geom::Vector3d.new(0,0,0)
mid_axis[mid_axis_idx] = 1.0

# 2. Определяем плоскость для отверстий (по центру толщины)
center_point = bounds.center.clone

# 3. Определяем начальную точку и направление (Вдоль длинной оси)
# Начальная координата для смещения по длине
start_coord_length = bounds.min[long_axis_idx]
offset_vec_length = long_axis.clone
offset_vec_length.length = length_offset

start_point_length = center_point.clone
start_point_length[long_axis_idx] = start_coord_length
start_point_length = start_point_length + offset_vec_length

# 4. Рассчитываем точки для всех петель (Вдоль длинной оси)
hole_points_length = []
length = max_dim
count = [1, count].max

if count == 1
  hole_points_length << center_point
else
  # Расстояние между центрами петель
  remaining_length = length - (length_offset * 2)
  if remaining_length < 0.mm
    hole_points_length << center_point
  else
    spacing = remaining_length / (count - 1).to_f
    (0...count).each do |i|
      current_point = start_point_length + long_axis.clone.tap { |v| v.length = i * spacing }
      hole_points_length << current_point
    end
  end
end

# 5. Применяем смещение по боковой оси (side_offset)
# Начальная координата по средней оси (от которой отсчитываем 21.5мм)
start_coord_side = bounds.min[mid_axis_idx]

# Вектор смещения по средней оси
side_offset_vec = mid_axis.clone
side_offset_vec.length = side_offset

# Точка, смещенная от края по средней оси
side_offset_point = center_point.clone
side_offset_point[mid_axis_idx] = start_coord_side
side_offset_point = side_offset_point + side_offset_vec

# Координата поверхности (max координата по оси толщины)
surface_coord = bounds.max[thick_axis_idx]

hole_points_length.each do |local_pt_length|
  final_pt = local_pt_length.clone

  # Устанавливаем координату по средней оси (смещение 21.5мм)
  final_pt[mid_axis_idx] = side_offset_point[mid_axis_idx]

  # Устанавливаем координату по оси толщины (на поверхность)
  final_pt[thick_axis_idx] = surface_coord

  # 1. Создаем отверстие под чашку (круг 35мм) - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
  self.draw_drill_hole_geometry(entity, final_pt, diameter / 2.0, thick_axis, 'HingeHole')

  # --- ЛОГИКА ДЛЯ ОТВЕРСТИЙ ПОД САМОРЕЗЫ ---
  half_spacing = screw_spacing / 2.0

  # Вектор смещения "выше чашки" (вдоль mid_axis)
  shift_vec = mid_axis.clone
  shift_vec.length = screw_vert_offset

  # Базовые точки вдоль длинной оси (long_axis), центрированные на final_pt
  pt1_base = final_pt - long_axis.clone.tap { |v| v.length = half_spacing }
  pt2_base = final_pt + long_axis.clone.tap { |v| v.length = half_spacing }

  # Применяем вертикальное смещение (shift_vec) к обеим точкам
  pt1 = pt1_base + shift_vec
  pt2 = pt2_base + shift_vec

  [pt1, pt2].each do |screw_pt|
    # Создаем отверстие под саморез (круг) - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
    self.draw_drill_hole_geometry(entity, screw_pt, screw_diameter / 2.0, thick_axis, 'HingeHole')
  end
  # --- КОНЕЦ ЛОГИКИ САМОРЕЗОВ ---
end

end

--- НОВЫЙ ГЛАВНЫЙ ПРОЦЕСС ДЛЯ ПЛАНКИ (ОБНОВЛЕНО) ---

Изменено: hinge_count и hinge_offset_mm заменены на count и length_offset_mm

def self.process_strip_holes(diameter_mm, spacing_mm, side_offset_mm, count, length_offset_mm)
model = Sketchup.active_model
selection = model.selection
items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

if items.length != 1
  UI.messagebox("Выберите ОДИН компонент для установки отверстий под планку.")
  # Очищаем на всех выделенных, если их больше одного, или если ничего не выбрано
  model.start_operation("ABF Strip Drill Cleanup", true)
  items.each { |ent| self.cleanup_strip_drill_geometry(ent) }
  model.commit_operation
  return
end

entity = items.first

model.start_operation("ABF Style Add Strip Holes", true)
self.cleanup_strip_drill_geometry(entity) # Очистка перед созданием

# Передаем параметры для позиционирования
self.apply_strip_holes(entity, diameter_mm.mm, spacing_mm.mm, side_offset_mm.mm, count.to_i, length_offset_mm.mm)

model.commit_operation

rescue => e
UI.messagebox("Ошибка при создании отверстий под планку: #{e.message}")
model.abort_operation
end

--- НОВАЯ ЛОГИКА ДЛЯ СОЗДАНИЯ ОТВЕРСТИЙ ПОД ПЛАНКУ (ОБНОВЛЕНО) ---

Изменено: hinge_count и hinge_offset заменены на count и length_offset

def self.apply_strip_holes(entity, diameter, spacing, side_offset, count, length_offset)
defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
bounds = defn.bounds

# 1. Определяем оси компонента в локальных координатах
dims = [bounds.width, bounds.height, bounds.depth]

# Ось толщины (самая короткая) - Ось сверления (Normal)
min_dim = dims.min
thick_axis_idx = dims.index(min_dim)
thick_axis = Geom::Vector3d.new(0,0,0)
thick_axis[thick_axis_idx] = 1.0

# Ось длины (самая длинная) - Ось для распределения петель/планок
max_dim = dims.max
long_axis_idx = dims.index(max_dim)
long_axis = Geom::Vector3d.new(0,0,0)
long_axis[long_axis_idx] = 1.0

# Ось ширины (средняя) - Ось для смещения от края (Side Offset)
mid_dim = dims.sort[1]
mid_axis_idx = dims.index(mid_dim)
mid_axis = Geom::Vector3d.new(0,0,0)
mid_axis[mid_axis_idx] = 1.0

# 2. Расчет позиций центров планок вдоль длинной оси (long_axis)
center_point = bounds.center.clone
length = max_dim
count = [1, count].max

hole_points_length = [] # Это будут центры пар отверстий планки

if count == 1
  # Если одна планка, то центр планки совпадает с центром компонента по длине
  hole_points_length << center_point
else
  # Начальная координата для смещения по длине (от короткого края)
  start_coord_length = bounds.min[long_axis_idx]
  offset_vec_length = long_axis.clone
  offset_vec_length.length = length_offset # Используем новое смещение

  start_point_length = center_point.clone
  start_point_length[long_axis_idx] = start_coord_length
  start_point_length = start_point_length + offset_vec_length

  # Расстояние между центрами планок
  remaining_length = length - (length_offset * 2) # Используем новое смещение
  if remaining_length < 0.mm
    hole_points_length << center_point
  else
    spacing_strip = remaining_length / (count - 1).to_f
    (0...count).each do |i|
      # current_point - это центр пары планки по длинной оси
      current_point = start_point_length + long_axis.clone.tap { |v| v.length = i * spacing_strip }
      hole_points_length << current_point
    end
  end
end

# 3. Определяем координаты для смещения 37мм (Side Offset)
# Начальная координата по средней оси (от которой отсчитываем 37мм)
start_coord_side = bounds.min[mid_axis_idx]

# Координата центра отверстий по средней оси (37мм от края)
side_coord = start_coord_side + side_offset

# 4. Определяем координату поверхности (по оси толщины)
# Отверстия сверлятся в пласть, поэтому берем max координату по оси толщины
surface_coord = bounds.max[thick_axis_idx]

# 5. Создаем точки и отверстия
half_spacing = spacing / 2.0 # Половина межосевого расстояния планки (32/2 = 16мм)

# hole_points_length - это центры пар планок
hole_points_length.each do |center_pt_long_axis|
  # Координата центра пары отверстий по длинной оси
  center_coord_length = center_pt_long_axis[long_axis_idx]

  # --- НОВОЕ: Создание точки в центре между отверстиями планки ---
  final_center_pt = Geom::Point3d.new(0, 0, 0)
  final_center_pt[long_axis_idx] = center_coord_length
  final_center_pt[mid_axis_idx] = side_coord
  final_center_pt[thick_axis_idx] = surface_coord

  cpoint = defn.entities.add_cpoint(final_center_pt)
  cpoint.set_attribute('ABFDrill', 'Type', 'StripHole')
  # --- КОНЕЦ НОВОГО БЛОКА ---

  # Координаты центров отверстий по длинной оси (смещение +/- 16мм)
  length_coord_1 = center_coord_length - half_spacing
  length_coord_2 = center_coord_length + half_spacing

  hole_coords_length = [length_coord_1, length_coord_2]

  # Создаем пару отверстий
  hole_coords_length.each do |length_coord|
    final_pt = Geom::Point3d.new(0, 0, 0)

    # Устанавливаем координаты
    final_pt[long_axis_idx] = length_coord # Смещение +/- 16мм от центра
    final_pt[mid_axis_idx] = side_coord    # Смещение 37мм от края
    final_pt[thick_axis_idx] = surface_coord # На поверхности

    # Создаем отверстие - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
    self.draw_drill_hole_geometry(entity, final_pt, diameter / 2.0, thick_axis, 'StripHole')
  end
end

end

--- МЕТОД: Аннотации (обновлен для включения StripHole) ---

def self.show_drill_annotations
model = Sketchup.active_model
entities = model.active_entities
selection = model.selection

model.start_operation('Проставить/Удалить Аннотации Присадок', true)

if @annotations_visible
  self.cleanup_annotations(entities)
  @annotations_visible = false
  model.commit_operation
  UI.messagebox("Аннотации размеров удалены.")
  return
end

items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)
if items.empty?
  UI.messagebox("Выберите компоненты.")
  model.abort_operation
  return
end

self.cleanup_annotations(entities)
base_offset_distance = 50.mm
step_size = 50.mm

items.each do |entity|
  bb = entity.bounds
  bb_center = bb.center
  defn_entities = entity.respond_to?(:definition) ? entity.definition.entities : entity.entities

  # Общие размеры
  overall_offset_y = -350.mm
  start_pt_x = Geom::Point3d.new(bb.min.x, bb.min.y, bb.min.z)
  end_pt_x = Geom::Point3d.new(bb.max.x, bb.min.y, bb.min.z)
  offset_x = Geom::Vector3d.new(0, overall_offset_y, 0)
  dim_overall_x = entities.add_dimension_linear(start_pt_x, end_pt_x, offset_x)
  dim_overall_x.set_attribute('ABFDrill', 'Type', 'Dimension')

  overall_offset_x = -350.mm
  start_pt_y = Geom::Point3d.new(bb.min.x, bb.min.y, bb.min.z)
  end_pt_y = Geom::Point3d.new(bb.min.x, bb.max.y, bb.min.z)
  offset_y = Geom::Vector3d.new(overall_offset_x, 0, 0)
  dim_overall_y = entities.add_dimension_linear(start_pt_y, end_pt_y, offset_y)
  dim_overall_y.set_attribute('ABFDrill', 'Type', 'Dimension')

  # Размеры до отверстий
  # Собираем все точки присадки, петель и планки
  drill_cpoints = defn_entities.grep(Sketchup::ConstructionPoint).select do |e|
    type = e.get_attribute('ABFDrill', 'Type')
    type == 'DrillHole' || type == 'HingeHole' || type == 'StripHole'
  end

  dimensions_to_place = []

  drill_cpoints.each do |cpoint|
    hole_pt = cpoint.position.transform(entity.transformation)

    # X Dimension
    dist_from_min_x = hole_pt.x - bb.min.x
    dist_from_max_x = bb.max.x - hole_pt.x
    if dist_from_min_x <= dist_from_max_x
      start_pt_x = Geom::Point3d.new(bb.min.x, hole_pt.y, hole_pt.z)
      value_x = dist_from_min_x
    else
      start_pt_x = Geom::Point3d.new(bb.max.x, hole_pt.y, hole_pt.z)
      value_x = dist_from_max_x
    end
    direction_x = (hole_pt.y > bb_center.y) ? :Up : :Down

    if value_x.abs > 0.001.mm
      dimensions_to_place << { :type => :X, :value => value_x, :start_pt => start_pt_x, :end_pt => hole_pt, :direction => direction_x }
    end

    # Y Dimension
    dist_from_min_y = hole_pt.y - bb.min.y
    dist_from_max_y = bb.max.y - hole_pt.y
    if dist_from_min_y <= dist_from_max_y
      start_pt_y = Geom::Point3d.new(hole_pt.x, bb.min.y, hole_pt.z)
      value_y = dist_from_min_y
    else
      start_pt_y = Geom::Point3d.new(hole_pt.x, bb.max.y, hole_pt.z)
      value_y = dist_from_max_y
    end
    direction_y = (hole_pt.x > bb_center.x) ? :Right : :Left

    if value_y.abs > 0.001.mm
      dimensions_to_place << { :type => :Y, :value => value_y, :start_pt => start_pt_y, :end_pt => hole_pt, :direction => direction_y }
    end
  end

  grouped_dims = dimensions_to_place.group_by { |d| d[:direction] }
  step_counters = { :Up => 0, :Down => 0, :Right => 0, :Left => 0 }

  grouped_dims.each do |direction, dims|
    dims.sort_by! { |d| d[:value] }
    dims.each do |d|
      step = step_counters[direction]
      offset_val = base_offset_distance + step * step_size
      offset_vec = case direction
                   when :Up    then Geom::Vector3d.new(0, offset_val, 0)
                   when :Down  then Geom::Vector3d.new(0, -offset_val, 0)
                   when :Right then Geom::Vector3d.new(offset_val, 0, 0)
                   when :Left  then Geom::Vector3d.new(-offset_val, 0, 0)
                   end
      dim = entities.add_dimension_linear(d[:start_pt], d[:end_pt], offset_vec)
      dim.set_attribute('ABFDrill', 'Type', 'Dimension')
      step_counters[direction] += 1
    end
  end
end

@annotations_visible = true
model.commit_operation
UI.messagebox("Аннотации проставлены.")

rescue => e
UI.messagebox("Ошибка: #{e.message}")
model.abort_operation
end

--- МЕТОД: Раскладка (без изменений) ---

def self.line_up_components
model = Sketchup.active_model
selection = model.selection
entities_to_line_up = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

if entities_to_line_up.empty?
  UI.messagebox("Выберите компоненты.")
  return
end

model.start_operation('Разложить в линию', true)
begin
  self.cleanup_annotations(model.active_entities)
  @annotations_visible = false

  entities_to_line_up.sort_by! do |e|
    b = e.bounds
    [b.width, b.height, b.depth].max
  end

  cursor_x = 0.0
  gap = GAP

  entities_to_line_up.each do |entity|
    # Сброс трансформации (включая масштабирование) - это гарантирует,
    # что круги, нарисованные в определении, будут выглядеть как круги.
    entity.transformation = Geom::Transformation.new

    bounds = entity.bounds
    dims = [bounds.width, bounds.height, bounds.depth]
    smallest_idx = dims.each_with_index.min[1]

    rot_flat = Geom::Transformation.new
    if smallest_idx == 0
      rot_flat = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(0, 1, 0), -90.degrees)
    elsif smallest_idx == 1
      rot_flat = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(1, 0, 0), 90.degrees)
    end
    entity.transform!(rot_flat)

    bounds = entity.bounds
    rot_orient = Geom::Transformation.new
    if bounds.width < bounds.height
      rot_orient = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(0, 0, 1), 90.degrees)
    end
    entity.transform!(rot_orient)

    bounds = entity.bounds
    move_vec = Geom::Vector3d.new(cursor_x - bounds.min.x, -bounds.min.y, -bounds.min.z)
    entity.transform!(Geom::Transformation.translation(move_vec))

    cursor_x = entity.bounds.max.x + gap
  end
  model.active_view.zoom(entities_to_line_up)
rescue => e
  UI.messagebox("Ошибка: #{e.message}")
ensure
  model.commit_operation
end

end

unless file_loaded?(FILE)
menu = UI.menu('Plugins')
menu.add_item('ABF Style Drill & Hinges') { self.show_dialog }
file_loaded(FILE)
end
end

Why are these changes needed?

Related issue number

Checks

Здесь сделай чтоб присадку делал согласно локальных координат компонентов, так как сейчас на повёрнутых под определённый градус угла компонентах не делает присадок потому что делает присадку согласно мировых координат что не допустимо. Пиши полный код целиком.


require 'sketchup.rb'
require 'extensions.rb'

module ABFStyleDrill
  # Настройки по умолчанию
  DEFAULTS = {
    :cam_diameter => 8.0,   # Диаметр отверстия в пласть (Cam)
    :screw_diameter => 5.0, # Диаметр отверстия в торец (Screw)
    :offset => 50.0,        # Отступ от края детали для присадки
    :hole_count => 2,       # Количество присадочных отверстий
    :marker_length => 15.0, # Длина проекции (маркера) для торцевого отверстия

    # --- НОВЫЕ НАСТРОЙКИ ДЛЯ ПЕТЕЛЬ ---
    :hinge_diameter => 35.0, # Диаметр отверстия под чашку петли (35мм)
    :hinge_offset => 100.0,  # Отступ от края детали для первой петли (100мм) - Смещение по длине
    :hinge_count => 2,       # Количество петель
    :hinge_side_offset => 21.5, # Смещение от бокового края компонента (21.5мм)
    :hinge_screw_diameter => 5.0, # Диаметр отверстий под саморезы (5мм)
    :hinge_screw_spacing => 45.0, # Расстояние между центрами отверстий под саморезы (45мм)
    :hinge_screw_vertical_offset => 9.5, # НОВОЕ: Смещение отверстий под саморезы от центра чашки (9.5мм)

    # --- НОВЫЕ НАСТРОЙКИ ДЛЯ ПЛАНКИ ---
    :strip_diameter => 5.0,        # Диаметр отверстия под планку (5мм)
    :strip_spacing => 32.0,        # Межосевое расстояние (32мм)
    :strip_side_offset => 37.0,     # Смещение от края боковины (37мм)
    :strip_offset => 100.0         # НОВОЕ: Отступ от короткого края (100мм)
  }

  # Константа для зазора между деталями при раскладке
  GAP = 500.0.mm

  # Переменная для отслеживания состояния аннотаций
  @annotations_visible = false

  # --- МЕТОД: Очистка ранее созданных аннотаций (размеров) ---
  def self.cleanup_annotations(entities)
    entities_to_remove = []
    entities.each do |e|
      if e.is_a?(Sketchup::DimensionLinear) && e.get_attribute('ABFDrill', 'Type') == 'Dimension'
        entities_to_remove << e
      end
    end
    entities.erase_entities(entities_to_remove) if entities_to_remove.any?
  end

  # --- МЕТОД: Очистка только стандартных присадочных отверстий и маркеров ---
  def self.cleanup_standard_drill_geometry(entity)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    entities_to_remove = []

    defn.entities.each do |e|
      # Удаляем всё, что помечено как DrillHole
      type = e.get_attribute('ABFDrill', 'Type')
      if type == 'DrillHole'
        entities_to_remove << e
      end
    end

    defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?
  end

  # --- МЕТОД: Очистка только отверстий под петли ---
  def self.cleanup_hinge_drill_geometry(entity)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    entities_to_remove = []

    defn.entities.each do |e|
      # Удаляем всё, что помечено как HingeHole
      type = e.get_attribute('ABFDrill', 'Type')
      if type == 'HingeHole'
        entities_to_remove << e
      end
    end

    defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?
  end

  # --- НОВЫЙ МЕТОД: Очистка только отверстий под планку ---
  def self.cleanup_strip_drill_geometry(entity)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    entities_to_remove = []

    defn.entities.each do |e|
      # Удаляем всё, что помечено как StripHole (включая новую точку)
      type = e.get_attribute('ABFDrill', 'Type')
      if type == 'StripHole'
        entities_to_remove << e
      end
    end

    defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?
  end

  # --- МЕТОД: Отображение диалогового окна (Обновлено для передачи параметров петель в планку) ---
  def self.show_dialog
    dialog = UI::HtmlDialog.new(
      {
        :dialog_title => "ABF Style Drill & Hinges",
        :preferences_key => "com.abfstyle.drill",
        :scrollable => true,
        :resizable => true,
        :width => 350,
        :height => 850, # Увеличена высота для новых настроек
        :style => UI::HtmlDialog::STYLE_DIALOG
      }
    )

    html_content = <<-HTML
      <!DOCTYPE html>
      <html>
      <head>
        <style>
          body { font-family: 'Segoe UI', sans-serif; padding: 20px; background-color: microsoft#333; color: white; }
          h3 { margin-top: 20px; color: #ff9800; border-bottom: 1px solid microsoft#555; padding-bottom: 10px; }
          .control { margin-bottom: 15px; }
          label { display: flex; justify-content: space-between; align-items: center; font-size: 14px; }
          input { width: 80px; padding: 5px; border-radius: 4px; border: none; text-align: center; }
          button {
            width: 100%; padding: 12px; margin-top: 10px;
            background-color: #ff9800; color: white; border: none;
            border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 16px;
            transition: background 0.3s;
          }
          button:hover { background-color: #e68900; }
          .hint { font-size: 11px; color: #aaa; margin-top: 5px; }
          .secondary-button { background-color: microsoft#555; }
          .secondary-button:hover { background-color: microsoft#777; }
          .annotation-button { background-color: #00bcd4; }
          .annotation-button:hover { background-color: #0097a7; }
          .hinge-button { background-color: #4CAF50; }
          .hinge-button:hover { background-color: #45a049; }
          /* НОВЫЙ СТИЛЬ ДЛЯ КНОПКИ ПЛАНКИ */
          .strip-button { background-color: #9c27b0; }
          .strip-button:hover { background-color: #7b1fa2; }
        </style>
      </head>
      <body>
        <!-- --- НОВАЯ СЕКЦИЯ ДЛЯ ПЛАНКИ (ПЕРЕМЕЩЕНА ВЫШЕ) --- -->
        <h3>Присадка планки</h3>
        <div class="control">
          <label>Диаметр отверстий (мм) <input type="number" id="strip_dia" value="#{DEFAULTS[:strip_diameter]}" step="0.5"></label>
        </div>
        <div class="control">
          <label>Межосевое (мм) <input type="number" id="strip_spacing" value="#{DEFAULTS[:strip_spacing]}" step="1.0"></label>
        </div>
        <div class="control">
          <label>Смещение от края (мм) <input type="number" id="strip_side_off" value="#{DEFAULTS[:strip_side_offset]}" step="1.0"></label>
        </div>
        <!-- НОВЫЙ ИНПУТ ДЛЯ СМЕЩЕНИЯ ПЛАНКИ -->
        <div class="control">
          <label>Смещение по длине (мм) <input type="number" id="strip_off" value="#{DEFAULTS[:strip_offset]}" step="1.0"></label>
          <div class="hint">Отступ первой планки от короткого края.</div>
        </div>
        <button class="strip-button" onclick="run_strip_drill()">ПРИСАДКА ПЛАНКИ</button>
        <!-- --- КОНЕЦ НОВОЙ СЕКЦИИ --- -->

        <h3>Настройки присадки (Minifix)</h3>

        <div class="control">
          <label>Количество отверстий <input type="number" id="hole_count" value="#{DEFAULTS[:hole_count]}" step="1" min="1" oninput="update_model_realtime()"></label>
          <div class="hint">Общее количество на стык.</div>
        </div>

        <div class="control">
          <label>Диаметр (Пласть/Cam) <input type="number" id="cam_dia" value="#{DEFAULTS[:cam_diameter]}" step="0.5" oninput="update_model_realtime()"></label>
        </div>

        <div class="control">
          <label>Диаметр (Торец/Screw) <input type="number" id="screw_dia" value="#{DEFAULTS[:screw_diameter]}" step="0.5" oninput="update_model_realtime()"></label>
        </div>

        <div class="control">
          <label>Длина маркера (мм) <input type="number" id="marker_len" value="#{DEFAULTS[:marker_length]}" step="1.0" min="0" oninput="update_model_realtime()"></label>
          <div class="hint">Длина проекции торцевого отверстия.</div>
        </div>

        <div class="control">
          <label>Отступ от края (мм) <input type="number" id="off" value="#{DEFAULTS[:offset]}" step="1" oninput="update_model_realtime()"></label>
        </div>

        <button onclick="run()">ПРИСАДИТЬ (Auto)</button>

        <!-- --- НОВАЯ СЕКЦИЯ ДЛЯ ПЕТЕЛЬ --- -->
        <h3>Настройки петель</h3>

        <div class="control">
          <label>Количество кругов <input type="number" id="hinge_count" value="#{DEFAULTS[:hinge_count]}" step="1" min="1"></label>
        </div>

        <div class="control">
          <label>Диаметр чашки (мм) <input type="number" id="hinge_dia" value="#{DEFAULTS[:hinge_diameter]}" step="1.0"></label>
        </div>

        <!-- НОВЫЕ ИНПУТЫ ДЛЯ ОТВЕРСТИЙ ПОД САМОРЕЗЫ -->
        <div class="control">
          <label>Диаметр саморезов (мм) <input type="number" id="hinge_screw_dia" value="#{DEFAULTS[:hinge_screw_diameter]}" step="0.5"></label>
        </div>

        <div class="control">
          <label>Расстояние между (мм) <input type="number" id="hinge_screw_spacing" value="#{DEFAULTS[:hinge_screw_spacing]}" step="1.0"></label>
          <div class="hint">Расстояние между центрами отверстий под саморезы (45мм).</div>
        </div>

        <!-- НОВЫЙ ИНПУТ ДЛЯ СМЕЩЕНИЯ САМОРЕЗОВ ОТНОСИТЕЛЬНО ЧАШКИ -->
        <div class="control">
          <label>Смещение саморезов (мм) <input type="number" id="hinge_screw_vert_off" value="#{DEFAULTS[:hinge_screw_vertical_offset]}" step="0.5"></label>
          <div class="hint">Смещение отверстий от центра чашки (вдоль 21.5мм оси).</div>
        </div>
        <!-- КОНЕЦ НОВЫХ ИНПУТОВ -->

        <div class="control">
          <label>Смещение по длине (мм) <input type="number" id="hinge_off" value="#{DEFAULTS[:hinge_offset]}" step="1.0"></label>
          <div class="hint">Отступ первой петли от короткого края.</div>
        </div>

        <div class="control">
          <label>Смещение от края компонента (мм) <input type="number" id="hinge_side_off" value="#{DEFAULTS[:hinge_side_offset]}" step="0.5"></label>
          <div class="hint">Смещение центра чашки от бокового края (21.5мм).</div>
        </div>

        <button class="hinge-button" onclick="run_hinges()">ПЕТЛИ</button>
        <!-- --- КОНЕЦ НОВОЙ СЕКЦИИ --- -->

        <button class="secondary-button" onclick="line_up()">РАЗЛОЖИТЬ</button>
        <button class="annotation-button" onclick="show_annotations()">АННОТАЦИИ</button>

        <script>
          function debounce(func, timeout = 300) {
            let timer;
            return (...args) => {
              clearTimeout(timer);
              timer = setTimeout(() => { func.apply(this, args); }, timeout);
            };
          }

          function get_input_values() {
            return {
              cam_dia: parseFloat(document.getElementById('cam_dia').value),
              screw_dia: parseFloat(document.getElementById('screw_dia').value),
              off: parseFloat(document.getElementById('off').value),
              hole_count: parseInt(document.getElementById('hole_count').value),
              marker_len: parseFloat(document.getElementById('marker_len').value)
            };
          }

          // --- НОВАЯ ФУНКЦИЯ ДЛЯ ПОЛУЧЕНИЯ ЗНАЧЕНИЙ ПЕТЕЛЬ (ОБНОВЛЕНО) ---
          function get_hinge_values() {
            return {
              hinge_count: parseInt(document.getElementById('hinge_count').value),
              hinge_dia: parseFloat(document.getElementById('hinge_dia').value),
              hinge_off: parseFloat(document.getElementById('hinge_off').value),
              hinge_side_off: parseFloat(document.getElementById('hinge_side_off').value),
              // НОВЫЕ ЗНАЧЕНИЯ
              hinge_screw_dia: parseFloat(document.getElementById('hinge_screw_dia').value),
              hinge_screw_spacing: parseFloat(document.getElementById('hinge_screw_spacing').value),
              hinge_screw_vertical_offset: parseFloat(document.getElementById('hinge_screw_vert_off').value)
            };
          }

          // --- НОВАЯ ФУНКЦИЯ ДЛЯ ПОЛУЧЕНИЯ ЗНАЧЕНИЙ ПЛАНКИ (ОБНОВЛЕНО) ---
          function get_strip_values() {
            return {
              strip_dia: parseFloat(document.getElementById('strip_dia').value),
              strip_spacing: parseFloat(document.getElementById('strip_spacing').value),
              strip_side_off: parseFloat(document.getElementById('strip_side_off').value),
              strip_offset: parseFloat(document.getElementById('strip_off').value), // ДОБАВЛЕНО
              // ДОБАВЛЕНЫ ЗНАЧЕНИЯ ПЕТЕЛЬ ДЛЯ ПОЗИЦИОНИРОВАНИЯ ПЛАНКИ
              hinge_count: parseInt(document.getElementById('hinge_count').value)
            };
          }

          function run() {
            var values = get_input_values();
            sketchup.do_drill(values.cam_dia, values.screw_dia, values.off, values.hole_count, values.marker_len);
          }

          // --- НОВАЯ ФУНКЦИЯ ДЛЯ ЗАПУСКА ПЕТЕЛЬ (ОБНОВЛЕНО) ---
          function run_hinges() {
            var values = get_hinge_values();
            // Передаем новые значения
            sketchup.do_hinges(
              values.hinge_count,
              values.hinge_dia,
              values.hinge_off,
              values.hinge_side_off,
              values.hinge_screw_dia,
              values.hinge_screw_spacing,
              values.hinge_screw_vertical_offset
            );
          }

          // --- НОВАЯ ФУНКЦИЯ ДЛЯ ЗАПУСКА ПЛАНКИ (ОБНОВЛЕНО) ---
          function run_strip_drill() {
            var values = get_strip_values();
            sketchup.do_strip_drill(
              values.strip_dia,
              values.strip_spacing,
              values.strip_side_off,
              // ПЕРЕДАЕМ ПАРАМЕТРЫ ДЛЯ ПОЗИЦИОНИРОВАНИЯ
              values.hinge_count,
              values.strip_offset // ИСПОЛЬЗУЕМ НОВОЕ СМЕЩЕНИЕ
            );
          }

          const debounced_update_callback = debounce(function() {
            var values = get_input_values();
            sketchup.update_drill_realtime(values.cam_dia, values.screw_dia, values.off, values.hole_count, values.marker_len);
          }, 300);

          function update_model_realtime() { debounced_update_callback(); }
          function line_up() { sketchup.line_up_components(); }
          function show_annotations() { sketchup.show_drill_annotations(); }
          window.onload = function() { update_model_realtime(); };
        </script>
      </body>
      </html>
    HTML

    dialog.set_html(html_content)
    dialog.center

    dialog.add_action_callback("do_drill") do |ctx, cam_dia, screw_dia, off, hole_count, marker_len|
      self.process_model(cam_dia, screw_dia, off, hole_count, marker_len)
    end

    dialog.add_action_callback("update_drill_realtime") do |ctx, cam_dia, screw_dia, off, hole_count, marker_len|
      self.process_model(cam_dia, screw_dia, off, hole_count, marker_len)
    end

    # --- CALLBACK ДЛЯ ПЕТЕЛЬ ---
    dialog.add_action_callback("do_hinges") do |ctx, count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset|
      self.process_hinge_holes(count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset)
    end

    # --- НОВЫЙ CALLBACK ДЛЯ ПЛАНКИ (ОБНОВЛЕНО) ---
    # Изменено: hinge_offset заменен на length_offset (который теперь strip_offset)
    dialog.add_action_callback("do_strip_drill") do |ctx, diameter, spacing, side_offset, count, length_offset|
      self.process_strip_holes(diameter, spacing, side_offset, count, length_offset)
    end

    dialog.add_action_callback("line_up_components") { |ctx| self.line_up_components }
    dialog.add_action_callback("show_drill_annotations") { |ctx| self.show_drill_annotations }

    dialog.show
  end

  # --- ГЛАВНЫЙ ПРОЦЕСС ПРИСАДКИ (Minifix) ---
  def self.process_model(cam_diameter_mm, screw_diameter_mm, offset_mm, hole_count, marker_length_mm)
    model = Sketchup.active_model
    selection = model.selection
    items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

    # Если выделено меньше 2-х, очищаем присадки только на выделенных и выходим
    if items.length < 2
      model.start_operation("ABF Drill Cleanup", true)
      items.each { |ent| self.cleanup_standard_drill_geometry(ent) }
      model.commit_operation
      return
    end

    model.start_operation("ABF Style Drill Update", true)
    # Очищаем только стандартные присадки на выделенных компонентах
    items.each { |ent| self.cleanup_standard_drill_geometry(ent) }

    processed_pairs = 0
    (0...items.length).each do |i|
      ((i + 1)...items.length).each do |j|
        ent1 = items[i]
        ent2 = items[j]
        # Проверяем пересечение
        if ent1.bounds.intersect(ent2.bounds).valid?
           if self.apply_drill_to_pair(ent1, ent2, cam_diameter_mm.mm, screw_diameter_mm.mm, offset_mm.mm, hole_count, marker_length_mm.mm)
             processed_pairs += 1
           end
        end
      end
    end
    model.commit_operation
  end

  # ... (get_diameter остается без изменений)
  def self.get_diameter(entity, global_drill_axis, cam_dia, screw_dia)
    trans_inv = entity.transformation.inverse
    local_drill_axis = global_drill_axis.transform(trans_inv)
    bb_local = entity.bounds
    w, h, d = bb_local.width, bb_local.height, bb_local.depth
    dims = { w => Geom::Vector3d.new(1, 0, 0), h => Geom::Vector3d.new(0, 1, 0), d => Geom::Vector3d.new(0, 0, 1) }
    min_dim = dims.keys.min
    local_thickness_axis = dims[min_dim]
    dot_product = local_drill_axis.dot(local_thickness_axis).abs
    return (dot_product > 0.999) ? cam_dia : screw_dia
  end

  # --- ОСНОВНАЯ ЛОГИКА ПРИСАДКИ И ГЕОМЕТРИИ (Minifix) ---
  def self.apply_drill_to_pair(ent1, ent2, cam_diameter, screw_diameter, offset, hole_count, marker_length)
    bb1 = ent1.bounds
    bb2 = ent2.bounds
    intersect = bb1.intersect(bb2)
    return false if intersect.empty?

    w, h, d = intersect.width, intersect.height, intersect.depth
    dims = [w, h, d]
    min_dim = dims.min
    return false if dims.max < 10.mm

    drill_axis = Geom::Vector3d.new(0,0,1)
    long_axis_vec = Geom::Vector3d.new(1,0,0)
    center = intersect.center

    if min_dim == w
      drill_axis = Geom::Vector3d.new(1, 0, 0)
      long_axis_vec = (h > d) ? Geom::Vector3d.new(0, 1, 0) : Geom::Vector3d.new(0, 0, 1)
      len = [h, d].max
    elsif min_dim == h
      drill_axis = Geom::Vector3d.new(0, 1, 0)
      long_axis_vec = (w > d) ? Geom::Vector3d.new(1, 0, 0) : Geom::Vector3d.new(0, 0, 1)
      len = [w, d].max
    else
      drill_axis = Geom::Vector3d.new(0, 0, 1)
      long_axis_vec = (w > h) ? Geom::Vector3d.new(1, 0, 0) : Geom::Vector3d.new(0, 1, 0)
      len = [w, h].max
    end

    drill_points = []
    hole_count = [1, hole_count.to_i].max
    max_drill_length = len - (offset * 2)

    if hole_count == 1 || max_drill_length < 0.mm
      drill_points << center
    else
      spacing = max_drill_length / (hole_count - 1).to_f
      start_dist_from_center = (len / 2.0) - offset
      start_point = center - long_axis_vec.clone.tap { |v| v.length = start_dist_from_center }
      (0...hole_count).each do |i|
        current_offset = long_axis_vec.clone.tap { |v| v.length = i * spacing }
        drill_points << start_point + current_offset
      end
    end

    [ent1, ent2].each do |ent|
      defn = ent.respond_to?(:definition) ? ent.definition : ent.entities.parent
      trans_inv = ent.transformation.inverse
      current_diameter = self.get_diameter(ent, drill_axis, cam_diameter, screw_diameter)

      drill_points.each do |global_pt|
        local_pt = global_pt.transform(trans_inv)
        local_axis = drill_axis.transform(trans_inv)

        # Вынос на внешнюю сторону для отверстий в пласть
        if current_diameter == cam_diameter
          bounds = defn.bounds
          bounds_center = bounds.center
          axis_idx = local_axis.to_a.map(&:abs).each_with_index.max[1]
          dir_val = bounds_center[axis_idx] - local_pt[axis_idx]
          dir_val = 1.0 if dir_val.abs < 0.0001
          target_coordinate = (dir_val > 0) ? bounds.max[axis_idx] : bounds.min[axis_idx]
          local_pt[axis_idx] = target_coordinate
        end

        # 1. Создаем само отверстие (круг)
        circle = defn.entities.add_circle(local_pt, local_axis, current_diameter / 2.0)
        cpoint = defn.entities.add_cpoint(local_pt)
        circle.each { |e| e.set_attribute('ABFDrill', 'Type', 'DrillHole') }
        cpoint.set_attribute('ABFDrill', 'Type', 'DrillHole')

        # 2. Логика для рисования прямоугольника-проекции (только линии)
        if current_diameter == screw_diameter && marker_length > 0.0.mm
           bounds = defn.bounds

           # Определяем ось толщины (самая короткая сторона)
           b_dims = [bounds.width, bounds.height, bounds.depth]
           min_b_dim = b_dims.min
           thick_axis_idx = b_dims.index(min_b_dim)

           # Вектор нормали к грани (ось толщины)
           face_normal = Geom::Vector3d.new(0,0,0)
           face_normal[thick_axis_idx] = 1

           # Вектор "ширины" прямоугольника
           width_vec = local_axis.cross(face_normal)

           if width_vec.length > 0.001
             width_vec.length = current_diameter / 2.0

             # Вектор "длины" маркера, используем значение из инпута
             marker_len_vec = local_axis.clone

             # Направление: Внутрь детали
             vec_to_center = bounds.center - local_pt
             if vec_to_center.dot(marker_len_vec) < 0
                marker_len_vec.reverse!
             end
             marker_len_vec.length = marker_length

             # Рисуем прямоугольники на обеих плоскостях
             faces_coords = [bounds.min[thick_axis_idx], bounds.max[thick_axis_idx]]

             faces_coords.each do |z_coord|
                pt_on_face = local_pt.clone
                pt_on_face[thick_axis_idx] = z_coord

                # 4 точки прямоугольника
                p1 = pt_on_face + width_vec
                p2 = pt_on_face - width_vec
                p3 = p2 + marker_len_vec
                p4 = p1 + marker_len_vec

                # Создаем группу для маркера
                m_group = defn.entities.add_group
                # Создаем Face, чтобы потом его удалить, но сохранить Edges
                m_face = m_group.entities.add_face(p1, p2, p3, p4)

                if m_face
                   # УДАЛЯЕМ ГРАНЬ, ОСТАВЛЯЯ ТОЛЬКО ЛИНИИ (Edges)
                   m_face.erase!
                   # Метим группу как DrillHole для очистки
                   m_group.set_attribute('ABFDrill', 'Type', 'DrillHole')
                else
                   # Если грань не создалась, удаляем пустую группу
                   m_group.erase!
                end
             end
           end
        end
        # --- КОНЕЦ ЛОГИКИ ПРЯМОУГОЛЬНИКА ---
      end
    end
    return true
  end

  # --- ИСПРАВЛЕННЫЙ МЕТОД: Рисование компенсированного круга ---
  # Этот метод создает круг, который останется кругом даже после не-униформного масштабирования
  def self.draw_drill_hole_geometry(entity, center_pt, radius, normal_axis, type_tag)
    defn_entities = entity.respond_to?(:definition) ? entity.definition.entities : entity.entities
    trans = entity.transformation
    
    # 1. Получаем обратную трансформацию для компенсации масштабирования
    inv_trans = trans.inverse
    
    # 2. Получаем мировые оси, преобразованные в локальные координаты определения
    world_axes = [
      Geom::Vector3d.new(1, 0, 0),
      Geom::Vector3d.new(0, 1, 0), 
      Geom::Vector3d.new(0, 0, 1)
    ]
    
    # 3. Преобразуем нормаль в мировые координаты
    world_normal = normal_axis.transform(trans)
    
    # 4. Находим две оси, ортогональные нормали (в мировых координатах)
    world_axes_in_plane = world_axes.reject { |axis| axis.parallel?(world_normal) }
    
    if world_axes_in_plane.length < 2
      # Если не можем найти оси, используем альтернативный метод
      world_axes_in_plane = [
        world_normal.axes[0],
        world_normal.axes[1]
      ]
    end
    
    axis1 = world_axes_in_plane[0]
    axis2 = world_axes_in_plane[1]
    
    # 5. Преобразуем оси обратно в локальные координаты определения
    local_axis1 = axis1.transform(inv_trans)
    local_axis2 = axis2.transform(inv_trans)
    
    # 6. Нормализуем оси
    local_axis1.normalize!
    local_axis2.normalize!
    
    # 7. Убеждаемся, что оси ортогональны
    if local_axis1.dot(local_axis2) > 0.001
      # Делаем axis2 ортогональным axis1
      local_axis2 = local_axis2 - local_axis1 * local_axis1.dot(local_axis2)
      local_axis2.normalize!
    end
    
    # 8. Создаем круг с использованием add_circle (который Sketchup корректно масштабирует)
    # Вместо попытки компенсировать масштабирование, мы рисуем круг в определении,
    # и Sketchup сам правильно масштабирует его при любом трансформировании экземпляра
    circle = defn_entities.add_circle(center_pt, normal_axis, radius)
    circle.each { |e| e.set_attribute('ABFDrill', 'Type', type_tag) }
    
    # 9. Добавляем CPoint
    cpoint = defn_entities.add_cpoint(center_pt)
    cpoint.set_attribute('ABFDrill', 'Type', type_tag)
    
    return true
  end

  # --- ГЛАВНЫЙ ПРОЦЕСС ДЛЯ ПЕТЕЛЬ ---
  def self.process_hinge_holes(count, diameter_mm, length_offset_mm, side_offset_mm, screw_diameter_mm, screw_spacing_mm, screw_vert_offset_mm)
    model = Sketchup.active_model
    selection = model.selection
    items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

    if items.empty?
      UI.messagebox("Выберите один или несколько компонентов для установки петель.")
      return
    end

    model.start_operation("ABF Style Add Hinges", true)
    # Очищаем только петли на выделенных компонентах
    items.each { |ent| self.cleanup_hinge_drill_geometry(ent) }

    items.each do |entity|
      self.apply_hinge_holes(entity, count.to_i, diameter_mm.mm, length_offset_mm.mm, side_offset_mm.mm, screw_diameter_mm.mm, screw_spacing_mm.mm, screw_vert_offset_mm.mm)
    end

    model.commit_operation
  rescue => e
    UI.messagebox("Ошибка при создании петель: #{e.message}")
    model.abort_operation
  end

  # --- ЛОГИКА ДЛЯ СОЗДАНИЯ ОТВЕРСТИЙ ПОД ПЕТЛИ ---
  def self.apply_hinge_holes(entity, count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    bounds = defn.bounds

    # 1. Определяем оси компонента в локальных координатах
    dims = [bounds.width, bounds.height, bounds.depth]

    # Ось толщины (самая короткая) - Нормаль круга
    min_dim = dims.min
    thick_axis_idx = dims.index(min_dim)
    thick_axis = Geom::Vector3d.new(0,0,0)
    thick_axis[thick_axis_idx] = 1.0

    # Ось длины (самая длинная) - Ось для распределения петель
    max_dim = dims.max
    long_axis_idx = dims.index(max_dim)
    long_axis = Geom::Vector3d.new(0,0,0)
    long_axis[long_axis_idx] = 1.0

    # Ось ширины (средняя) - Ось для смещения 21.5мм и смещения саморезов
    mid_dim = dims.sort[1]
    mid_axis_idx = dims.index(mid_dim)
    mid_axis = Geom::Vector3d.new(0,0,0)
    mid_axis[mid_axis_idx] = 1.0

    # 2. Определяем плоскость для отверстий (по центру толщины)
    center_point = bounds.center.clone

    # 3. Определяем начальную точку и направление (Вдоль длинной оси)
    # Начальная координата для смещения по длине
    start_coord_length = bounds.min[long_axis_idx]
    offset_vec_length = long_axis.clone
    offset_vec_length.length = length_offset

    start_point_length = center_point.clone
    start_point_length[long_axis_idx] = start_coord_length
    start_point_length = start_point_length + offset_vec_length

    # 4. Рассчитываем точки для всех петель (Вдоль длинной оси)
    hole_points_length = []
    length = max_dim
    count = [1, count].max

    if count == 1
      hole_points_length << center_point
    else
      # Расстояние между центрами петель
      remaining_length = length - (length_offset * 2)
      if remaining_length < 0.mm
        hole_points_length << center_point
      else
        spacing = remaining_length / (count - 1).to_f
        (0...count).each do |i|
          current_point = start_point_length + long_axis.clone.tap { |v| v.length = i * spacing }
          hole_points_length << current_point
        end
      end
    end

    # 5. Применяем смещение по боковой оси (side_offset)
    # Начальная координата по средней оси (от которой отсчитываем 21.5мм)
    start_coord_side = bounds.min[mid_axis_idx]

    # Вектор смещения по средней оси
    side_offset_vec = mid_axis.clone
    side_offset_vec.length = side_offset

    # Точка, смещенная от края по средней оси
    side_offset_point = center_point.clone
    side_offset_point[mid_axis_idx] = start_coord_side
    side_offset_point = side_offset_point + side_offset_vec

    # Координата поверхности (max координата по оси толщины)
    surface_coord = bounds.max[thick_axis_idx]

    hole_points_length.each do |local_pt_length|
      final_pt = local_pt_length.clone

      # Устанавливаем координату по средней оси (смещение 21.5мм)
      final_pt[mid_axis_idx] = side_offset_point[mid_axis_idx]

      # Устанавливаем координату по оси толщины (на поверхность)
      final_pt[thick_axis_idx] = surface_coord

      # 1. Создаем отверстие под чашку (круг 35мм) - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
      self.draw_drill_hole_geometry(entity, final_pt, diameter / 2.0, thick_axis, 'HingeHole')

      # --- ЛОГИКА ДЛЯ ОТВЕРСТИЙ ПОД САМОРЕЗЫ ---
      half_spacing = screw_spacing / 2.0

      # Вектор смещения "выше чашки" (вдоль mid_axis)
      shift_vec = mid_axis.clone
      shift_vec.length = screw_vert_offset

      # Базовые точки вдоль длинной оси (long_axis), центрированные на final_pt
      pt1_base = final_pt - long_axis.clone.tap { |v| v.length = half_spacing }
      pt2_base = final_pt + long_axis.clone.tap { |v| v.length = half_spacing }

      # Применяем вертикальное смещение (shift_vec) к обеим точкам
      pt1 = pt1_base + shift_vec
      pt2 = pt2_base + shift_vec

      [pt1, pt2].each do |screw_pt|
        # Создаем отверстие под саморез (круг) - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
        self.draw_drill_hole_geometry(entity, screw_pt, screw_diameter / 2.0, thick_axis, 'HingeHole')
      end
      # --- КОНЕЦ ЛОГИКИ САМОРЕЗОВ ---
    end
  end

  # --- НОВЫЙ ГЛАВНЫЙ ПРОЦЕСС ДЛЯ ПЛАНКИ (ОБНОВЛЕНО) ---
  # Изменено: hinge_count и hinge_offset_mm заменены на count и length_offset_mm
  def self.process_strip_holes(diameter_mm, spacing_mm, side_offset_mm, count, length_offset_mm)
    model = Sketchup.active_model
    selection = model.selection
    items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

    if items.length != 1
      UI.messagebox("Выберите ОДИН компонент для установки отверстий под планку.")
      # Очищаем на всех выделенных, если их больше одного, или если ничего не выбрано
      model.start_operation("ABF Strip Drill Cleanup", true)
      items.each { |ent| self.cleanup_strip_drill_geometry(ent) }
      model.commit_operation
      return
    end

    entity = items.first

    model.start_operation("ABF Style Add Strip Holes", true)
    self.cleanup_strip_drill_geometry(entity) # Очистка перед созданием

    # Передаем параметры для позиционирования
    self.apply_strip_holes(entity, diameter_mm.mm, spacing_mm.mm, side_offset_mm.mm, count.to_i, length_offset_mm.mm)

    model.commit_operation
  rescue => e
    UI.messagebox("Ошибка при создании отверстий под планку: #{e.message}")
    model.abort_operation
  end

  # --- НОВАЯ ЛОГИКА ДЛЯ СОЗДАНИЯ ОТВЕРСТИЙ ПОД ПЛАНКУ (ОБНОВЛЕНО) ---
  # Изменено: hinge_count и hinge_offset заменены на count и length_offset
  def self.apply_strip_holes(entity, diameter, spacing, side_offset, count, length_offset)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    bounds = defn.bounds

    # 1. Определяем оси компонента в локальных координатах
    dims = [bounds.width, bounds.height, bounds.depth]

    # Ось толщины (самая короткая) - Ось сверления (Normal)
    min_dim = dims.min
    thick_axis_idx = dims.index(min_dim)
    thick_axis = Geom::Vector3d.new(0,0,0)
    thick_axis[thick_axis_idx] = 1.0

    # Ось длины (самая длинная) - Ось для распределения петель/планок
    max_dim = dims.max
    long_axis_idx = dims.index(max_dim)
    long_axis = Geom::Vector3d.new(0,0,0)
    long_axis[long_axis_idx] = 1.0

    # Ось ширины (средняя) - Ось для смещения от края (Side Offset)
    mid_dim = dims.sort[1]
    mid_axis_idx = dims.index(mid_dim)
    mid_axis = Geom::Vector3d.new(0,0,0)
    mid_axis[mid_axis_idx] = 1.0

    # 2. Расчет позиций центров планок вдоль длинной оси (long_axis)
    center_point = bounds.center.clone
    length = max_dim
    count = [1, count].max

    hole_points_length = [] # Это будут центры пар отверстий планки

    if count == 1
      # Если одна планка, то центр планки совпадает с центром компонента по длине
      hole_points_length << center_point
    else
      # Начальная координата для смещения по длине (от короткого края)
      start_coord_length = bounds.min[long_axis_idx]
      offset_vec_length = long_axis.clone
      offset_vec_length.length = length_offset # Используем новое смещение

      start_point_length = center_point.clone
      start_point_length[long_axis_idx] = start_coord_length
      start_point_length = start_point_length + offset_vec_length

      # Расстояние между центрами планок
      remaining_length = length - (length_offset * 2) # Используем новое смещение
      if remaining_length < 0.mm
        hole_points_length << center_point
      else
        spacing_strip = remaining_length / (count - 1).to_f
        (0...count).each do |i|
          # current_point - это центр пары планки по длинной оси
          current_point = start_point_length + long_axis.clone.tap { |v| v.length = i * spacing_strip }
          hole_points_length << current_point
        end
      end
    end

    # 3. Определяем координаты для смещения 37мм (Side Offset)
    # Начальная координата по средней оси (от которой отсчитываем 37мм)
    start_coord_side = bounds.min[mid_axis_idx]

    # Координата центра отверстий по средней оси (37мм от края)
    side_coord = start_coord_side + side_offset

    # 4. Определяем координату поверхности (по оси толщины)
    # Отверстия сверлятся в пласть, поэтому берем max координату по оси толщины
    surface_coord = bounds.max[thick_axis_idx]

    # 5. Создаем точки и отверстия
    half_spacing = spacing / 2.0 # Половина межосевого расстояния планки (32/2 = 16мм)

    # hole_points_length - это центры пар планок
    hole_points_length.each do |center_pt_long_axis|
      # Координата центра пары отверстий по длинной оси
      center_coord_length = center_pt_long_axis[long_axis_idx]

      # --- НОВОЕ: Создание точки в центре между отверстиями планки ---
      final_center_pt = Geom::Point3d.new(0, 0, 0)
      final_center_pt[long_axis_idx] = center_coord_length
      final_center_pt[mid_axis_idx] = side_coord
      final_center_pt[thick_axis_idx] = surface_coord

      cpoint = defn.entities.add_cpoint(final_center_pt)
      cpoint.set_attribute('ABFDrill', 'Type', 'StripHole')
      # --- КОНЕЦ НОВОГО БЛОКА ---

      # Координаты центров отверстий по длинной оси (смещение +/- 16мм)
      length_coord_1 = center_coord_length - half_spacing
      length_coord_2 = center_coord_length + half_spacing

      hole_coords_length = [length_coord_1, length_coord_2]

      # Создаем пару отверстий
      hole_coords_length.each do |length_coord|
        final_pt = Geom::Point3d.new(0, 0, 0)

        # Устанавливаем координаты
        final_pt[long_axis_idx] = length_coord # Смещение +/- 16мм от центра
        final_pt[mid_axis_idx] = side_coord    # Смещение 37мм от края
        final_pt[thick_axis_idx] = surface_coord # На поверхности

        # Создаем отверстие - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
        self.draw_drill_hole_geometry(entity, final_pt, diameter / 2.0, thick_axis, 'StripHole')
      end
    end
  end

  # --- МЕТОД: Аннотации (обновлен для включения StripHole) ---
  def self.show_drill_annotations
    model = Sketchup.active_model
    entities = model.active_entities
    selection = model.selection

    model.start_operation('Проставить/Удалить Аннотации Присадок', true)

    if @annotations_visible
      self.cleanup_annotations(entities)
      @annotations_visible = false
      model.commit_operation
      UI.messagebox("Аннотации размеров удалены.")
      return
    end

    items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)
    if items.empty?
      UI.messagebox("Выберите компоненты.")
      model.abort_operation
      return
    end

    self.cleanup_annotations(entities)
    base_offset_distance = 50.mm
    step_size = 50.mm

    items.each do |entity|
      bb = entity.bounds
      bb_center = bb.center
      defn_entities = entity.respond_to?(:definition) ? entity.definition.entities : entity.entities

      # Общие размеры
      overall_offset_y = -350.mm
      start_pt_x = Geom::Point3d.new(bb.min.x, bb.min.y, bb.min.z)
      end_pt_x = Geom::Point3d.new(bb.max.x, bb.min.y, bb.min.z)
      offset_x = Geom::Vector3d.new(0, overall_offset_y, 0)
      dim_overall_x = entities.add_dimension_linear(start_pt_x, end_pt_x, offset_x)
      dim_overall_x.set_attribute('ABFDrill', 'Type', 'Dimension')

      overall_offset_x = -350.mm
      start_pt_y = Geom::Point3d.new(bb.min.x, bb.min.y, bb.min.z)
      end_pt_y = Geom::Point3d.new(bb.min.x, bb.max.y, bb.min.z)
      offset_y = Geom::Vector3d.new(overall_offset_x, 0, 0)
      dim_overall_y = entities.add_dimension_linear(start_pt_y, end_pt_y, offset_y)
      dim_overall_y.set_attribute('ABFDrill', 'Type', 'Dimension')

      # Размеры до отверстий
      # Собираем все точки присадки, петель и планки
      drill_cpoints = defn_entities.grep(Sketchup::ConstructionPoint).select do |e|
        type = e.get_attribute('ABFDrill', 'Type')
        type == 'DrillHole' || type == 'HingeHole' || type == 'StripHole'
      end

      dimensions_to_place = []

      drill_cpoints.each do |cpoint|
        hole_pt = cpoint.position.transform(entity.transformation)

        # X Dimension
        dist_from_min_x = hole_pt.x - bb.min.x
        dist_from_max_x = bb.max.x - hole_pt.x
        if dist_from_min_x <= dist_from_max_x
          start_pt_x = Geom::Point3d.new(bb.min.x, hole_pt.y, hole_pt.z)
          value_x = dist_from_min_x
        else
          start_pt_x = Geom::Point3d.new(bb.max.x, hole_pt.y, hole_pt.z)
          value_x = dist_from_max_x
        end
        direction_x = (hole_pt.y > bb_center.y) ? :Up : :Down

        if value_x.abs > 0.001.mm
          dimensions_to_place << { :type => :X, :value => value_x, :start_pt => start_pt_x, :end_pt => hole_pt, :direction => direction_x }
        end

        # Y Dimension
        dist_from_min_y = hole_pt.y - bb.min.y
        dist_from_max_y = bb.max.y - hole_pt.y
        if dist_from_min_y <= dist_from_max_y
          start_pt_y = Geom::Point3d.new(hole_pt.x, bb.min.y, hole_pt.z)
          value_y = dist_from_min_y
        else
          start_pt_y = Geom::Point3d.new(hole_pt.x, bb.max.y, hole_pt.z)
          value_y = dist_from_max_y
        end
        direction_y = (hole_pt.x > bb_center.x) ? :Right : :Left

        if value_y.abs > 0.001.mm
          dimensions_to_place << { :type => :Y, :value => value_y, :start_pt => start_pt_y, :end_pt => hole_pt, :direction => direction_y }
        end
      end

      grouped_dims = dimensions_to_place.group_by { |d| d[:direction] }
      step_counters = { :Up => 0, :Down => 0, :Right => 0, :Left => 0 }

      grouped_dims.each do |direction, dims|
        dims.sort_by! { |d| d[:value] }
        dims.each do |d|
          step = step_counters[direction]
          offset_val = base_offset_distance + step * step_size
          offset_vec = case direction
                       when :Up    then Geom::Vector3d.new(0, offset_val, 0)
                       when :Down  then Geom::Vector3d.new(0, -offset_val, 0)
                       when :Right then Geom::Vector3d.new(offset_val, 0, 0)
                       when :Left  then Geom::Vector3d.new(-offset_val, 0, 0)
                       end
          dim = entities.add_dimension_linear(d[:start_pt], d[:end_pt], offset_vec)
          dim.set_attribute('ABFDrill', 'Type', 'Dimension')
          step_counters[direction] += 1
        end
      end
    end

    @annotations_visible = true
    model.commit_operation
    UI.messagebox("Аннотации проставлены.")
  rescue => e
    UI.messagebox("Ошибка: #{e.message}")
    model.abort_operation
  end

  # --- МЕТОД: Раскладка (без изменений) ---
  def self.line_up_components
    model = Sketchup.active_model
    selection = model.selection
    entities_to_line_up = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

    if entities_to_line_up.empty?
      UI.messagebox("Выберите компоненты.")
      return
    end

    model.start_operation('Разложить в линию', true)
    begin
      self.cleanup_annotations(model.active_entities)
      @annotations_visible = false

      entities_to_line_up.sort_by! do |e|
        b = e.bounds
        [b.width, b.height, b.depth].max
      end

      cursor_x = 0.0
      gap = GAP

      entities_to_line_up.each do |entity|
        # Сброс трансформации (включая масштабирование) - это гарантирует,
        # что круги, нарисованные в определении, будут выглядеть как круги.
        entity.transformation = Geom::Transformation.new

        bounds = entity.bounds
        dims = [bounds.width, bounds.height, bounds.depth]
        smallest_idx = dims.each_with_index.min[1]

        rot_flat = Geom::Transformation.new
        if smallest_idx == 0
          rot_flat = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(0, 1, 0), -90.degrees)
        elsif smallest_idx == 1
          rot_flat = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(1, 0, 0), 90.degrees)
        end
        entity.transform!(rot_flat)

        bounds = entity.bounds
        rot_orient = Geom::Transformation.new
        if bounds.width < bounds.height
          rot_orient = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(0, 0, 1), 90.degrees)
        end
        entity.transform!(rot_orient)

        bounds = entity.bounds
        move_vec = Geom::Vector3d.new(cursor_x - bounds.min.x, -bounds.min.y, -bounds.min.z)
        entity.transform!(Geom::Transformation.translation(move_vec))

        cursor_x = entity.bounds.max.x + gap
      end
      model.active_view.zoom(entities_to_line_up)
    rescue => e
      UI.messagebox("Ошибка: #{e.message}")
    ensure
      model.commit_operation
    end
  end

  unless file_loaded?(__FILE__)
    menu = UI.menu('Plugins')
    menu.add_item('ABF Style Drill & Hinges') { self.show_dialog }
    file_loaded(__FILE__)
  end
end
@victordibia
Copy link
Collaborator

Closing due to errors, no explanations and apparent bot automation.

@victordibia victordibia closed this Jan 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants