149 lines
5.0 KiB
Ruby
Executable File
149 lines
5.0 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require "fileutils"
|
|
require "json"
|
|
require "open3"
|
|
require "pathname"
|
|
require "tmpdir"
|
|
require "yaml"
|
|
|
|
ROOT = Pathname(__dir__).expand_path
|
|
|
|
def executable(env_name, command)
|
|
override = ENV[env_name]
|
|
return File.expand_path(override) if override && File.executable?(override)
|
|
|
|
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
|
|
candidate = File.join(directory, command)
|
|
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
end
|
|
nil
|
|
end
|
|
|
|
def literal(value)
|
|
case value
|
|
when String then JSON.generate(value)
|
|
when TrueClass then "true"
|
|
when FalseClass then "false"
|
|
when Numeric then value.to_s
|
|
when Array then "[#{value.map { |item| literal(item) }.join(",")}]"
|
|
else raise "Cannot convert #{value.inspect} to OpenSCAD"
|
|
end
|
|
end
|
|
|
|
def run!(*command, env: {})
|
|
stdout, stderr, status = Open3.capture3(env, *command)
|
|
return if status.success?
|
|
|
|
warn [stdout, stderr].reject(&:empty?).join("\n")
|
|
raise "Command failed (#{status.exitstatus}): #{command.first}"
|
|
end
|
|
|
|
config = YAML.safe_load_file(ROOT.join("case.yml"), permitted_classes: [],
|
|
permitted_symbols: [], aliases: false)
|
|
openscad = executable("OPENSCAD", "openscad")
|
|
blender = executable("BLENDER", "blender")
|
|
python = executable("PYTHON", "python3")
|
|
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
|
abort "Blender not found; install it or set BLENDER" unless blender
|
|
|
|
parameters = config.fetch("parameters").merge(
|
|
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
|
)
|
|
artwork = !parameters.fetch("lid_label", "").empty? ||
|
|
parameters.fetch("lid_logo_enabled", false)
|
|
abort "Python 3 not found; install it or set PYTHON" if artwork && !python
|
|
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
|
parts.concat(%w[lid_base lid_accent]) if artwork
|
|
buttons = %w[button_a button_b button_c button_reset]
|
|
if parameters["top_profile"] == "oled128x64" &&
|
|
buttons.any? { |name| parameters[name] }
|
|
parts << "buttons"
|
|
end
|
|
|
|
stage = Pathname(Dir.mktmpdir(".rebuild-", ROOT.to_s))
|
|
begin
|
|
exports = stage.join("exports")
|
|
previews = stage.join("previews")
|
|
scratch = stage.join("meshes")
|
|
FileUtils.mkdir_p([exports, previews, scratch])
|
|
model = ROOT.join("source/feather_case.scad")
|
|
renderer = ROOT.join("source/render_stl_previews.py")
|
|
|
|
export = lambda do |output, part, overrides = {}|
|
|
values = parameters.merge(overrides).merge("part" => part)
|
|
command = [openscad, "-o", output.to_s]
|
|
values.each { |name, value| command.concat(["-D", "#{name}=#{literal(value)}"]) }
|
|
run!(*command, model.to_s)
|
|
end
|
|
|
|
config.fetch("outputs").fetch("formats").each do |format|
|
|
directory = exports.join(format)
|
|
FileUtils.mkdir_p(directory)
|
|
parts.each do |part|
|
|
puts "Exporting #{part}.#{format}…"
|
|
export.call(directory.join("#{part}.#{format}"), part)
|
|
end
|
|
if artwork && format == "3mf"
|
|
puts "Packaging lid_multicolor.3mf…"
|
|
run!(
|
|
python, ROOT.join("tools/package_multimaterial_3mf.py").to_s,
|
|
directory.join("lid_base.3mf").to_s,
|
|
directory.join("lid_accent.3mf").to_s,
|
|
directory.join("lid_multicolor.3mf").to_s,
|
|
"--title", "#{config.fetch("name")} Lid"
|
|
)
|
|
end
|
|
end
|
|
|
|
assembly = scratch.join("assembly.stl")
|
|
puts "Preparing assembly preview…"
|
|
export.call(assembly, "assembly",
|
|
"exploded_view" => config.fetch("outputs").fetch("assembly_exploded_view"),
|
|
"show_references" => false)
|
|
render_env = {"DISPLAY" => nil, "WAYLAND_DISPLAY" => nil}
|
|
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
|
assembly.to_s, previews.to_s, "assembly", env: render_env)
|
|
parts.each do |part|
|
|
puts "Rendering #{part} preview…"
|
|
mesh = scratch.join("#{part}.stl")
|
|
export.call(mesh, part) unless mesh.file?
|
|
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
|
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
|
end
|
|
if artwork
|
|
puts "Rendering multicolor lid preview…"
|
|
run!(
|
|
blender, "--background", "-noaudio", "--python",
|
|
ROOT.join("source/render_multicolor_lid.py").to_s, "--",
|
|
scratch.join("lid_base.stl").to_s,
|
|
scratch.join("lid_accent.stl").to_s,
|
|
previews.to_s, "lid_multicolor", parameters["lid_art_orientation"], env: render_env
|
|
)
|
|
end
|
|
|
|
backup = ROOT.join(".build-old-#{Process.pid}")
|
|
FileUtils.mkdir_p(backup)
|
|
%w[exports previews].each do |name|
|
|
current = ROOT.join(name)
|
|
FileUtils.mv(current, backup.join(name)) if current.exist?
|
|
end
|
|
begin
|
|
FileUtils.mv(exports, ROOT.join("exports"))
|
|
FileUtils.mv(previews, ROOT.join("previews"))
|
|
FileUtils.rm_rf(backup)
|
|
rescue StandardError
|
|
FileUtils.rm_rf(ROOT.join("exports"))
|
|
FileUtils.rm_rf(ROOT.join("previews"))
|
|
%w[exports previews].each do |name|
|
|
saved = backup.join(name)
|
|
FileUtils.mv(saved, ROOT.join(name)) if saved.exist?
|
|
end
|
|
raise
|
|
end
|
|
puts "Rebuilt #{ROOT}"
|
|
ensure
|
|
FileUtils.rm_rf(stage)
|
|
end
|