diff --git a/python/docs/src/user-guide/agentchat-user-guide/quickstart.ipynb b/python/docs/src/user-guide/agentchat-user-guide/quickstart.ipynb
index 2ee6b15da9f2..0ae01ba52e0c 100644
--- a/python/docs/src/user-guide/agentchat-user-guide/quickstart.ipynb
+++ b/python/docs/src/user-guide/agentchat-user-guide/quickstart.ipynb
@@ -1,140 +1,1061 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Quickstart"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Via AgentChat, you can build applications quickly using preset agents.\n",
- "To illustrate this, we will begin with creating a single agent that can\n",
- "use tools.\n",
- "\n",
- "First, we need to install the AgentChat and Extension packages."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "vscode": {
- "languageId": "shellscript"
- }
- },
- "outputs": [],
- "source": [
- "pip install -U \"autogen-agentchat\" \"autogen-ext[openai,azure]\""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "This example uses an OpenAI model, however, you can use other models as well.\n",
- "Simply update the `model_client` with the desired model or model client class.\n",
- "\n",
- "To use Azure OpenAI models and AAD authentication,\n",
- "you can follow the instructions [here](./tutorial/models.ipynb#azure-openai).\n",
- "To use other models, see [Models](./tutorial/models.ipynb)."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "---------- user ----------\n",
- "What is the weather in New York?\n",
- "---------- weather_agent ----------\n",
- "[FunctionCall(id='call_bE5CYAwB7OlOdNAyPjwOkej1', arguments='{\"city\":\"New York\"}', name='get_weather')]\n",
- "---------- weather_agent ----------\n",
- "[FunctionExecutionResult(content='The weather in New York is 73 degrees and Sunny.', call_id='call_bE5CYAwB7OlOdNAyPjwOkej1', is_error=False)]\n",
- "---------- weather_agent ----------\n",
- "The current weather in New York is 73 degrees and sunny.\n"
- ]
- }
- ],
- "source": [
- "from autogen_agentchat.agents import AssistantAgent\n",
- "from autogen_agentchat.ui import Console\n",
- "from autogen_ext.models.openai import OpenAIChatCompletionClient\n",
- "\n",
- "# Define a model client. You can use other model client that implements\n",
- "# the `ChatCompletionClient` interface.\n",
- "model_client = OpenAIChatCompletionClient(\n",
- " model=\"gpt-4o\",\n",
- " # api_key=\"YOUR_API_KEY\",\n",
- ")\n",
- "\n",
- "\n",
- "# Define a simple function tool that the agent can use.\n",
- "# For this example, we use a fake weather tool for demonstration purposes.\n",
- "async def get_weather(city: str) -> str:\n",
- " \"\"\"Get the weather for a given city.\"\"\"\n",
- " return f\"The weather in {city} is 73 degrees and Sunny.\"\n",
- "\n",
- "\n",
- "# Define an AssistantAgent with the model, tool, system message, and reflection enabled.\n",
- "# The system message instructs the agent via natural language.\n",
- "agent = AssistantAgent(\n",
- " name=\"weather_agent\",\n",
- " model_client=model_client,\n",
- " tools=[get_weather],\n",
- " system_message=\"You are a helpful assistant.\",\n",
- " reflect_on_tool_use=True,\n",
- " model_client_stream=True, # Enable streaming tokens from the model client.\n",
- ")\n",
- "\n",
- "\n",
- "# Run the agent and stream the messages to the console.\n",
- "async def main() -> None:\n",
- " await Console(agent.run_stream(task=\"What is the weather in New York?\"))\n",
- " # Close the connection to the model client.\n",
- " await model_client.close()\n",
- "\n",
- "\n",
- "# NOTE: if running this inside a Python script you'll need to use asyncio.run(main()).\n",
- "await main()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## What's Next?\n",
- "\n",
- "Now that you have a basic understanding of how to use a single agent, consider following the [tutorial](./tutorial/index.md) for a walkthrough on other features of AgentChat."
- ]
+
+Здесь сделай чтоб присадку делал согласно локальных координат компонентов, так как сейчас на повёрнутых под определённый градус угла компонентах не делает присадок потому что делает присадку согласно мировых координат что не допустимо. Пиши полный код целиком.
+
+
+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мм)
}
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.12.7"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
+
+ # Константа для зазора между деталями при раскладке
+ 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
+
+
+
+
+
+
+
+
Присадка планки
+
+
+
+
+
+
+
+
+
+
+
+
+
Отступ первой планки от короткого края.
+
+
+
+
+
Настройки присадки (Minifix)
+
+
+
+
Общее количество на стык.
+
+
+
+
+
+
+
+
+
+
+
+
+
Длина проекции торцевого отверстия.
+
+
+
+
+
+
+
+
+
+
Настройки петель
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Расстояние между центрами отверстий под саморезы (45мм).
+
+
+
+
+
+
Смещение отверстий от центра чашки (вдоль 21.5мм оси).
+
+
+
+
+
+
Отступ первой петли от короткого края.
+
+
+
+
+
Смещение центра чашки от бокового края (21.5мм).
+
+
+
+
+
+
+
+
+
+
+
+ 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