Files
3d_globe/lib/moon_model/config.rb
T
2026-09-07 12:11:43 -07:00

112 lines
4.2 KiB
Ruby

# frozen_string_literal: true
module MoonModel
class Config
MOON_DATUM_DIAMETER_MM = 3_474_800_000.0
X2D = {
"name" => "Bambu Lab X2D",
"tooling" => "both_nozzles",
"build_volume_mm" => [235.5, 256.0, 256.0],
"main_only_volume_mm" => [256.0, 256.0, 260.0]
}.freeze
DEFAULTS = {
"printer" => X2D,
"nozzle_mm" => 0.4,
"edge_clearance_mm" => 2.0,
"purpose" => "display",
"assembly" => "auto",
"size" => { "input" => "diameter", "value" => 120.0, "unit" => "mm", "meaning" => "final_envelope" },
"vertical_exaggeration" => 1.0,
"palette" => [{ "name" => "Moon gray", "hex" => "#A9A9A9" }],
"grid" => { "enabled" => false, "interval_degrees" => 30.0 },
"shell" => {},
"magnets" => { "diameter_mm" => 6.0, "thickness_mm" => 3.0, "count" => 6 },
"stand" => { "enabled" => false, "style" => "orbital_triskelion", "base_clearance_mm" => 10.0, "material" => nil },
"resolution" => { "longitude_segments" => nil },
"output" => { "name" => "moon" }
}.freeze
attr_reader :data
def initialize(values = {})
@data = deep_merge(Marshal.load(Marshal.dump(DEFAULTS)), stringify(values))
derive!
validate!
end
def [](key) = data.fetch(key.to_s)
def to_h = Marshal.load(Marshal.dump(data))
def save(path)
File.write(path, YAML.dump(to_h))
end
def diameter_mm
size = self["size"]
return MOON_DATUM_DIAMETER_MM / Float(size["value"]) if size["input"] == "scale"
value = Float(size["value"])
size["unit"] == "in" ? value * 25.4 : value
end
def build_volume
self["printer"]["build_volume_mm"].map(&:to_f)
end
def segmented?
self["purpose"] == "box" || self["assembly"] == "eight_piece" ||
(self["assembly"] == "auto" && diameter_mm > maximum_diameter("one_piece"))
end
def maximum_diameter(mode)
usable = build_volume.map { |v| v - 2.0 * self["edge_clearance_mm"] }
mode == "one_piece" ? usable.min : 2.0 * usable.min
end
private
def derive!
nozzle = Float(data["nozzle_mm"])
shell = data["shell"]
shell["interior"] ||= "solid"
shell["wall_mm"] ||= 4.0 * nozzle
shell["inlay_depth_mm"] ||= 2.0 * nozzle
shell["minimum_feature_mm"] ||= 2.0 * nozzle
shell["joint_clearance_mm"] ||= [0.2, 0.5 * nozzle].max
data["grid"]["line_width_mm"] ||= 2.0 * nozzle
data["grid"]["depth_mm"] ||= nozzle
data["size"]["meaning"] = "datum" if data["size"]["input"] == "scale"
end
def validate!
raise ArgumentError, "nozzle must be positive" unless self["nozzle_mm"].to_f.positive?
raise ArgumentError, "build volume requires three positive dimensions" unless build_volume.length == 3 && build_volume.all?(&:positive?)
raise ArgumentError, "purpose must be display or box" unless %w[display box].include?(self["purpose"])
raise ArgumentError, "assembly must be auto, one_piece, or eight_piece" unless %w[auto one_piece eight_piece].include?(self["assembly"])
raise ArgumentError, "shell interior must be solid or hollow" unless %w[solid hollow].include?(self["shell"]["interior"])
raise ArgumentError, "unsupported stand style" unless self["stand"]["style"] == "orbital_triskelion"
raise ArgumentError, "stand base clearance must be nonnegative" if Float(self["stand"]["base_clearance_mm"]).negative?
raise ArgumentError, "palette must contain at least one color" if self["palette"].empty?
self["palette"].each do |color|
raise ArgumentError, "invalid color #{color.inspect}" unless color["name"].to_s.match?(/\S/) && color["hex"].to_s.match?(/\A#[0-9a-fA-F]{6}\z/)
end
raise ArgumentError, "diameter/scale must be positive" unless diameter_mm.positive?
raise ArgumentError, "vertical exaggeration must be nonnegative" if self["vertical_exaggeration"].to_f.negative?
end
def deep_merge(left, right)
left.merge(right) { |_key, a, b| a.is_a?(Hash) && b.is_a?(Hash) ? deep_merge(a, b) : b }
end
def stringify(value)
case value
when Hash then value.to_h { |key, val| [key.to_s, stringify(val)] }
when Array then value.map { |val| stringify(val) }
else value
end
end
end
end