update some deisng parameters and add new cases

This commit is contained in:
2026-07-29 14:32:22 -07:00
parent 683697604a
commit 45a77885f9
34 changed files with 4602 additions and 79 deletions
+1 -1
View File
@@ -11,6 +11,7 @@ require "yaml"
require "highline"
require_relative "feather_case_wizard/prompting_ui"
require_relative "feather_case_wizard/config"
require_relative "feather_case_wizard/toolchain"
require_relative "feather_case_wizard/package_builder"
@@ -23,4 +24,3 @@ module FeatherCaseWizard
MODEL_SOURCE = ROOT.join("feather_case.scad").freeze
RENDERER_SOURCE = ROOT.join("tools/render_stl_previews.py").freeze
end
+3 -3
View File
@@ -5,18 +5,18 @@ module FeatherCaseWizard
def initialize(ui: HighLine.new, cases_root: CASES_ROOT,
model_source: MODEL_SOURCE, renderer_source: RENDERER_SOURCE,
toolchain: Toolchain.new)
@ui = ui
@ui = PromptingUI.wrap(ui)
@cases_root = Pathname(cases_root)
@model_source = Pathname(model_source)
@renderer_source = Pathname(renderer_source)
@toolchain = toolchain
@wizard = Wizard.new(ui: ui)
@wizard = Wizard.new(ui: @ui)
end
def run
@ui.say(@ui.color("Feather Case Wizard", :bold))
loop do
action = @ui.choose("\nWhat would you like to do?") do |menu|
action = @ui.choose("What would you like to do?") do |menu|
menu.choice("Create a new case") { :create }
menu.choice("Edit an existing case") { :edit }
menu.choice("Rebuild an existing case") { :rebuild }
+84 -9
View File
@@ -14,13 +14,14 @@ module FeatherCaseWizard
DEFAULT_PARAMETERS = {
"top_profile" => "generic",
"wing_count" => 1,
"wing_pitch" => 11.0,
"wing_pitch" => 12.25,
"stack_height_override" => 0.0,
"generic_component_height" => 3.0,
"generic_top_gap" => 2.0,
"gps_antenna_height" => 4.0,
"gps_antenna_height" => 6.0,
"gps_top_gap" => 5.0,
"oled_component_height" => 2.4,
"oled_display_height" => 1.5,
"oled_face_gap" => 0.8,
"button_a" => true,
"button_b" => true,
@@ -62,13 +63,19 @@ module FeatherCaseWizard
"board_bottom_clearance" => 5.4,
"board_side_clearance" => 7.0,
"board_rear_clearance" => 4.0,
"usb_board_wall_gap" => 1.2,
"usb_board_wall_gap" => 2.0,
"lid_lip_depth" => 2.4,
"lid_lip_thickness" => 1.2,
"fit_clearance" => 0.35,
"cutout_min_web" => 1.2,
"hole_compensation" => 0.2,
"lid_screw_head_diameter" => 5.2,
"lid_screw_head_height" => 2.5,
"usb_opening_enabled" => true,
"usb_guide_enabled" => true,
"usb_connector_overhang" => 2.0,
"usb_guide_wall_thickness" => 1.2,
"usb_guide_end_gap" => 1.0,
"usb_opening_width" => 12.5,
"usb_opening_height" => 7.0,
"usb_opening_radius" => 1.5,
@@ -77,11 +84,12 @@ module FeatherCaseWizard
ADVANCED_FIELDS = {
"Stack" => %w[wing_pitch generic_component_height generic_top_gap
gps_antenna_height gps_top_gap oled_component_height oled_face_gap],
gps_antenna_height gps_top_gap oled_component_height
oled_display_height oled_face_gap],
"Shell and fit" => %w[wall_thickness floor_thickness lid_thickness corner_radius
board_bottom_clearance board_side_clearance board_rear_clearance
usb_board_wall_gap lid_lip_depth lid_lip_thickness fit_clearance
hole_compensation lid_screw_head_diameter lid_screw_head_height],
board_bottom_clearance board_side_clearance lid_lip_depth
lid_lip_thickness fit_clearance cutout_min_web hole_compensation
lid_screw_head_diameter lid_screw_head_height],
"M2.5 inserts" => %w[m25_insert_length m25_insert_hole_diameter
m25_screw_clearance m25_boss_diameter
insert_entry_chamfer insert_melt_relief],
@@ -93,7 +101,8 @@ module FeatherCaseWizard
switch_flange_diameter switch_body_length
switch_body_diameter switch_wire_allowance],
"USB-C opening" => %w[usb_opening_width usb_opening_height
usb_opening_radius usb_vertical_offset],
usb_opening_radius usb_vertical_offset usb_connector_overhang
usb_guide_wall_thickness usb_guide_end_gap],
"OLED buttons" => %w[button_projection button_flange_thickness
button_travel button_contact_extension]
}.freeze
@@ -264,6 +273,9 @@ module FeatherCaseWizard
unless (outputs["coupons"] - COUPONS).empty?
raise ConfigError, "Unsupported calibration coupon"
end
if parameters["usb_guide_enabled"] && !parameters["usb_opening_enabled"]
raise ConfigError, "USB guide requires the USB opening"
end
positive = DEFAULT_PARAMETERS.keys - %w[usb_vertical_offset stack_height_override]
positive.each do |key|
@@ -289,7 +301,10 @@ module FeatherCaseWizard
def normalize!
data["schema_version"] = Integer(data.fetch("schema_version", SCHEMA_VERSION))
data["generator"] ||= {}
data["parameters"] = self.class.deep_copy(DEFAULT_PARAMETERS).merge(data["parameters"] || {})
supplied_parameters = data["parameters"] || {}
legacy_usb_guide = !supplied_parameters.key?("usb_guide_enabled")
data["parameters"] = self.class.deep_copy(DEFAULT_PARAMETERS).merge(supplied_parameters)
data["parameters"]["usb_guide_enabled"] = false if legacy_usb_guide
data["cutouts"] ||= []
data["outputs"] ||= {}
data["outputs"]["formats"] ||= ["3mf"]
@@ -302,6 +317,7 @@ module FeatherCaseWizard
def validate_cutouts!
dims = dimensions
names = {}
bounds = []
cutouts.each do |cutout|
name = cutout["name"].to_s.strip
raise ConfigError, "Every cutout needs a name" if name.empty?
@@ -328,9 +344,68 @@ module FeatherCaseWizard
radius = Float(cutout.fetch("radius", [width, height].min / 2))
raise ConfigError, "#{name} dimensions must be positive" unless
width.positive? && height.positive? && radius.positive?
bounds << {
"name" => name, "face" => face,
"left" => horizontal - width / 2, "right" => horizontal + width / 2,
"bottom" => z - height / 2, "top" => z + height / 2
}
rescue ArgumentError, TypeError, KeyError => e
raise ConfigError, "Invalid cutout #{name.inspect}: #{e.message}"
end
validate_cutout_spacing!(bounds, dims)
end
def validate_cutout_spacing!(bounds, dims)
web = parameters["cutout_min_web"]
bounds.combination(2) do |first, second|
next unless first["face"] == second["face"]
next if bounds_separated?(first, second, web)
raise ConfigError,
"#{first["name"]} and #{second["name"]} are too close on the #{first["face"]} wall"
end
return unless parameters["usb_opening_enabled"]
usb = usb_wall_bounds(dims)
bounds.select { |item| item["face"] == "front" }.each do |cutout|
next if bounds_separated?(cutout, usb, web)
raise ConfigError, "#{cutout["name"]} is too close to the USB opening or guide"
end
end
def bounds_separated?(first, second, web)
first["right"] + web <= second["left"] ||
second["right"] + web <= first["left"] ||
first["top"] + web <= second["bottom"] ||
second["top"] + web <= first["bottom"]
end
def usb_wall_bounds(dims)
p = parameters
effective_floor = p["tripod_insert_enabled"] ?
[p["floor_thickness"], p["tripod_insert_length"] + p["insert_melt_relief"] + 1.2].max :
p["floor_thickness"]
effective_board_clearance = [
p["board_bottom_clearance"],
p["m25_insert_length"] + p["insert_melt_relief"] + 0.6
].max
board_top_z = effective_floor + effective_board_clearance + 1.6
global_y = p["wall_thickness"] + p["board_side_clearance"] + 11.43
horizontal = dims["width"] - global_y
z = board_top_z + 1.6 + p["usb_vertical_offset"]
guide_length = [
p["usb_board_wall_gap"] - p["usb_connector_overhang"] - p["usb_guide_end_gap"], 0
].max
guide_active = p["usb_guide_enabled"] && guide_length.positive?
shell = guide_active ? 2 * p["usb_guide_wall_thickness"] : 0
width = p["usb_opening_width"] + p["hole_compensation"] + shell
height = p["usb_opening_height"] + p["hole_compensation"] + shell
{
"name" => "USB opening", "face" => "front",
"left" => horizontal - width / 2, "right" => horizontal + width / 2,
"bottom" => z - height / 2, "top" => z + height / 2
}
end
def stringify_keys(value)
+46
View File
@@ -0,0 +1,46 @@
# frozen_string_literal: true
module FeatherCaseWizard
class PromptingUI
def self.wrap(ui)
ui.is_a?(self) ? ui : new(ui)
end
def initialize(ui)
@ui = ui
end
def ask(*arguments, &block)
separate_after { @ui.ask(*arguments, &block) }
end
def agree(*arguments, &block)
separate_after { @ui.agree(*arguments, &block) }
end
def choose(prompt, &block)
separate_after do
@ui.choose do |menu|
menu.header = prompt.to_s.sub(/[?:]\s*\z/, "")
block.call(menu)
end
end
end
def say(*arguments, &block)
@ui.say(*arguments, &block)
end
def color(*arguments, &block)
@ui.color(*arguments, &block)
end
private
def separate_after
result = yield
@ui.say("\n")
result
end
end
end
+50 -14
View File
@@ -5,7 +5,7 @@ module FeatherCaseWizard
attr_reader :ui
def initialize(ui: HighLine.new)
@ui = ui
@ui = PromptingUI.wrap(ui)
end
def create
@@ -15,14 +15,14 @@ module FeatherCaseWizard
end
def edit(config)
ui.say("\nEditing #{config.name} (`#{config.slug}`)")
ui.say("Editing #{config.name} (`#{config.slug}`)")
config.data["name"] = text("Case name", config.name)
configure(config, new_case: false)
end
def confirm_build(config)
dims = config.dimensions
ui.say("\n#{ui.color("Build summary", :bold)}")
ui.say(ui.color("Build summary", :bold))
ui.say(" Case: #{config.name} (#{config.slug})")
ui.say(" Profile: #{config.parameters["top_profile"]}")
ui.say(" Wings: #{config.parameters["wing_count"]}")
@@ -42,6 +42,7 @@ module FeatherCaseWizard
choose_formats(config)
choose_profile(config)
choose_stack(config)
choose_end_clearance(config)
choose_features(config)
manage_cutouts(config)
choose_coupons(config)
@@ -63,7 +64,7 @@ module FeatherCaseWizard
menu.choice("3MF") { ["3mf"] }
menu.choice("STL") { ["stl"] }
menu.choice("Both 3MF and STL") { %w[3mf stl] }
menu.default = label == "both" ? 3 : (label == "stl" ? 2 : 1)
menu.default = (label == "both" ? 3 : (label == "stl" ? 2 : 1)).to_s
end
config.outputs["formats"] = selected
end
@@ -74,7 +75,7 @@ module FeatherCaseWizard
menu.choice("Generic / other") { "generic" }
menu.choice("GPS") { "gps" }
menu.choice("128×64 OLED") { "oled128x64" }
menu.default = {"generic" => 1, "gps" => 2, "oled128x64" => 3}.fetch(current)
menu.default = {"generic" => 1, "gps" => 2, "oled128x64" => 3}.fetch(current).to_s
end
end
@@ -82,11 +83,11 @@ module FeatherCaseWizard
p = config.parameters
p["wing_count"] = integer("Number of FeatherWings", p["wing_count"], 1..8)
measured = p["stack_height_override"].positive?
if ui.agree("Use a measured board-to-top-PCB stack height? (y/n) ") do |question|
if ui.agree("Use a measured Feather-PCB-top to top-Wing-PCB stack height? (y/n) ") do |question|
question.default = measured ? "yes" : "no"
end
p["stack_height_override"] = number(
"Measured height in mm", measured ? p["stack_height_override"] :
"PCB-top to PCB-top height in mm", measured ? p["stack_height_override"] :
p["wing_count"] * p["wing_pitch"], min: 0.1
)
else
@@ -96,22 +97,55 @@ module FeatherCaseWizard
def choose_features(config)
p = config.parameters
p["usb_opening_enabled"] = agreement(
"Include front USB-C programming opening?", p["usb_opening_enabled"]
)
p["usb_guide_enabled"] = if p["usb_opening_enabled"]
agreement("Include inward USB plug guide?",
p["usb_guide_enabled"])
else
false
end
p["battery_enabled"] = agreement("Include 18650 battery compartment and switch?",
p["battery_enabled"])
p["tripod_insert_enabled"] = agreement("Include centered 1/4-20 insert?",
p["tripod_insert_enabled"])
return unless p["top_profile"] == "oled128x64"
ui.say("\nOLED captive buttons")
ui.say("OLED captive buttons")
p["button_a"] = agreement("Enable button A?", p["button_a"])
p["button_b"] = agreement("Enable button B?", p["button_b"])
p["button_c"] = agreement("Enable button C?", p["button_c"])
p["button_reset"] = agreement("Enable reset button?", p["button_reset"])
end
def choose_end_clearance(config)
p = config.parameters
defaults = Config::DEFAULT_PARAMETERS
custom = p["usb_board_wall_gap"] != defaults["usb_board_wall_gap"] ||
p["board_rear_clearance"] != defaults["board_rear_clearance"]
enabled = ui.agree("Use extra clearance beyond the Feather PCB ends? (y/n) ") do |question|
question.default = custom ? "yes" : "no"
end
unless enabled
p["usb_board_wall_gap"] = defaults["usb_board_wall_gap"]
p["board_rear_clearance"] = defaults["board_rear_clearance"]
return
end
p["usb_board_wall_gap"] = number(
"USB/front PCB-edge to inner-wall clearance (mm)",
p["usb_board_wall_gap"], min: 0.1
)
p["board_rear_clearance"] = number(
"Rear PCB-edge to inner-wall clearance (mm)",
p["board_rear_clearance"], min: 0.1
)
end
def manage_cutouts(config)
loop do
ui.say("\nCustom cable cutouts: #{config.cutouts.empty? ? "none" :
ui.say("Custom cable cutouts: #{config.cutouts.empty? ? "none" :
config.cutouts.map { |cutout| cutout["name"] }.join(", ")}")
action = ui.choose("Cable cutouts") do |menu|
menu.choice("Done") { :done }
@@ -141,12 +175,15 @@ module FeatherCaseWizard
dims = config.dimensions
name = text("Cutout name", existing["name"] || "cable")
face = ui.choose("Wall") do |menu|
Config::FACES.each { |item| menu.choice(item) { item } }
menu.default = Config::FACES.index(existing["face"]).to_i + 1 if existing["face"]
menu.choice("Front — USB-C short wall") { "front" }
menu.choice("Rear — opposite short wall") { "rear" }
menu.choice("Left — plain/non-JST long wall") { "left" }
menu.choice("Right — JST/battery long wall") { "right" }
menu.default = (Config::FACES.index(existing["face"]).to_i + 1).to_s if existing["face"]
end
shape = ui.choose("Shape") do |menu|
Config::SHAPES.each { |item| menu.choice(item) { item } }
menu.default = Config::SHAPES.index(existing["shape"]).to_i + 1 if existing["shape"]
menu.default = (Config::SHAPES.index(existing["shape"]).to_i + 1).to_s if existing["shape"]
end
wall_width = %w[front rear].include?(face) ? dims["width"] : dims["length"]
ui.say("Viewing the #{face} wall from outside: width #{format("%.1f", wall_width)} mm; " \
@@ -209,7 +246,7 @@ module FeatherCaseWizard
Config::ADVANCED_FIELDS.each do |section, fields|
next unless ui.agree("Adjust #{section.downcase}? (y/n) ") { |q| q.default = "no" }
ui.say("\n#{section}")
ui.say(section)
fields.each do |field|
config.parameters[field] = number(field.tr("_", " "), config.parameters[field],
min: field == "usb_vertical_offset" ? nil : 0.01)
@@ -286,4 +323,3 @@ module FeatherCaseWizard
def yes_no(value) = value ? "yes" : "no"
end
end