Yes, I used AI, I achieved many advancements with the plugin as a whole, it is very robust and has several tools, it frustrates me a little not knowing how to fix potential errors.
`# material.rb
module GCut
require 'json'
# =========================================================
# Helpers para filtro de componentes / faces
# =========================================================
# Componente é “válido” para receber material do GCut?
# → precisa ter algum atributo us_material (no instance ou definition)
def self.componente_com_us_material?(inst)
return false unless inst.is_a?(Sketchup::ComponentInstance)
defn = inst.definition
return false unless defn
fontes = [
inst.get_attribute('dynamic_attributes', 'us_material'),
inst.get_attribute('gcut', 'us_material'),
defn.get_attribute('dynamic_attributes','us_material'),
defn.get_attribute('gcut', 'us_material')
]
fontes.compact.any? { |v| !v.to_s.strip.empty? }
end
# Face marcada como “Sem material” em qualquer lado?
def self.face_sem_material?(face)
return false unless face.is_a?(Sketchup::Face)
mats = []
mats << face.material if face.material
mats << face.back_material if face.back_material
mats.any? do |m|
nome =
if m.respond_to?(:display_name)
m.display_name.to_s
else
m.name.to_s
end
nome.downcase.include?('sem material')
end
end
# =========================================================
# Aplicar material recursivamente
# =========================================================
def self.apply_material_recursivo(component_instance, material, filtro_tipo = "TODOS")
return unless component_instance.is_a?(Sketchup::ComponentInstance)
definition = component_instance.definition
return unless definition && definition.valid?
# item_type da peça (PORTA, LATERAL, etc.)
tipo = definition.get_attribute("dynamic_attributes", "item_type", "").to_s.upcase.strip
filtro = filtro_tipo.to_s.upcase.strip
# Só aplica se:
# - o tipo bate com o filtro (ou filtro = TODOS)
# - E o componente tiver marker us_material
aplicar_aqui = (filtro == "TODOS" || tipo == filtro)
aplicar_aqui &&= componente_com_us_material?(component_instance)
if aplicar_aqui
# pinta o instance (borda/aresta do componente)
component_instance.material = material
# pinta faces da definição, respeitando faces “Sem material”
definition.entities.grep(Sketchup::Face).each do |face|
next if face_sem_material?(face)
begin
# Frente
if face.material
nome = face.material.display_name.to_s.downcase rescue face.material.name.to_s.downcase
face.material = material unless nome.include?('sem material')
else
face.material = material
end
# Verso
if face.back_material
nome_b = face.back_material.display_name.to_s.downcase rescue face.back_material.name.to_s.downcase
face.back_material = material unless nome_b.include?('sem material')
else
face.back_material = material
end
rescue => e
puts "⚠️ apply_material_recursivo(face): #{e.message}"
end
end
# grava us_material com o nome do material aplicado
nome_mat = material.name.to_s
definition.set_attribute("dynamic_attributes", "us_material", nome_mat)
component_instance.set_attribute("dynamic_attributes", "us_material", nome_mat)
end
# recursão nos subcomponentes (mantendo o mesmo filtro)
definition.entities.grep(Sketchup::ComponentInstance).each do |sub_inst|
apply_material_recursivo(sub_inst, material, filtro_tipo)
end
# se aplicou material aqui, ajusta o veio dessa definição
orientar_veio_na_definicao(definition, material) if aplicar_aqui
end
# =========================================================
# Orientar veio da textura na definição
# =========================================================
def self.orientar_veio_na_definicao(definition, material)
return unless definition&.valid? && material
model = Sketchup.active_model
ro = model.rendering_options
definition.entities.grep(Sketchup::Face).each do |face|
# só ajusta faces que estejam usando esse material (frente ou verso)
next unless face.material == material || face.back_material == material
begin
uvh = face.get_UVHelper(true, true, ro) # pode ser usado depois se precisar
verts = face.outer_loop.vertices
next if verts.length < 3
e_long = face.edges.max_by { |e| e.length }
a = e_long.start.position
b = e_long.end.position
restante = (face.vertices.map(&:position) - [a, b])
c = restante.first || verts[2].position
u_len = 1000.mm
v_len = 1000.mm
uv_a = Geom::Point3d.new(0, 0, 0)
uv_b = Geom::Point3d.new(u_len, 0, 0)
uv_c = Geom::Point3d.new(0, v_len,0)
face.position_material(material, [a, uv_a, b, uv_b, c, uv_c], true)
if face.back_material == material
face.position_material(material, [a, uv_a, b, uv_b, c, uv_c], false)
end
rescue => ex
puts "⚠️ orientar_veio_na_definicao: #{ex.message}"
end
end
end
# =========================================================
# Diálogo de Materiais
# =========================================================
def self.abrir_materiais
dlg = UI::HtmlDialog.new({
dialog_title: "Materiais Disponíveis",
preferences_key: "GCut.materiais",
scrollable: true,
resizable: true,
width: 500,
height: 750,
style: UI::HtmlDialog::STYLE_DIALOG
})
html = <<-HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
}
h2 {
text-align: center;
color: #007ACC;
margin-bottom: 20px;
}
#tipoFiltro, #search {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 14px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 15px;
margin-top: 15px;
}
.item {
background: white;
border-radius: 8px;
box-shadow: 1px 1px 4px rgba(0,0,0,0.1);
text-align: center;
padding: 15px;
transition: transform 0.2s;
cursor: pointer;
}
.item:hover {
background-color: #e6f4ff;
transform: scale(1.02);
}
.thumbnail {
width: 80px;
height: 80px;
margin: 0 auto 10px;
background-color: #ddd;
background-size: cover;
background-position: center;
border-radius: 5px;
border: 1px solid #ccc;
}
.name {
margin-top: 8px;
font-size: 12px;
font-weight: bold;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rotate, .price {
display: none;
font-size: 11px;
margin-top: 8px;
text-align: left;
}
.price input {
width: 100%;
font-size: 11px;
padding: 4px;
box-sizing: border-box;
margin-top: 4px;
}
#togglePriceBtn {
width: 100%;
padding: 10px;
background: #009933;
color: white;
border: none;
border-radius: 5px;
font-size: 14px;
margin-bottom: 15px;
cursor: pointer;
font-weight: bold;
}
#togglePriceBtn:hover {
background: #007a29;
}
.config-label {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 5px;
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 40px 20px;
color: #666;
}
</style>
</head>
<body>
<h2>Materiais</h2>
<select id="tipoFiltro">
<option value="TODOS">Todos</option>
<option value="PORTA">Porta</option>
<option value="LATERAL">Lateral</option>
<option value="FRENTE">Frente</option>
<option value="FUNDO">Fundo</option>
</select>
<input type="text" id="search" placeholder="Buscar material...">
<button id="togglePriceBtn">Parâmetros</button>
<div class="grid" id="material-list">Carregando materiais...</div>
<script>
let allMaterials = [];
let filtroSelecionado = "TODOS";
let priceMode = false;
document.getElementById("tipoFiltro").addEventListener("change", function () {
filtroSelecionado = this.value;
if (window.sketchup && sketchup.setFilter) {
sketchup.setFilter(filtroSelecionado);
}
});
document.getElementById('togglePriceBtn').addEventListener('click', function () {
priceMode = !priceMode;
this.innerText = priceMode ? "✅ Salvar" : "Parâmetros";
if (window.sketchup && sketchup.getMaterials) {
sketchup.getMaterials();
}
});
function loadMaterials(materials) {
allMaterials = materials;
renderMaterials(materials);
}
function renderMaterials(materials) {
const list = document.getElementById('material-list');
list.innerHTML = '';
if (materials.length === 0) {
list.innerHTML = '<div class="empty-state"><p>Nenhum material encontrado.</p></div>';
return;
}
materials.forEach(mat => {
const div = document.createElement('div');
div.className = 'item';
const thumb = document.createElement('div');
thumb.className = 'thumbnail';
thumb.style.backgroundImage = "url('file:///" + mat.thumbnail + "')";
const label = document.createElement('div');
label.className = 'name';
label.innerText = mat.name;
const rotateBox = document.createElement('div');
rotateBox.className = 'rotate';
rotateBox.innerHTML = `
<div class="config-label">
<input type="checkbox" ${mat.rotate ? 'checked' : ''} onchange="setRotation('${mat.name}', this.checked)">
Pode rotacionar
</div>`;
const priceBox = document.createElement('div');
priceBox.className = 'price';
priceBox.innerHTML = `
<div>Preço:</div>
<input type="text" placeholder="R$ 0,00" value="${mat.price || ''}"
onchange="setPrice('${mat.name}', this.value)">
`;
if (priceMode) {
rotateBox.style.display = 'block';
priceBox.style.display = 'block';
}
div.appendChild(thumb);
div.appendChild(label);
div.appendChild(rotateBox);
div.appendChild(priceBox);
if (!priceMode) {
div.onclick = () => sketchup.applyMaterial(mat.path);
}
list.appendChild(div);
});
}
function setRotation(materialName, canRotate) {
if (window.sketchup && sketchup.updateRotation) {
sketchup.updateRotation(materialName, canRotate ? 'true' : 'false');
}
}
function setPrice(materialName, priceValue) {
if (window.sketchup && sketchup.updatePrice) {
sketchup.updatePrice(materialName, priceValue.toString());
}
}
document.getElementById('search').addEventListener('input', function () {
const term = this.value.toLowerCase();
const filtered = allMaterials.filter(m => m.name.toLowerCase().includes(term));
renderMaterials(filtered);
});
window.onload = function () {
if (window.sketchup && sketchup.ready) {
sketchup.ready();
}
};
</script>
</body>
</html>
HTML
dlg.set_html(html)
@selected_type = "TODOS"
dlg.add_action_callback("ready") do |_|
carregar_materiais(dlg)
end
dlg.add_action_callback("getMaterials") do |_|
carregar_materiais(dlg)
end
dlg.add_action_callback("setFilter") do |_context, tipo|
@selected_type = tipo
end
dlg.add_action_callback("applyMaterial") do |_context, skm_path|
begin
model = Sketchup.active_model
material = model.materials.load(skm_path)
filtro = @selected_type || "TODOS"
selection = model.selection.grep(Sketchup::ComponentInstance)
model.start_operation("Aplicar Material", true)
if selection.empty?
model.entities.grep(Sketchup::ComponentInstance).each do |ent|
GCut.apply_material_recursivo(ent, material, filtro)
end
else
selection.each do |ent|
GCut.apply_material_recursivo(ent, material, filtro)
end
end
model.commit_operation
UI.messagebox("✅ Material '#{material.name}' aplicado com sucesso!")
rescue => e
model.abort_operation
UI.messagebox("❌ Erro: #{e.message}")
end
end
dlg.add_action_callback("updateRotation") do |_context, material_name, can_rotate|
update_material_config(material_name, "rotate", can_rotate == 'true')
end
dlg.add_action_callback("updatePrice") do |_context, material_name, price_value|
update_material_config(material_name, "price", price_value.to_s)
end
dlg.show
end
# =========================================================
# JSON de configuração dos materiais (rotate / price)
# =========================================================
def self.update_material_config(material_name, key, value)
material_folder = "C:/LibraryManager/Components/Materiais"
json_path = File.join(material_folder, "materials.json")
config = File.exist?(json_path) ? JSON.parse(File.read(json_path)) : {}
config[material_name] ||= {}
config[material_name][key] = value
File.write(json_path, JSON.pretty_generate(config))
puts "🔁 Atualizado #{key}: #{material_name} → #{value}"
end
def self.carregar_materiais(dlg)
material_folder = "C:/LibraryManager/Components/Materiais"
json_path = File.join(material_folder, "materials.json")
config = File.exist?(json_path) ? JSON.parse(File.read(json_path)) : {}
materiais = []
if Dir.exist?(material_folder)
Dir.entries(material_folder).each do |file|
next unless file.downcase.end_with?(".skm")
path = File.join(material_folder, file)
material_name = File.basename(file, ".skm")
thumb_dir = File.join(material_folder, material_name)
thumb_path = File.join(thumb_dir, "doc_thumbnail.png")
unless File.exist?(thumb_path)
candidatos = Dir.glob(File.join(thumb_dir, "**", "*.{png,jpg,jpeg}"), File::FNM_CASEFOLD)
thumb_path = candidatos.first if candidatos.any?
end
pode_rotacionar = (config.dig(material_name, "rotate") == true)
preco = config.dig(material_name, "price").to_s
materiais << {
name: material_name,
path: path.tr("\\", "/"),
thumbnail: File.exist?(thumb_path) ? thumb_path.tr("\\", "/") : "",
rotate: pode_rotacionar,
price: preco
}
end
end
dlg.execute_script("loadMaterials(#{materiais.to_json})")
end
end
``
```