SketchupMCP
by BearNetwork-BRNKC
Verified
- SketchUp-MCP
- su_mcp
- su_mcp
require 'sketchup'
require 'json'
require 'socket'
require 'fileutils'
puts "MCP Extension loading..."
SKETCHUP_CONSOLE.show rescue nil
module SU_MCP
class Server
def initialize
@port = 9876
@server = nil
@running = false
@timer_id = nil
# Try multiple ways to show console
begin
SKETCHUP_CONSOLE.show
rescue
begin
Sketchup.send_action("showRubyPanel:")
rescue
UI.start_timer(0) { SKETCHUP_CONSOLE.show }
end
end
end
def log(msg)
begin
SKETCHUP_CONSOLE.write("MCP: #{msg}\n")
rescue
puts "MCP: #{msg}"
end
STDOUT.flush
end
def start
return if @running
begin
log "Starting server on localhost:#{@port}..."
@server = TCPServer.new('127.0.0.1', @port)
log "Server created on port #{@port}"
@running = true
@timer_id = UI.start_timer(0.1, true) {
begin
if @running
# Check for connection
ready = IO.select([@server], nil, nil, 0)
if ready
log "Connection waiting..."
client = @server.accept_nonblock
log "Client accepted"
data = client.gets
log "Raw data: #{data.inspect}"
if data
begin
# Parse the raw JSON first to check format
raw_request = JSON.parse(data)
log "Raw parsed request: #{raw_request.inspect}"
# Extract the original request ID if it exists in the raw data
original_id = nil
if data =~ /"id":\s*(\d+)/
original_id = $1.to_i
log "Found original request ID: #{original_id}"
end
# Use the raw request directly without transforming it
# Just ensure the ID is preserved if it exists
request = raw_request
if !request["id"] && original_id
request["id"] = original_id
log "Added missing ID: #{original_id}"
end
log "Processed request: #{request.inspect}"
response = handle_jsonrpc_request(request)
response_json = response.to_json + "\n"
log "Sending response: #{response_json.strip}"
client.write(response_json)
client.flush
log "Response sent"
rescue JSON::ParserError => e
log "JSON parse error: #{e.message}"
error_response = {
jsonrpc: "2.0",
error: { code: -32700, message: "Parse error" },
id: original_id
}.to_json + "\n"
client.write(error_response)
client.flush
rescue StandardError => e
log "Request error: #{e.message}"
error_response = {
jsonrpc: "2.0",
error: { code: -32603, message: e.message },
id: request ? request["id"] : original_id
}.to_json + "\n"
client.write(error_response)
client.flush
end
end
client.close
log "Client closed"
end
end
rescue IO::WaitReadable
# Normal for accept_nonblock
rescue StandardError => e
log "Timer error: #{e.message}"
log e.backtrace.join("\n")
end
}
log "Server started and listening"
rescue StandardError => e
log "Error: #{e.message}"
log e.backtrace.join("\n")
stop
end
end
def stop
log "Stopping server..."
@running = false
if @timer_id
UI.stop_timer(@timer_id)
@timer_id = nil
end
@server.close if @server
@server = nil
log "Server stopped"
end
private
def handle_jsonrpc_request(request)
log "Handling JSONRPC request: #{request.inspect}"
# Handle direct command format (for backward compatibility)
if request["command"]
tool_request = {
"method" => "tools/call",
"params" => {
"name" => request["command"],
"arguments" => request["parameters"]
},
"jsonrpc" => request["jsonrpc"] || "2.0",
"id" => request["id"]
}
log "Converting to tool request: #{tool_request.inspect}"
return handle_tool_call(tool_request)
end
# Handle jsonrpc format
case request["method"]
when "tools/call"
handle_tool_call(request)
when "resources/list"
{
jsonrpc: request["jsonrpc"] || "2.0",
result: {
resources: list_resources,
success: true
},
id: request["id"]
}
when "prompts/list"
{
jsonrpc: request["jsonrpc"] || "2.0",
result: {
prompts: [],
success: true
},
id: request["id"]
}
else
{
jsonrpc: request["jsonrpc"] || "2.0",
error: {
code: -32601,
message: "Method not found",
data: { success: false }
},
id: request["id"]
}
end
end
def list_resources
model = Sketchup.active_model
return [] unless model
model.entities.map do |entity|
{
id: entity.entityID,
type: entity.typename.downcase
}
end
end
def handle_tool_call(request)
log "Handling tool call: #{request.inspect}"
tool_name = request["params"]["name"]
args = request["params"]["arguments"]
begin
result = case tool_name
when "create_component"
create_component(args)
when "delete_component"
delete_component(args)
when "transform_component"
transform_component(args)
when "get_selection"
get_selection
when "export", "export_scene"
export_scene(args)
when "set_material"
set_material(args)
when "boolean_operation"
boolean_operation(args)
when "chamfer_edges"
chamfer_edges(args)
when "fillet_edges"
fillet_edges(args)
when "create_mortise_tenon"
create_mortise_tenon(args)
when "create_dovetail"
create_dovetail(args)
when "create_finger_joint"
create_finger_joint(args)
when "eval_ruby"
eval_ruby(args)
else
raise "Unknown tool: #{tool_name}"
end
log "Tool call result: #{result.inspect}"
if result[:success]
response = {
jsonrpc: request["jsonrpc"] || "2.0",
result: {
content: [{ type: "text", text: result[:result] || "Success" }],
isError: false,
success: true,
resourceId: result[:id]
},
id: request["id"]
}
log "Sending success response: #{response.inspect}"
response
else
response = {
jsonrpc: request["jsonrpc"] || "2.0",
error: {
code: -32603,
message: "Operation failed",
data: { success: false }
},
id: request["id"]
}
log "Sending error response: #{response.inspect}"
response
end
rescue StandardError => e
log "Tool call error: #{e.message}"
response = {
jsonrpc: request["jsonrpc"] || "2.0",
error: {
code: -32603,
message: e.message,
data: { success: false }
},
id: request["id"]
}
log "Sending error response: #{response.inspect}"
response
end
end
def create_component(params)
log "Creating component with params: #{params.inspect}"
model = Sketchup.active_model
log "Got active model: #{model.inspect}"
entities = model.active_entities
log "Got active entities: #{entities.inspect}"
pos = params["position"] || [0,0,0]
dims = params["dimensions"] || [1,1,1]
case params["type"]
when "cube"
log "Creating cube at position #{pos.inspect} with dimensions #{dims.inspect}"
begin
group = entities.add_group
log "Created group: #{group.inspect}"
face = group.entities.add_face(
[pos[0], pos[1], pos[2]],
[pos[0] + dims[0], pos[1], pos[2]],
[pos[0] + dims[0], pos[1] + dims[1], pos[2]],
[pos[0], pos[1] + dims[1], pos[2]]
)
log "Created face: #{face.inspect}"
face.pushpull(dims[2])
log "Pushed/pulled face by #{dims[2]}"
result = {
id: group.entityID,
success: true
}
log "Returning result: #{result.inspect}"
result
rescue StandardError => e
log "Error in create_component: #{e.message}"
log e.backtrace.join("\n")
raise
end
when "cylinder"
log "Creating cylinder at position #{pos.inspect} with dimensions #{dims.inspect}"
begin
# Create a group to contain the cylinder
group = entities.add_group
# Extract dimensions
radius = dims[0] / 2.0
height = dims[2]
# Create a circle at the base
center = [pos[0] + radius, pos[1] + radius, pos[2]]
# Create points for a circle
num_segments = 24 # Number of segments for the circle
circle_points = []
num_segments.times do |i|
angle = Math::PI * 2 * i / num_segments
x = center[0] + radius * Math.cos(angle)
y = center[1] + radius * Math.sin(angle)
z = center[2]
circle_points << [x, y, z]
end
# Create the circular face
face = group.entities.add_face(circle_points)
# Extrude the face to create the cylinder
face.pushpull(height)
result = {
id: group.entityID,
success: true
}
log "Created cylinder, returning result: #{result.inspect}"
result
rescue StandardError => e
log "Error creating cylinder: #{e.message}"
log e.backtrace.join("\n")
raise
end
when "sphere"
log "Creating sphere at position #{pos.inspect} with dimensions #{dims.inspect}"
begin
# Create a group to contain the sphere
group = entities.add_group
# Extract dimensions
radius = dims[0] / 2.0
center = [pos[0] + radius, pos[1] + radius, pos[2] + radius]
# Use SketchUp's built-in sphere method if available
if Sketchup::Tools.respond_to?(:create_sphere)
Sketchup::Tools.create_sphere(center, radius, 24, group.entities)
else
# Fallback implementation using polygons
# Create a UV sphere with latitude and longitude segments
segments = 16
# Create points for the sphere
points = []
for lat_i in 0..segments
lat = Math::PI * lat_i / segments
for lon_i in 0..segments
lon = 2 * Math::PI * lon_i / segments
x = center[0] + radius * Math.sin(lat) * Math.cos(lon)
y = center[1] + radius * Math.sin(lat) * Math.sin(lon)
z = center[2] + radius * Math.cos(lat)
points << [x, y, z]
end
end
# Create faces for the sphere (simplified approach)
for lat_i in 0...segments
for lon_i in 0...segments
i1 = lat_i * (segments + 1) + lon_i
i2 = i1 + 1
i3 = i1 + segments + 1
i4 = i3 + 1
# Create a quad face
begin
group.entities.add_face(points[i1], points[i2], points[i4], points[i3])
rescue StandardError => e
# Skip faces that can't be created (may happen at poles)
log "Skipping face: #{e.message}"
end
end
end
end
result = {
id: group.entityID,
success: true
}
log "Created sphere, returning result: #{result.inspect}"
result
rescue StandardError => e
log "Error creating sphere: #{e.message}"
log e.backtrace.join("\n")
raise
end
when "cone"
log "Creating cone at position #{pos.inspect} with dimensions #{dims.inspect}"
begin
# Create a group to contain the cone
group = entities.add_group
# Extract dimensions
radius = dims[0] / 2.0
height = dims[2]
# Create a circle at the base
center = [pos[0] + radius, pos[1] + radius, pos[2]]
apex = [center[0], center[1], center[2] + height]
# Create points for a circle
num_segments = 24 # Number of segments for the circle
circle_points = []
num_segments.times do |i|
angle = Math::PI * 2 * i / num_segments
x = center[0] + radius * Math.cos(angle)
y = center[1] + radius * Math.sin(angle)
z = center[2]
circle_points << [x, y, z]
end
# Create the circular face for the base
base = group.entities.add_face(circle_points)
# Create the cone sides
(0...num_segments).each do |i|
j = (i + 1) % num_segments
# Create a triangular face from two adjacent points on the circle to the apex
group.entities.add_face(circle_points[i], circle_points[j], apex)
end
result = {
id: group.entityID,
success: true
}
log "Created cone, returning result: #{result.inspect}"
result
rescue StandardError => e
log "Error creating cone: #{e.message}"
log e.backtrace.join("\n")
raise
end
else
raise "Unknown component type: #{params["type"]}"
end
end
def delete_component(params)
model = Sketchup.active_model
# Handle ID format - strip quotes if present
id_str = params["id"].to_s.gsub('"', '')
log "Looking for entity with ID: #{id_str}"
entity = model.find_entity_by_id(id_str.to_i)
if entity
log "Found entity: #{entity.inspect}"
entity.erase!
{ success: true }
else
raise "Entity not found"
end
end
def transform_component(params)
model = Sketchup.active_model
# Handle ID format - strip quotes if present
id_str = params["id"].to_s.gsub('"', '')
log "Looking for entity with ID: #{id_str}"
entity = model.find_entity_by_id(id_str.to_i)
if entity
log "Found entity: #{entity.inspect}"
# Handle position
if params["position"]
pos = params["position"]
log "Transforming position to #{pos.inspect}"
# Create a transformation to move the entity
translation = Geom::Transformation.translation(Geom::Point3d.new(pos[0], pos[1], pos[2]))
entity.transform!(translation)
end
# Handle rotation (in degrees)
if params["rotation"]
rot = params["rotation"]
log "Rotating by #{rot.inspect} degrees"
# Convert to radians
x_rot = rot[0] * Math::PI / 180
y_rot = rot[1] * Math::PI / 180
z_rot = rot[2] * Math::PI / 180
# Apply rotations
if rot[0] != 0
rotation = Geom::Transformation.rotation(entity.bounds.center, Geom::Vector3d.new(1, 0, 0), x_rot)
entity.transform!(rotation)
end
if rot[1] != 0
rotation = Geom::Transformation.rotation(entity.bounds.center, Geom::Vector3d.new(0, 1, 0), y_rot)
entity.transform!(rotation)
end
if rot[2] != 0
rotation = Geom::Transformation.rotation(entity.bounds.center, Geom::Vector3d.new(0, 0, 1), z_rot)
entity.transform!(rotation)
end
end
# Handle scale
if params["scale"]
scale = params["scale"]
log "Scaling by #{scale.inspect}"
# Create a transformation to scale the entity
center = entity.bounds.center
scaling = Geom::Transformation.scaling(center, scale[0], scale[1], scale[2])
entity.transform!(scaling)
end
{ success: true, id: entity.entityID }
else
raise "Entity not found"
end
end
def get_selection
model = Sketchup.active_model
selection = model.selection
log "Getting selection, count: #{selection.length}"
selected_entities = selection.map do |entity|
{
id: entity.entityID,
type: entity.typename.downcase
}
end
{ success: true, entities: selected_entities }
end
def export_scene(params)
log "Exporting scene with params: #{params.inspect}"
model = Sketchup.active_model
format = params["format"] || "skp"
begin
# Create a temporary directory for exports
temp_dir = File.join(ENV['TEMP'] || ENV['TMP'] || Dir.tmpdir, "sketchup_exports")
FileUtils.mkdir_p(temp_dir) unless Dir.exist?(temp_dir)
# Generate a unique filename
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
filename = "sketchup_export_#{timestamp}"
case format.downcase
when "skp"
# Export as SketchUp file
export_path = File.join(temp_dir, "#{filename}.skp")
log "Exporting to SketchUp file: #{export_path}"
model.save(export_path)
when "obj"
# Export as OBJ file
export_path = File.join(temp_dir, "#{filename}.obj")
log "Exporting to OBJ file: #{export_path}"
# Check if OBJ exporter is available
if Sketchup.require("sketchup.rb")
options = {
:triangulated_faces => true,
:double_sided_faces => true,
:edges => false,
:texture_maps => true
}
model.export(export_path, options)
else
raise "OBJ exporter not available"
end
when "dae"
# Export as COLLADA file
export_path = File.join(temp_dir, "#{filename}.dae")
log "Exporting to COLLADA file: #{export_path}"
# Check if COLLADA exporter is available
if Sketchup.require("sketchup.rb")
options = { :triangulated_faces => true }
model.export(export_path, options)
else
raise "COLLADA exporter not available"
end
when "stl"
# Export as STL file
export_path = File.join(temp_dir, "#{filename}.stl")
log "Exporting to STL file: #{export_path}"
# Check if STL exporter is available
if Sketchup.require("sketchup.rb")
options = { :units => "model" }
model.export(export_path, options)
else
raise "STL exporter not available"
end
when "png", "jpg", "jpeg"
# Export as image
ext = format.downcase == "jpg" ? "jpeg" : format.downcase
export_path = File.join(temp_dir, "#{filename}.#{ext}")
log "Exporting to image file: #{export_path}"
# Get the current view
view = model.active_view
# Set up options for the export
options = {
:filename => export_path,
:width => params["width"] || 1920,
:height => params["height"] || 1080,
:antialias => true,
:transparent => (ext == "png")
}
# Export the image
view.write_image(options)
else
raise "Unsupported export format: #{format}"
end
log "Export completed successfully to: #{export_path}"
{
success: true,
path: export_path,
format: format
}
rescue StandardError => e
log "Error in export_scene: #{e.message}"
log e.backtrace.join("\n")
raise
end
end
def set_material(params)
log "Setting material with params: #{params.inspect}"
model = Sketchup.active_model
# Handle ID format - strip quotes if present
id_str = params["id"].to_s.gsub('"', '')
log "Looking for entity with ID: #{id_str}"
entity = model.find_entity_by_id(id_str.to_i)
if entity
log "Found entity: #{entity.inspect}"
material_name = params["material"]
log "Setting material to: #{material_name}"
# Get or create the material
material = model.materials[material_name]
if !material
# Create a new material if it doesn't exist
material = model.materials.add(material_name)
# Handle color specification
case material_name.downcase
when "red"
material.color = Sketchup::Color.new(255, 0, 0)
when "green"
material.color = Sketchup::Color.new(0, 255, 0)
when "blue"
material.color = Sketchup::Color.new(0, 0, 255)
when "yellow"
material.color = Sketchup::Color.new(255, 255, 0)
when "cyan", "turquoise"
material.color = Sketchup::Color.new(0, 255, 255)
when "magenta", "purple"
material.color = Sketchup::Color.new(255, 0, 255)
when "white"
material.color = Sketchup::Color.new(255, 255, 255)
when "black"
material.color = Sketchup::Color.new(0, 0, 0)
when "brown"
material.color = Sketchup::Color.new(139, 69, 19)
when "orange"
material.color = Sketchup::Color.new(255, 165, 0)
when "gray", "grey"
material.color = Sketchup::Color.new(128, 128, 128)
else
# If it's a hex color code like "#FF0000"
if material_name.start_with?("#") && material_name.length == 7
begin
r = material_name[1..2].to_i(16)
g = material_name[3..4].to_i(16)
b = material_name[5..6].to_i(16)
material.color = Sketchup::Color.new(r, g, b)
rescue
# Default to a wood color if parsing fails
material.color = Sketchup::Color.new(184, 134, 72)
end
else
# Default to a wood color
material.color = Sketchup::Color.new(184, 134, 72)
end
end
end
# Apply the material to the entity
if entity.respond_to?(:material=)
entity.material = material
elsif entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance)
# For groups and components, we need to apply to all faces
entities = entity.is_a?(Sketchup::Group) ? entity.entities : entity.definition.entities
entities.grep(Sketchup::Face).each { |face| face.material = material }
end
{ success: true, id: entity.entityID }
else
raise "Entity not found"
end
end
def boolean_operation(params)
log "Performing boolean operation with params: #{params.inspect}"
model = Sketchup.active_model
# Get operation type
operation_type = params["operation"]
unless ["union", "difference", "intersection"].include?(operation_type)
raise "Invalid boolean operation: #{operation_type}. Must be 'union', 'difference', or 'intersection'."
end
# Get target and tool entities
target_id = params["target_id"].to_s.gsub('"', '')
tool_id = params["tool_id"].to_s.gsub('"', '')
log "Looking for target entity with ID: #{target_id}"
target_entity = model.find_entity_by_id(target_id.to_i)
log "Looking for tool entity with ID: #{tool_id}"
tool_entity = model.find_entity_by_id(tool_id.to_i)
unless target_entity && tool_entity
missing = []
missing << "target" unless target_entity
missing << "tool" unless tool_entity
raise "Entity not found: #{missing.join(', ')}"
end
# Ensure both entities are groups or component instances
unless (target_entity.is_a?(Sketchup::Group) || target_entity.is_a?(Sketchup::ComponentInstance)) &&
(tool_entity.is_a?(Sketchup::Group) || tool_entity.is_a?(Sketchup::ComponentInstance))
raise "Boolean operations require groups or component instances"
end
# Create a new group to hold the result
result_group = model.active_entities.add_group
# Perform the boolean operation
case operation_type
when "union"
log "Performing union operation"
perform_union(target_entity, tool_entity, result_group)
when "difference"
log "Performing difference operation"
perform_difference(target_entity, tool_entity, result_group)
when "intersection"
log "Performing intersection operation"
perform_intersection(target_entity, tool_entity, result_group)
end
# Clean up original entities if requested
if params["delete_originals"]
target_entity.erase! if target_entity.valid?
tool_entity.erase! if tool_entity.valid?
end
# Return the result
{
success: true,
id: result_group.entityID
}
end
def perform_union(target, tool, result_group)
model = Sketchup.active_model
# Create temporary copies of the target and tool
target_copy = target.copy
tool_copy = tool.copy
# Get the transformation of each entity
target_transform = target.transformation
tool_transform = tool.transformation
# Apply the transformations to the copies
target_copy.transform!(target_transform)
tool_copy.transform!(tool_transform)
# Get the entities from the copies
target_entities = target_copy.is_a?(Sketchup::Group) ? target_copy.entities : target_copy.definition.entities
tool_entities = tool_copy.is_a?(Sketchup::Group) ? tool_copy.entities : tool_copy.definition.entities
# Copy all entities from target to result
target_entities.each do |entity|
entity.copy(result_group.entities)
end
# Copy all entities from tool to result
tool_entities.each do |entity|
entity.copy(result_group.entities)
end
# Clean up temporary copies
target_copy.erase!
tool_copy.erase!
# Outer shell - this will merge overlapping geometry
result_group.entities.outer_shell
end
def perform_difference(target, tool, result_group)
model = Sketchup.active_model
# Create temporary copies of the target and tool
target_copy = target.copy
tool_copy = tool.copy
# Get the transformation of each entity
target_transform = target.transformation
tool_transform = tool.transformation
# Apply the transformations to the copies
target_copy.transform!(target_transform)
tool_copy.transform!(tool_transform)
# Get the entities from the copies
target_entities = target_copy.is_a?(Sketchup::Group) ? target_copy.entities : target_copy.definition.entities
tool_entities = tool_copy.is_a?(Sketchup::Group) ? tool_copy.entities : tool_copy.definition.entities
# Copy all entities from target to result
target_entities.each do |entity|
entity.copy(result_group.entities)
end
# Create a temporary group for the tool
temp_tool_group = model.active_entities.add_group
# Copy all entities from tool to temp group
tool_entities.each do |entity|
entity.copy(temp_tool_group.entities)
end
# Subtract the tool from the result
result_group.entities.subtract(temp_tool_group.entities)
# Clean up temporary copies and groups
target_copy.erase!
tool_copy.erase!
temp_tool_group.erase!
end
def perform_intersection(target, tool, result_group)
model = Sketchup.active_model
# Create temporary copies of the target and tool
target_copy = target.copy
tool_copy = tool.copy
# Get the transformation of each entity
target_transform = target.transformation
tool_transform = tool.transformation
# Apply the transformations to the copies
target_copy.transform!(target_transform)
tool_copy.transform!(tool_transform)
# Get the entities from the copies
target_entities = target_copy.is_a?(Sketchup::Group) ? target_copy.entities : target_copy.definition.entities
tool_entities = tool_copy.is_a?(Sketchup::Group) ? tool_copy.entities : tool_copy.definition.entities
# Create temporary groups for target and tool
temp_target_group = model.active_entities.add_group
temp_tool_group = model.active_entities.add_group
# Copy all entities from target and tool to temp groups
target_entities.each do |entity|
entity.copy(temp_target_group.entities)
end
tool_entities.each do |entity|
entity.copy(temp_tool_group.entities)
end
# Perform the intersection
result_group.entities.intersect_with(temp_target_group.entities, temp_tool_group.entities)
# Clean up temporary copies and groups
target_copy.erase!
tool_copy.erase!
temp_target_group.erase!
temp_tool_group.erase!
end
def chamfer_edges(params)
log "Chamfering edges with params: #{params.inspect}"
model = Sketchup.active_model
# Get entity ID
entity_id = params["entity_id"].to_s.gsub('"', '')
log "Looking for entity with ID: #{entity_id}"
entity = model.find_entity_by_id(entity_id.to_i)
unless entity
raise "Entity not found: #{entity_id}"
end
# Ensure entity is a group or component instance
unless entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance)
raise "Chamfer operation requires a group or component instance"
end
# Get the distance parameter
distance = params["distance"] || 0.5
# Get the entities collection
entities = entity.is_a?(Sketchup::Group) ? entity.entities : entity.definition.entities
# Find all edges in the entity
edges = entities.grep(Sketchup::Edge)
# If specific edges are provided, filter the edges
if params["edge_indices"] && params["edge_indices"].is_a?(Array)
edge_indices = params["edge_indices"]
edges = edges.select.with_index { |_, i| edge_indices.include?(i) }
end
# Create a new group to hold the result
result_group = model.active_entities.add_group
# Copy all entities from the original to the result
entities.each do |e|
e.copy(result_group.entities)
end
# Get the edges in the result group
result_edges = result_group.entities.grep(Sketchup::Edge)
# If specific edges were provided, filter the result edges
if params["edge_indices"] && params["edge_indices"].is_a?(Array)
edge_indices = params["edge_indices"]
result_edges = result_edges.select.with_index { |_, i| edge_indices.include?(i) }
end
# Perform the chamfer operation
begin
# Create a transformation for the chamfer
chamfer_transform = Geom::Transformation.scaling(1.0 - distance)
# For each edge, create a chamfer
result_edges.each do |edge|
# Get the faces connected to this edge
faces = edge.faces
next if faces.length < 2
# Get the start and end points of the edge
start_point = edge.start.position
end_point = edge.end.position
# Calculate the midpoint of the edge
midpoint = Geom::Point3d.new(
(start_point.x + end_point.x) / 2.0,
(start_point.y + end_point.y) / 2.0,
(start_point.z + end_point.z) / 2.0
)
# Create a chamfer by creating a new face
# This is a simplified approach - in a real implementation,
# you would need to handle various edge cases
new_points = []
# For each vertex of the edge
[edge.start, edge.end].each do |vertex|
# Get all edges connected to this vertex
connected_edges = vertex.edges - [edge]
# For each connected edge
connected_edges.each do |connected_edge|
# Get the other vertex of the connected edge
other_vertex = (connected_edge.vertices - [vertex])[0]
# Calculate a point along the connected edge
direction = other_vertex.position - vertex.position
new_point = vertex.position.offset(direction, distance)
new_points << new_point
end
end
# Create a new face using the new points
if new_points.length >= 3
result_group.entities.add_face(new_points)
end
end
# Clean up the original entity if requested
if params["delete_original"]
entity.erase! if entity.valid?
end
# Return the result
{
success: true,
id: result_group.entityID
}
rescue StandardError => e
log "Error in chamfer_edges: #{e.message}"
log e.backtrace.join("\n")
# Clean up the result group if there was an error
result_group.erase! if result_group.valid?
raise
end
end
def fillet_edges(params)
log "Filleting edges with params: #{params.inspect}"
model = Sketchup.active_model
# Get entity ID
entity_id = params["entity_id"].to_s.gsub('"', '')
log "Looking for entity with ID: #{entity_id}"
entity = model.find_entity_by_id(entity_id.to_i)
unless entity
raise "Entity not found: #{entity_id}"
end
# Ensure entity is a group or component instance
unless entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance)
raise "Fillet operation requires a group or component instance"
end
# Get the radius parameter
radius = params["radius"] || 0.5
# Get the number of segments for the fillet
segments = params["segments"] || 8
# Get the entities collection
entities = entity.is_a?(Sketchup::Group) ? entity.entities : entity.definition.entities
# Find all edges in the entity
edges = entities.grep(Sketchup::Edge)
# If specific edges are provided, filter the edges
if params["edge_indices"] && params["edge_indices"].is_a?(Array)
edge_indices = params["edge_indices"]
edges = edges.select.with_index { |_, i| edge_indices.include?(i) }
end
# Create a new group to hold the result
result_group = model.active_entities.add_group
# Copy all entities from the original to the result
entities.each do |e|
e.copy(result_group.entities)
end
# Get the edges in the result group
result_edges = result_group.entities.grep(Sketchup::Edge)
# If specific edges were provided, filter the result edges
if params["edge_indices"] && params["edge_indices"].is_a?(Array)
edge_indices = params["edge_indices"]
result_edges = result_edges.select.with_index { |_, i| edge_indices.include?(i) }
end
# Perform the fillet operation
begin
# For each edge, create a fillet
result_edges.each do |edge|
# Get the faces connected to this edge
faces = edge.faces
next if faces.length < 2
# Get the start and end points of the edge
start_point = edge.start.position
end_point = edge.end.position
# Calculate the midpoint of the edge
midpoint = Geom::Point3d.new(
(start_point.x + end_point.x) / 2.0,
(start_point.y + end_point.y) / 2.0,
(start_point.z + end_point.z) / 2.0
)
# Calculate the edge vector
edge_vector = end_point - start_point
edge_length = edge_vector.length
# Create points for the fillet curve
fillet_points = []
# Create a series of points along a circular arc
(0..segments).each do |i|
angle = Math::PI * i / segments
# Calculate the point on the arc
x = midpoint.x + radius * Math.cos(angle)
y = midpoint.y + radius * Math.sin(angle)
z = midpoint.z
fillet_points << Geom::Point3d.new(x, y, z)
end
# Create edges connecting the fillet points
(0...fillet_points.length - 1).each do |i|
result_group.entities.add_line(fillet_points[i], fillet_points[i+1])
end
# Create a face from the fillet points
if fillet_points.length >= 3
result_group.entities.add_face(fillet_points)
end
end
# Clean up the original entity if requested
if params["delete_original"]
entity.erase! if entity.valid?
end
# Return the result
{
success: true,
id: result_group.entityID
}
rescue StandardError => e
log "Error in fillet_edges: #{e.message}"
log e.backtrace.join("\n")
# Clean up the result group if there was an error
result_group.erase! if result_group.valid?
raise
end
end
def create_mortise_tenon(params)
log "Creating mortise and tenon joint with params: #{params.inspect}"
model = Sketchup.active_model
# Get the mortise and tenon board IDs
mortise_id = params["mortise_id"].to_s.gsub('"', '')
tenon_id = params["tenon_id"].to_s.gsub('"', '')
log "Looking for mortise board with ID: #{mortise_id}"
mortise_board = model.find_entity_by_id(mortise_id.to_i)
log "Looking for tenon board with ID: #{tenon_id}"
tenon_board = model.find_entity_by_id(tenon_id.to_i)
unless mortise_board && tenon_board
missing = []
missing << "mortise board" unless mortise_board
missing << "tenon board" unless tenon_board
raise "Entity not found: #{missing.join(', ')}"
end
# Ensure both entities are groups or component instances
unless (mortise_board.is_a?(Sketchup::Group) || mortise_board.is_a?(Sketchup::ComponentInstance)) &&
(tenon_board.is_a?(Sketchup::Group) || tenon_board.is_a?(Sketchup::ComponentInstance))
raise "Mortise and tenon operation requires groups or component instances"
end
# Get joint parameters
width = params["width"] || 1.0
height = params["height"] || 1.0
depth = params["depth"] || 1.0
offset_x = params["offset_x"] || 0.0
offset_y = params["offset_y"] || 0.0
offset_z = params["offset_z"] || 0.0
# Get the bounds of both boards
mortise_bounds = mortise_board.bounds
tenon_bounds = tenon_board.bounds
# Determine the face to place the joint on based on the relative positions of the boards
mortise_center = mortise_bounds.center
tenon_center = tenon_bounds.center
# Calculate the direction vector from mortise to tenon
direction_vector = tenon_center - mortise_center
# Determine which face of the mortise board is closest to the tenon board
mortise_face_direction = determine_closest_face(direction_vector)
# Create the mortise (hole) in the mortise board
mortise_result = create_mortise(
mortise_board,
width,
height,
depth,
mortise_face_direction,
mortise_bounds,
offset_x,
offset_y,
offset_z
)
# Determine which face of the tenon board is closest to the mortise board
tenon_face_direction = determine_closest_face(direction_vector.reverse)
# Create the tenon (projection) on the tenon board
tenon_result = create_tenon(
tenon_board,
width,
height,
depth,
tenon_face_direction,
tenon_bounds,
offset_x,
offset_y,
offset_z
)
# Return the result
{
success: true,
mortise_id: mortise_result[:id],
tenon_id: tenon_result[:id]
}
end
def determine_closest_face(direction_vector)
# Normalize the direction vector
direction_vector.normalize!
# Determine which axis has the largest component
x_abs = direction_vector.x.abs
y_abs = direction_vector.y.abs
z_abs = direction_vector.z.abs
if x_abs >= y_abs && x_abs >= z_abs
# X-axis is dominant
return direction_vector.x > 0 ? :east : :west
elsif y_abs >= x_abs && y_abs >= z_abs
# Y-axis is dominant
return direction_vector.y > 0 ? :north : :south
else
# Z-axis is dominant
return direction_vector.z > 0 ? :top : :bottom
end
end
def create_mortise(board, width, height, depth, face_direction, bounds, offset_x, offset_y, offset_z)
model = Sketchup.active_model
# Get the board's entities
entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
# Calculate the position of the mortise based on the face direction
mortise_position = calculate_position_on_face(face_direction, bounds, width, height, depth, offset_x, offset_y, offset_z)
log "Creating mortise at position: #{mortise_position.inspect} with dimensions: #{[width, height, depth].inspect}"
# Create a box for the mortise
mortise_group = entities.add_group
# Create the mortise box with the correct orientation
case face_direction
when :east, :west
# Mortise on east or west face (YZ plane)
mortise_face = mortise_group.entities.add_face(
[mortise_position[0], mortise_position[1], mortise_position[2]],
[mortise_position[0], mortise_position[1] + width, mortise_position[2]],
[mortise_position[0], mortise_position[1] + width, mortise_position[2] + height],
[mortise_position[0], mortise_position[1], mortise_position[2] + height]
)
mortise_face.pushpull(face_direction == :east ? -depth : depth)
when :north, :south
# Mortise on north or south face (XZ plane)
mortise_face = mortise_group.entities.add_face(
[mortise_position[0], mortise_position[1], mortise_position[2]],
[mortise_position[0] + width, mortise_position[1], mortise_position[2]],
[mortise_position[0] + width, mortise_position[1], mortise_position[2] + height],
[mortise_position[0], mortise_position[1], mortise_position[2] + height]
)
mortise_face.pushpull(face_direction == :north ? -depth : depth)
when :top, :bottom
# Mortise on top or bottom face (XY plane)
mortise_face = mortise_group.entities.add_face(
[mortise_position[0], mortise_position[1], mortise_position[2]],
[mortise_position[0] + width, mortise_position[1], mortise_position[2]],
[mortise_position[0] + width, mortise_position[1] + height, mortise_position[2]],
[mortise_position[0], mortise_position[1] + height, mortise_position[2]]
)
mortise_face.pushpull(face_direction == :top ? -depth : depth)
end
# Subtract the mortise from the board
entities.subtract(mortise_group.entities)
# Clean up the temporary group
mortise_group.erase!
# Return the result
{
success: true,
id: board.entityID
}
end
def create_tenon(board, width, height, depth, face_direction, bounds, offset_x, offset_y, offset_z)
model = Sketchup.active_model
# Get the board's entities
entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
# Calculate the position of the tenon based on the face direction
tenon_position = calculate_position_on_face(face_direction, bounds, width, height, depth, offset_x, offset_y, offset_z)
log "Creating tenon at position: #{tenon_position.inspect} with dimensions: #{[width, height, depth].inspect}"
# Create a box for the tenon
tenon_group = model.active_entities.add_group
# Create the tenon box with the correct orientation
case face_direction
when :east, :west
# Tenon on east or west face (YZ plane)
tenon_face = tenon_group.entities.add_face(
[tenon_position[0], tenon_position[1], tenon_position[2]],
[tenon_position[0], tenon_position[1] + width, tenon_position[2]],
[tenon_position[0], tenon_position[1] + width, tenon_position[2] + height],
[tenon_position[0], tenon_position[1], tenon_position[2] + height]
)
tenon_face.pushpull(face_direction == :east ? depth : -depth)
when :north, :south
# Tenon on north or south face (XZ plane)
tenon_face = tenon_group.entities.add_face(
[tenon_position[0], tenon_position[1], tenon_position[2]],
[tenon_position[0] + width, tenon_position[1], tenon_position[2]],
[tenon_position[0] + width, tenon_position[1], tenon_position[2] + height],
[tenon_position[0], tenon_position[1], tenon_position[2] + height]
)
tenon_face.pushpull(face_direction == :north ? depth : -depth)
when :top, :bottom
# Tenon on top or bottom face (XY plane)
tenon_face = tenon_group.entities.add_face(
[tenon_position[0], tenon_position[1], tenon_position[2]],
[tenon_position[0] + width, tenon_position[1], tenon_position[2]],
[tenon_position[0] + width, tenon_position[1] + height, tenon_position[2]],
[tenon_position[0], tenon_position[1] + height, tenon_position[2]]
)
tenon_face.pushpull(face_direction == :top ? depth : -depth)
end
# Get the transformation of the board
board_transform = board.transformation
# Apply the inverse transformation to the tenon group
tenon_group.transform!(board_transform.inverse)
# Union the tenon with the board
board_entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
board_entities.add_instance(tenon_group.entities.parent, Geom::Transformation.new)
# Clean up the temporary group
tenon_group.erase!
# Return the result
{
success: true,
id: board.entityID
}
end
def calculate_position_on_face(face_direction, bounds, width, height, depth, offset_x, offset_y, offset_z)
# Calculate the position on the specified face with offsets
case face_direction
when :east
# Position on the east face (max X)
[
bounds.max.x,
bounds.center.y - width/2 + offset_y,
bounds.center.z - height/2 + offset_z
]
when :west
# Position on the west face (min X)
[
bounds.min.x,
bounds.center.y - width/2 + offset_y,
bounds.center.z - height/2 + offset_z
]
when :north
# Position on the north face (max Y)
[
bounds.center.x - width/2 + offset_x,
bounds.max.y,
bounds.center.z - height/2 + offset_z
]
when :south
# Position on the south face (min Y)
[
bounds.center.x - width/2 + offset_x,
bounds.min.y,
bounds.center.z - height/2 + offset_z
]
when :top
# Position on the top face (max Z)
[
bounds.center.x - width/2 + offset_x,
bounds.center.y - height/2 + offset_y,
bounds.max.z
]
when :bottom
# Position on the bottom face (min Z)
[
bounds.center.x - width/2 + offset_x,
bounds.center.y - height/2 + offset_y,
bounds.min.z
]
end
end
def create_dovetail(params)
log "Creating dovetail joint with params: #{params.inspect}"
model = Sketchup.active_model
# Get the tail and pin board IDs
tail_id = params["tail_id"].to_s.gsub('"', '')
pin_id = params["pin_id"].to_s.gsub('"', '')
log "Looking for tail board with ID: #{tail_id}"
tail_board = model.find_entity_by_id(tail_id.to_i)
log "Looking for pin board with ID: #{pin_id}"
pin_board = model.find_entity_by_id(pin_id.to_i)
unless tail_board && pin_board
missing = []
missing << "tail board" unless tail_board
missing << "pin board" unless pin_board
raise "Entity not found: #{missing.join(', ')}"
end
# Ensure both entities are groups or component instances
unless (tail_board.is_a?(Sketchup::Group) || tail_board.is_a?(Sketchup::ComponentInstance)) &&
(pin_board.is_a?(Sketchup::Group) || pin_board.is_a?(Sketchup::ComponentInstance))
raise "Dovetail operation requires groups or component instances"
end
# Get joint parameters
width = params["width"] || 1.0
height = params["height"] || 2.0
depth = params["depth"] || 1.0
angle = params["angle"] || 15.0 # Dovetail angle in degrees
num_tails = params["num_tails"] || 3
offset_x = params["offset_x"] || 0.0
offset_y = params["offset_y"] || 0.0
offset_z = params["offset_z"] || 0.0
# Create the tails on the tail board
tail_result = create_tails(tail_board, width, height, depth, angle, num_tails, offset_x, offset_y, offset_z)
# Create the pins on the pin board
pin_result = create_pins(pin_board, width, height, depth, angle, num_tails, offset_x, offset_y, offset_z)
# Return the result
{
success: true,
tail_id: tail_result[:id],
pin_id: pin_result[:id]
}
end
def create_tails(board, width, height, depth, angle, num_tails, offset_x, offset_y, offset_z)
model = Sketchup.active_model
# Get the board's entities
entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
# Get the board's bounds
bounds = board.bounds
# Calculate the position of the dovetail joint
center_x = bounds.center.x + offset_x
center_y = bounds.center.y + offset_y
center_z = bounds.center.z + offset_z
# Calculate the width of each tail and space
total_width = width
tail_width = total_width / (2 * num_tails - 1)
# Create a group for the tails
tails_group = entities.add_group
# Create each tail
num_tails.times do |i|
# Calculate the position of this tail
tail_center_x = center_x - width/2 + tail_width * (2 * i)
# Calculate the dovetail shape
angle_rad = angle * Math::PI / 180.0
tail_top_width = tail_width
tail_bottom_width = tail_width + 2 * depth * Math.tan(angle_rad)
# Create the tail shape
tail_points = [
[tail_center_x - tail_top_width/2, center_y - height/2, center_z],
[tail_center_x + tail_top_width/2, center_y - height/2, center_z],
[tail_center_x + tail_bottom_width/2, center_y - height/2, center_z - depth],
[tail_center_x - tail_bottom_width/2, center_y - height/2, center_z - depth]
]
# Create the tail face
tail_face = tails_group.entities.add_face(tail_points)
# Extrude the tail
tail_face.pushpull(height)
end
# Return the result
{
success: true,
id: board.entityID
}
end
def create_pins(board, width, height, depth, angle, num_tails, offset_x, offset_y, offset_z)
model = Sketchup.active_model
# Get the board's entities
entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
# Get the board's bounds
bounds = board.bounds
# Calculate the position of the dovetail joint
center_x = bounds.center.x + offset_x
center_y = bounds.center.y + offset_y
center_z = bounds.center.z + offset_z
# Calculate the width of each tail and space
total_width = width
tail_width = total_width / (2 * num_tails - 1)
# Create a group for the pins
pins_group = entities.add_group
# Create a box for the entire pin area
pin_area_face = pins_group.entities.add_face(
[center_x - width/2, center_y - height/2, center_z],
[center_x + width/2, center_y - height/2, center_z],
[center_x + width/2, center_y + height/2, center_z],
[center_x - width/2, center_y + height/2, center_z]
)
# Extrude the pin area
pin_area_face.pushpull(depth)
# Create each tail cutout
num_tails.times do |i|
# Calculate the position of this tail
tail_center_x = center_x - width/2 + tail_width * (2 * i)
# Calculate the dovetail shape
angle_rad = angle * Math::PI / 180.0
tail_top_width = tail_width
tail_bottom_width = tail_width + 2 * depth * Math.tan(angle_rad)
# Create a group for the tail cutout
tail_cutout_group = entities.add_group
# Create the tail cutout shape
tail_points = [
[tail_center_x - tail_top_width/2, center_y - height/2, center_z],
[tail_center_x + tail_top_width/2, center_y - height/2, center_z],
[tail_center_x + tail_bottom_width/2, center_y - height/2, center_z - depth],
[tail_center_x - tail_bottom_width/2, center_y - height/2, center_z - depth]
]
# Create the tail cutout face
tail_face = tail_cutout_group.entities.add_face(tail_points)
# Extrude the tail cutout
tail_face.pushpull(height)
# Subtract the tail cutout from the pin area
pins_group.entities.subtract(tail_cutout_group.entities)
# Clean up the temporary group
tail_cutout_group.erase!
end
# Return the result
{
success: true,
id: board.entityID
}
end
def create_finger_joint(params)
log "Creating finger joint with params: #{params.inspect}"
model = Sketchup.active_model
# Get the two board IDs
board1_id = params["board1_id"].to_s.gsub('"', '')
board2_id = params["board2_id"].to_s.gsub('"', '')
log "Looking for board 1 with ID: #{board1_id}"
board1 = model.find_entity_by_id(board1_id.to_i)
log "Looking for board 2 with ID: #{board2_id}"
board2 = model.find_entity_by_id(board2_id.to_i)
unless board1 && board2
missing = []
missing << "board 1" unless board1
missing << "board 2" unless board2
raise "Entity not found: #{missing.join(', ')}"
end
# Ensure both entities are groups or component instances
unless (board1.is_a?(Sketchup::Group) || board1.is_a?(Sketchup::ComponentInstance)) &&
(board2.is_a?(Sketchup::Group) || board2.is_a?(Sketchup::ComponentInstance))
raise "Finger joint operation requires groups or component instances"
end
# Get joint parameters
width = params["width"] || 1.0
height = params["height"] || 2.0
depth = params["depth"] || 1.0
num_fingers = params["num_fingers"] || 5
offset_x = params["offset_x"] || 0.0
offset_y = params["offset_y"] || 0.0
offset_z = params["offset_z"] || 0.0
# Create the fingers on board 1
board1_result = create_board1_fingers(board1, width, height, depth, num_fingers, offset_x, offset_y, offset_z)
# Create the matching slots on board 2
board2_result = create_board2_slots(board2, width, height, depth, num_fingers, offset_x, offset_y, offset_z)
# Return the result
{
success: true,
board1_id: board1_result[:id],
board2_id: board2_result[:id]
}
end
def create_board1_fingers(board, width, height, depth, num_fingers, offset_x, offset_y, offset_z)
model = Sketchup.active_model
# Get the board's entities
entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
# Get the board's bounds
bounds = board.bounds
# Calculate the position of the joint
center_x = bounds.center.x + offset_x
center_y = bounds.center.y + offset_y
center_z = bounds.center.z + offset_z
# Calculate the width of each finger
finger_width = width / num_fingers
# Create a group for the fingers
fingers_group = entities.add_group
# Create a base rectangle for the joint area
base_face = fingers_group.entities.add_face(
[center_x - width/2, center_y - height/2, center_z],
[center_x + width/2, center_y - height/2, center_z],
[center_x + width/2, center_y + height/2, center_z],
[center_x - width/2, center_y + height/2, center_z]
)
# Create cutouts for the spaces between fingers
(num_fingers / 2).times do |i|
# Calculate the position of this cutout
cutout_center_x = center_x - width/2 + finger_width * (2 * i + 1)
# Create a group for the cutout
cutout_group = entities.add_group
# Create the cutout shape
cutout_face = cutout_group.entities.add_face(
[cutout_center_x - finger_width/2, center_y - height/2, center_z],
[cutout_center_x + finger_width/2, center_y - height/2, center_z],
[cutout_center_x + finger_width/2, center_y + height/2, center_z],
[cutout_center_x - finger_width/2, center_y + height/2, center_z]
)
# Extrude the cutout
cutout_face.pushpull(depth)
# Subtract the cutout from the fingers
fingers_group.entities.subtract(cutout_group.entities)
# Clean up the temporary group
cutout_group.erase!
end
# Extrude the fingers
base_face.pushpull(depth)
# Return the result
{
success: true,
id: board.entityID
}
end
def create_board2_slots(board, width, height, depth, num_fingers, offset_x, offset_y, offset_z)
model = Sketchup.active_model
# Get the board's entities
entities = board.is_a?(Sketchup::Group) ? board.entities : board.definition.entities
# Get the board's bounds
bounds = board.bounds
# Calculate the position of the joint
center_x = bounds.center.x + offset_x
center_y = bounds.center.y + offset_y
center_z = bounds.center.z + offset_z
# Calculate the width of each finger
finger_width = width / num_fingers
# Create a group for the slots
slots_group = entities.add_group
# Create cutouts for the fingers from board 1
(num_fingers / 2 + num_fingers % 2).times do |i|
# Calculate the position of this cutout
cutout_center_x = center_x - width/2 + finger_width * (2 * i)
# Create a group for the cutout
cutout_group = entities.add_group
# Create the cutout shape
cutout_face = cutout_group.entities.add_face(
[cutout_center_x - finger_width/2, center_y - height/2, center_z],
[cutout_center_x + finger_width/2, center_y - height/2, center_z],
[cutout_center_x + finger_width/2, center_y + height/2, center_z],
[cutout_center_x - finger_width/2, center_y + height/2, center_z]
)
# Extrude the cutout
cutout_face.pushpull(depth)
# Subtract the cutout from the board
entities.subtract(cutout_group.entities)
# Clean up the temporary group
cutout_group.erase!
end
# Return the result
{
success: true,
id: board.entityID
}
end
def eval_ruby(params)
log "Evaluating Ruby code with length: #{params['code'].length}"
begin
# Create a safe binding for evaluation
binding = TOPLEVEL_BINDING.dup
# Evaluate the Ruby code
log "Starting code evaluation..."
result = eval(params["code"], binding)
log "Code evaluation completed with result: #{result.inspect}"
# Return success with the result as a string
{
success: true,
result: result.to_s
}
rescue StandardError => e
log "Error in eval_ruby: #{e.message}"
log e.backtrace.join("\n")
raise "Ruby evaluation error: #{e.message}"
end
end
end
unless file_loaded?(__FILE__)
@server = Server.new
menu = UI.menu("Plugins").add_submenu("MCP Server")
menu.add_item("Start Server") { @server.start }
menu.add_item("Stop Server") { @server.stop }
file_loaded(__FILE__)
end
end