From 6f15d6ff24f5a1db047ae55276353dea409d7bd6 Mon Sep 17 00:00:00 2001 From: Snowsune Date: Fri, 2 Jan 2026 19:21:40 -0500 Subject: [PATCH 01/15] Implement inital prototype with Vol's formula --- .gitignore | 1 + app/__init__.py | 117 +++++++++- app/species_data/african_buffalo.yaml | 6 + app/species_data/arctic_fox.yaml | 6 + app/species_data/canine.yaml | 6 + app/species_data/equine.yaml | 6 + app/species_data/feline.yaml | 6 + app/species_data/fennec_fox.yaml | 6 + app/species_data/giraffe.yaml | 6 + app/species_data/mouse.yaml | 6 + app/species_data/red_fox.yaml | 6 + app/species_data/saber-toothed_tiger.yaml | 6 + app/species_data/shark.yaml | 6 + app/species_data/wolf.yaml | 6 + app/static/css/taur_app.css | 150 ++++++++++++ .../images/taur/TaurCalculatorReference.png | Bin 0 -> 43238 bytes app/templates/taur.html | 219 +++++++++++++++++- app/utils/taur_calculator.py | 98 ++++++++ 18 files changed, 643 insertions(+), 14 deletions(-) create mode 100644 app/static/css/taur_app.css create mode 100644 app/static/images/taur/TaurCalculatorReference.png create mode 100644 app/utils/taur_calculator.py diff --git a/.gitignore b/.gitignore index 269d87a..3ab4911 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ cache .env *.sqlite3 art/dist/ +__pycache__ diff --git a/app/__init__.py b/app/__init__.py index e2d203a..65e92de 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -22,7 +22,11 @@ from functools import wraps from app.utils.species_lookup import load_species_data -from app.utils.calculate_heights import calculate_height_offset, convert_to_inches +from app.utils.calculate_heights import ( + calculate_height_offset, + convert_to_inches, + inches_to_feet_inches, +) from app.utils.parse_data import ( extract_characters, filter_valid_characters, @@ -34,6 +38,7 @@ from app.utils.stats import StatsManager from app.utils.generate_image import render_image from app.utils.character import Character +from app.utils.taur_calculator import calculate_taur app = Flask(__name__) app.secret_key = os.urandom(24) @@ -300,13 +305,117 @@ def add_preset(): return redirect(f"/?characters={characters_query}{settings_query}") -@app.route("/taur") +@app.route("/taur", methods=["GET", "POST"]) def taur(): """ - Base route for volnar's sub-page! + Taur calculator! + + Collaboration with Volnar <3 """ + # Filter out some ones we dont want to show/dont have data + filtered_species = [ + s + for s in species_list + if s not in ["taur_(generic)", "preset_species", "rexouium"] + ] + + # Load species data for auto-population + species_data_map = {} + for species_name in filtered_species: + try: + data = load_species_data(species_name) + # Extract species_length, species_tail_length, species_weight from male section + # and get a default species_height from the first data point + if "male" in data: + male_data = data["male"] + species_data_map[species_name] = { + "species_length": male_data.get("species_length", 0), + "species_tail_length": male_data.get("species_tail_length", 0), + "species_weight": male_data.get("species_weight", 0), + "species_height": ( + male_data.get("data", [{}])[0].get("height", 0) + if male_data.get("data") + else 0 + ), + } + except Exception as e: + logging.warning(f"Failed to load species data for {species_name}: {e}") + species_data_map[species_name] = { + "species_length": 0, + "species_tail_length": 0, + "species_weight": 0, + "species_height": 0, + } + + if request.method == "POST": + # POST when handling a form submission - redirect with URL parameters + params = {} + for key in [ + "name", + "measurement_type", + "anthro_height", + "species_height", + "species_length", + "species_tail_length", + "taur_full_height", + "species_weight", + "taur_length", + ]: + value = request.form.get(key, "") + if value: + params[key] = value + + query_string = "&".join([f"{k}={v}" for k, v in params.items()]) + return redirect(f"/taur?{query_string}") + + # Handle GET with calculation parameters + calculation_result = None + cleaned_calculation_result = {} + if request.args.get("anthro_height"): + try: + anthro_height = float(request.args.get("anthro_height", 0)) + species_height = float(request.args.get("species_height", 0)) + species_length = float(request.args.get("species_length", 0)) + species_tail_length = float(request.args.get("species_tail_length", 0)) + taur_full_height = float(request.args.get("taur_full_height", 0)) + species_weight = float(request.args.get("species_weight", 0)) + taur_length = request.args.get("taur_length") + measurement_type = request.args.get("measurement_type", "vitruvian") + + taur_length_float = float(taur_length) if taur_length else None + + calculation_result = calculate_taur( + anthro_height=anthro_height, + species_height=species_height, + species_length=species_length, + species_tail_length=species_tail_length, + taur_full_height=taur_full_height, + species_weight=species_weight, + taur_length=taur_length_float, + measurement_type=measurement_type, + ) - return render_template("taur.html") + cleaned_calculation_result["AR"] = f"{inches_to_feet_inches(calculation_result["AR"])} (Anthropic Ratio)" + cleaned_calculation_result["TH"] = f"{inches_to_feet_inches(calculation_result["TH"])} (Taur Height)" + cleaned_calculation_result["TFH"] = f"{inches_to_feet_inches(calculation_result["TFH"])} (Taur Full Height)" + cleaned_calculation_result["TL"] = f"{inches_to_feet_inches(calculation_result["TL"])} (Taur Length)" + cleaned_calculation_result["TT"] = f"{inches_to_feet_inches(calculation_result["TT"])} (Taur Tail Length)" + cleaned_calculation_result["TTo"] = f"{inches_to_feet_inches(calculation_result["TTo"])} (Taur Torso Length)" + cleaned_calculation_result["THe"] = f"{inches_to_feet_inches(calculation_result["THe"])} (Taur Head Length)" + cleaned_calculation_result["TW"] = f"{calculation_result["TW"]:.2f} lbs (Taur Weight)" + + except (ValueError, TypeError) as e: + logging.warning(f"Taur calculation error: {e}") + calculation_result = None + cleaned_calculation_result = None + + return render_template( + "taur.html", + species=filtered_species, + species_data=species_data_map, + calculation_result=cleaned_calculation_result, + form_data=dict(request.args) if request.args else None, + ) # For WSGI diff --git a/app/species_data/african_buffalo.yaml b/app/species_data/african_buffalo.yaml index e9dccc2..f5d44fe 100644 --- a/app/species_data/african_buffalo.yaml +++ b/app/species_data/african_buffalo.yaml @@ -1,6 +1,9 @@ male: image: "Chrissy/missing.png" ears_offset: 0 + species_length: 96 + species_tail_length: 30 + species_weight: 1500 # Pounds data: - anthro_size: 76 # 6'4" height: 60 @@ -10,6 +13,9 @@ male: female: image: "Chrissy/missing.png" ears_offset: 0 + species_length: 96 + species_tail_length: 30 + species_weight: 1500 # Pounds data: - anthro_size: 74 # 6'2" height: 54 diff --git a/app/species_data/arctic_fox.yaml b/app/species_data/arctic_fox.yaml index b2cdbc7..cc5e348 100644 --- a/app/species_data/arctic_fox.yaml +++ b/app/species_data/arctic_fox.yaml @@ -1,6 +1,9 @@ male: image: "Chrissy/randal.png" ears_offset: 4 + species_length: 28 + species_tail_length: 16 + species_weight: 12 # Pounds data: - anthro_size: 76 # 6'4" height: 22 # 22" @@ -10,6 +13,9 @@ male: female: image: "Chrissy/vixi.png" ears_offset: 4 + species_length: 28 + species_tail_length: 16 + species_weight: 12 # Pounds data: - anthro_size: 74 # 6'2" height: 20 # 22" diff --git a/app/species_data/canine.yaml b/app/species_data/canine.yaml index 0b8e618..130756a 100644 --- a/app/species_data/canine.yaml +++ b/app/species_data/canine.yaml @@ -2,6 +2,9 @@ male: image: "Hunner/Hieght_Ref_Maxene_for_Vixi.png" ears_offset: 3 color: "870716" + species_length: 36 + species_tail_length: 18 + species_weight: 80 # Pounds data: - anthro_size: 76 # 6'4" height: 28 @@ -12,6 +15,9 @@ female: image: "Hunner/Hieght_Ref_Ky-Li_for_Vixi.png" ears_offset: 9 color: "add6ed" + species_length: 36 + species_tail_length: 18 + species_weight: 80 # Pounds data: - anthro_size: 76 # 6'4" height: 26 diff --git a/app/species_data/equine.yaml b/app/species_data/equine.yaml index 6883e3a..19604c2 100644 --- a/app/species_data/equine.yaml +++ b/app/species_data/equine.yaml @@ -1,6 +1,9 @@ male: image: "Placeholder/f_equine.png" ears_offset: 0 + species_length: 72 + species_tail_length: 24 + species_weight: 1000 # Pounds data: - anthro_size: 76 # 6'4" height: 72 @@ -10,6 +13,9 @@ male: female: image: "Placeholder/f_equine.png" ears_offset: 0 + species_length: 72 + species_tail_length: 24 + species_weight: 1000 # Pounds data: - anthro_size: 76 # 6'4" height: 70 diff --git a/app/species_data/feline.yaml b/app/species_data/feline.yaml index 238de43..7c13215 100644 --- a/app/species_data/feline.yaml +++ b/app/species_data/feline.yaml @@ -2,6 +2,9 @@ male: image: "Rhainbowmetall/Felid_Sketch_Male.png" ears_offset: 9 color: "282b1d" + species_length: 30 + species_tail_length: 12 + species_weight: 15 # Pounds data: - anthro_size: 76 # 6'4" height: 14 @@ -12,6 +15,9 @@ female: image: "Rhainbowmetall/Felid_Sketch_Female.png" ears_offset: 9 color: "1d751d" + species_length: 30 + species_tail_length: 12 + species_weight: 15 # Pounds data: - anthro_size: 74 # 6'2" height: 11 diff --git a/app/species_data/fennec_fox.yaml b/app/species_data/fennec_fox.yaml index 0acb5e0..cac335a 100644 --- a/app/species_data/fennec_fox.yaml +++ b/app/species_data/fennec_fox.yaml @@ -1,6 +1,9 @@ male: image: "Chrissy/m_fennec.png" ears_offset: 10 + species_length: 20 + species_tail_length: 15 + species_weight: 3 # Pounds data: - anthro_size: 76 # 6'4" height: 11 @@ -10,6 +13,9 @@ male: female: image: "Chrissy/missing.png" ears_offset: 0 + species_length: 20 + species_tail_length: 14 + species_weight: 3 # Pounds data: - anthro_size: 74 # 6'2" height: 10 diff --git a/app/species_data/giraffe.yaml b/app/species_data/giraffe.yaml index ff95520..d5f59be 100644 --- a/app/species_data/giraffe.yaml +++ b/app/species_data/giraffe.yaml @@ -1,6 +1,9 @@ male: image: "Placeholder/m_giraffe.png" ears_offset: 0 + species_length: 120 + species_tail_length: 36 + species_weight: 3000 # Pounds data: - anthro_size: 76 # 6'4" height: 220 @@ -10,6 +13,9 @@ male: female: image: "Placeholder/m_giraffe.png" ears_offset: 0 + species_length: 120 + species_tail_length: 36 + species_weight: 2600 # Pounds data: - anthro_size: 76 # 6'4" height: 200 diff --git a/app/species_data/mouse.yaml b/app/species_data/mouse.yaml index bc8f116..1e1cca6 100644 --- a/app/species_data/mouse.yaml +++ b/app/species_data/mouse.yaml @@ -2,6 +2,9 @@ male: image: "Hunner/Size_Refs_Mouse_for_Vixi (male).png" ears_offset: 6 color: "007516" + species_length: 4 + species_tail_length: 3 + species_weight: 0.5 # Pounds data: - anthro_size: 76 # 6'4" height: 3 @@ -12,6 +15,9 @@ female: image: "Hunner/Size_Refs_Mouse_for_Vixi (female).png" ears_offset: 7 color: "7900ad" + species_length: 4 + species_tail_length: 3 + species_weight: 0.5 # Pounds data: - anthro_size: 74 # 6'2" height: 2.5 diff --git a/app/species_data/red_fox.yaml b/app/species_data/red_fox.yaml index afe5cec..db3fe8a 100644 --- a/app/species_data/red_fox.yaml +++ b/app/species_data/red_fox.yaml @@ -1,6 +1,9 @@ male: image: "Chrissy/randal.png" ears_offset: 4 + species_length: 28 + species_tail_length: 16 + species_weight: 12 # Pounds data: - anthro_size: 76 # 6'4" height: 17 @@ -10,6 +13,9 @@ male: female: image: "Chrissy/sinopa.png" ears_offset: 1.5 + species_length: 28 + species_tail_length: 16 + species_weight: 12 # Pounds data: - anthro_size: 76 # 6'4" height: 15 diff --git a/app/species_data/saber-toothed_tiger.yaml b/app/species_data/saber-toothed_tiger.yaml index 62dc89c..821a244 100644 --- a/app/species_data/saber-toothed_tiger.yaml +++ b/app/species_data/saber-toothed_tiger.yaml @@ -2,6 +2,9 @@ male: image: "Hunner/Hieght_Ref_NO_BAK_Tirga_for_Tirga.png" ears_offset: 3 color: "00730b" + species_length: 60 + species_tail_length: 30 + species_weight: 400 # Pounds data: - anthro_size: 76 # 6'4" height: 44 @@ -12,6 +15,9 @@ female: image: "Hunner/Hieght_Ref_NO_BAK_Tirga_for_Tirga.png" ears_offset: 3 color: "b216f0" + species_length: 60 + species_tail_length: 30 + species_weight: 400 # Pounds data: - anthro_size: 74 # 6'2" height: 46 diff --git a/app/species_data/shark.yaml b/app/species_data/shark.yaml index 0d2f8b7..bafa677 100644 --- a/app/species_data/shark.yaml +++ b/app/species_data/shark.yaml @@ -3,6 +3,9 @@ male: image: "Mori/m_shark.png" ears_offset: 3 # color: "311b9e" + species_length: 72 + species_tail_length: 36 + species_weight: 500 # Pounds data: - anthro_size: 76 # 6'4" height: 90 @@ -13,6 +16,9 @@ female: image: "Mori/f_shark.png" ears_offset: 3 # color: "664ce6" + species_length: 72 + species_tail_length: 36 + species_weight: 500 # Pounds data: - anthro_size: 74 # 6'2" height: 92 diff --git a/app/species_data/wolf.yaml b/app/species_data/wolf.yaml index 3dcb731..f9de2b2 100644 --- a/app/species_data/wolf.yaml +++ b/app/species_data/wolf.yaml @@ -1,6 +1,9 @@ male: image: "Placeholder/m_wolf.png" ears_offset: 0 + species_length: 40 + species_tail_length: 20 + species_weight: 100 # Pounds data: - anthro_size: 76 # 6'4" height: 36 @@ -10,6 +13,9 @@ male: female: image: "Hunner/Hieght_Ref_Ky-Li_for_Vixi.png" ears_offset: 10 + species_length: 40 + species_tail_length: 20 + species_weight: 100 # Pounds data: - anthro_size: 76 # 6'4" height: 34 diff --git a/app/static/css/taur_app.css b/app/static/css/taur_app.css new file mode 100644 index 0000000..73ea6be --- /dev/null +++ b/app/static/css/taur_app.css @@ -0,0 +1,150 @@ +.taur-container { + max-width: 1400px; + margin: 20px auto; + padding: 20px; + display: grid; + grid-template-columns: 1fr 400px; + gap: 30px; + min-height: calc(100vh - 40px); +} + +.taur-left-column { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + max-height: 100vh; +} + +.taur-right-column { + display: flex; + flex-direction: column; + gap: 20px; +} + +.taur-header { + text-align: center; +} + +.taur-header h1 { + color: #f80; + font-size: 2.5em; + margin-bottom: 10px; +} + +.taur-header p { + color: #fff; + font-size: 1.1em; +} + +.taur-canvas-container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +#taur-canvas { + border: 2px solid #f80; + border-radius: 8px; + background: #000; + max-width: 50vw; + max-height: 100vh; + width: auto; + height: auto; +} + +.taur-form { + display: flex; + flex-direction: column; + gap: 15px; +} + +.taur-form-group { + display: flex; + flex-direction: column; +} + +.taur-form-group label { + color: #fff; + margin-bottom: 5px; + font-size: 1em; +} + +.info-icon { + cursor: help; + font-size: 0.9em; + opacity: 0.7; + margin-left: 4px; + display: inline-block; + vertical-align: middle; +} + +.info-icon:hover { + opacity: 1; +} + +.taur-form-group input, +.taur-form-group select { + padding: 10px; + font-size: 1em; + border: 1px solid #444; + border-radius: 5px; + background: #222; + color: #fff; + width: 100%; + box-sizing: border-box; +} + +.taur-form-group input:focus, +.taur-form-group select:focus { + outline: none; + border-color: #f80; +} + +.taur-form button { + padding: 10px 20px; + font-size: 1em; + background: #f80; + color: #000; + border: none; + border-radius: 5px; + cursor: pointer; + font-weight: bold; +} + +.taur-form button:hover { + background: #ffab40; +} + +.back-link-container { + text-align: center; +} + +.back-link { + color: #78d0ff; + text-decoration: none; + font-size: 1.2em; +} + +.back-link:hover { + color: #a0e0ff; +} + +@media (max-width: 968px) { + .taur-container { + grid-template-columns: 1fr; + } + + .taur-left-column { + order: -1; + height: auto; + min-height: 300px; + } + + #taur-canvas { + max-width: 100vw; + max-height: 50vh; + } +} diff --git a/app/static/images/taur/TaurCalculatorReference.png b/app/static/images/taur/TaurCalculatorReference.png new file mode 100644 index 0000000000000000000000000000000000000000..1ef1e1ff5e66af75bd939b89f7963930a2ac8833 GIT binary patch literal 43238 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 - Taur Calculator (Coming Soon!) + Taur Calculator + -
-

Taur Calculator

-

Thanks to Volnar this feature is coming soon! - You'll be able to size your taur-self up!

-
- ← Back - to Main Calculator +
+
+
+ +
+
+
+
+

Taur Calculator

+

By Vixi and Volnar!

+
+ + + + + + {% if calculation_result %} +
+

Calculation Results

+
+ + + {% for key, value in calculation_result.items() %} + + + + + {% endfor %} + +
{{ key }} + + {% if value is string %} + {{ value.replace('\\u0027', "'") }} + {% elif value is mapping %} + { } + {% else %} + {{ value }} + {% endif %} +
+
+
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + \ No newline at end of file diff --git a/app/utils/taur_calculator.py b/app/utils/taur_calculator.py new file mode 100644 index 0000000..a1a70a0 --- /dev/null +++ b/app/utils/taur_calculator.py @@ -0,0 +1,98 @@ +# Conceived by Volnar <3 +# Implemented by Vixi! + +from typing import Dict, Any, Tuple, Optional + + +def calculate_taur( + anthro_height: float, + species_height: float, + species_length: float, + species_tail_length: float, + taur_full_height: float, + species_weight: float, + taur_length: Optional[float] = None, + measurement_type: str = "limb", # "limb" for mammals, "vitruvian" for reptiles + custom_body_parts: Optional[Dict[str, float]] = None, +) -> Dict[str, Any]: + """ + Calculate taur body dimensions based on provided inputs + using Volnar's Taur Calculator formula <3 + + Args: + anthro_height (float): Anthro height before taurification (AH). + species_height (float): Species height (SH). + species_length (float): Species length w/o tail (SL). + species_tail_length (float): Species tail length (ST). + taur_full_height (float): Taur full height (TFH). + species_weight (float): Species weight (SW). + taur_length (float, optional): Taur length w/o tail (TL). + measurement_type (str, optional): 'limb' (default) or 'vitruvian'. + custom_body_parts (dict, optional): Additional custom body part measurements (CBP). + + Returns: + Dict[str, Any]: Calculated taur results including TH, TFH, TL, etc. + """ + + # Splitting Anthro Height into parts + anthro_legs = anthro_height * 4 / 8 + anthro_torso = anthro_height * 3 / 8 + anthro_head = anthro_height * 1 / 8 + # Here approx: arm_length = anthro_legs + + # TH: Taur Height + if measurement_type == "limb": + # Limb Type: TH = AL + taur_height = anthro_legs + if taur_length is None: + taur_length_calc = species_length + else: + taur_length_calc = taur_length + else: + # Vitruvian Type: TL = (2 * AT) - AHe + taur_length_calc = (2 * anthro_torso) - anthro_head + # In this type, TH meaning is not directly defined; fallback to species scaling + taur_height = species_height * (taur_full_height / anthro_height) + + # Ratios + # TR: Taur Ratio (size change between SH and TH) + taur_ratio = taur_height / species_height if species_height > 0 else 0 + # AR: Anthro Ratio (size change between AH and TFH) + anthro_ratio = taur_full_height / anthro_height if anthro_height > 0 else 0 + + # Apply AR to custom body parts + cbp_result = {} + if custom_body_parts: + for part, value in custom_body_parts.items(): + cbp_result[part] = value * anthro_ratio + + # Taur Torso and Head + taur_torso = anthro_torso * anthro_ratio + taur_head = anthro_head * anthro_ratio + + # Taur Tail + taur_tail = ( + species_tail_length * taur_ratio if species_tail_length is not None else 0 + ) + + # Taur Weight estimate: cube AR, apply to SW + taur_weight = species_weight * (anthro_ratio**3) + taur_weight_minus = taur_weight * 0.9 + taur_weight_plus = taur_weight * 1.1 + + # Compose results + result = { + "TH": taur_height, + "TFH": taur_full_height, + "TL": taur_length_calc, + "TT": taur_tail, + "TTo": taur_torso, + "THe": taur_head, + "TW": taur_weight, + "TW-": taur_weight_minus, + "TW+": taur_weight_plus, + "CBP": cbp_result, + "AR": anthro_ratio, + "TR": taur_ratio, + } + return result From e8fed0631e2f5edc89c5eedffffe190f2fc78b79 Mon Sep 17 00:00:00 2001 From: Snowsune Date: Fri, 2 Jan 2026 19:22:47 -0500 Subject: [PATCH 02/15] Format with black --- app/__init__.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 65e92de..b549616 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -395,14 +395,30 @@ def taur(): measurement_type=measurement_type, ) - cleaned_calculation_result["AR"] = f"{inches_to_feet_inches(calculation_result["AR"])} (Anthropic Ratio)" - cleaned_calculation_result["TH"] = f"{inches_to_feet_inches(calculation_result["TH"])} (Taur Height)" - cleaned_calculation_result["TFH"] = f"{inches_to_feet_inches(calculation_result["TFH"])} (Taur Full Height)" - cleaned_calculation_result["TL"] = f"{inches_to_feet_inches(calculation_result["TL"])} (Taur Length)" - cleaned_calculation_result["TT"] = f"{inches_to_feet_inches(calculation_result["TT"])} (Taur Tail Length)" - cleaned_calculation_result["TTo"] = f"{inches_to_feet_inches(calculation_result["TTo"])} (Taur Torso Length)" - cleaned_calculation_result["THe"] = f"{inches_to_feet_inches(calculation_result["THe"])} (Taur Head Length)" - cleaned_calculation_result["TW"] = f"{calculation_result["TW"]:.2f} lbs (Taur Weight)" + cleaned_calculation_result["AR"] = ( + f"{inches_to_feet_inches(calculation_result["AR"])} (Anthropic Ratio)" + ) + cleaned_calculation_result["TH"] = ( + f"{inches_to_feet_inches(calculation_result["TH"])} (Taur Height)" + ) + cleaned_calculation_result["TFH"] = ( + f"{inches_to_feet_inches(calculation_result["TFH"])} (Taur Full Height)" + ) + cleaned_calculation_result["TL"] = ( + f"{inches_to_feet_inches(calculation_result["TL"])} (Taur Length)" + ) + cleaned_calculation_result["TT"] = ( + f"{inches_to_feet_inches(calculation_result["TT"])} (Taur Tail Length)" + ) + cleaned_calculation_result["TTo"] = ( + f"{inches_to_feet_inches(calculation_result["TTo"])} (Taur Torso Length)" + ) + cleaned_calculation_result["THe"] = ( + f"{inches_to_feet_inches(calculation_result["THe"])} (Taur Head Length)" + ) + cleaned_calculation_result["TW"] = ( + f"{calculation_result["TW"]:.2f} lbs (Taur Weight)" + ) except (ValueError, TypeError) as e: logging.warning(f"Taur calculation error: {e}") From e76c86f08e60ac9740ff529309c179af8a9512ab Mon Sep 17 00:00:00 2001 From: Snowsune Date: Fri, 2 Jan 2026 19:25:22 -0500 Subject: [PATCH 03/15] Fix.. again :P --- app/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index b549616..ad8b838 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -396,28 +396,28 @@ def taur(): ) cleaned_calculation_result["AR"] = ( - f"{inches_to_feet_inches(calculation_result["AR"])} (Anthropic Ratio)" + f"{inches_to_feet_inches(calculation_result['AR'])} (Anthropic Ratio)" ) cleaned_calculation_result["TH"] = ( - f"{inches_to_feet_inches(calculation_result["TH"])} (Taur Height)" + f"{inches_to_feet_inches(calculation_result['TH'])} (Taur Height)" ) cleaned_calculation_result["TFH"] = ( - f"{inches_to_feet_inches(calculation_result["TFH"])} (Taur Full Height)" + f"{inches_to_feet_inches(calculation_result['TFH'])} (Taur Full Height)" ) cleaned_calculation_result["TL"] = ( - f"{inches_to_feet_inches(calculation_result["TL"])} (Taur Length)" + f"{inches_to_feet_inches(calculation_result['TL'])} (Taur Length)" ) cleaned_calculation_result["TT"] = ( - f"{inches_to_feet_inches(calculation_result["TT"])} (Taur Tail Length)" + f"{inches_to_feet_inches(calculation_result['TT'])} (Taur Tail Length)" ) cleaned_calculation_result["TTo"] = ( - f"{inches_to_feet_inches(calculation_result["TTo"])} (Taur Torso Length)" + f"{inches_to_feet_inches(calculation_result['TTo'])} (Taur Torso Length)" ) cleaned_calculation_result["THe"] = ( - f"{inches_to_feet_inches(calculation_result["THe"])} (Taur Head Length)" + f"{inches_to_feet_inches(calculation_result['THe'])} (Taur Head Length)" ) cleaned_calculation_result["TW"] = ( - f"{calculation_result["TW"]:.2f} lbs (Taur Weight)" + f"{calculation_result['TW']:.2f} lbs (Taur Weight)" ) except (ValueError, TypeError) as e: From aecd46d98442c51e6bf723a6a4a9aab7048f053e Mon Sep 17 00:00:00 2001 From: Snowsune Date: Sun, 21 Jun 2026 23:08:25 -0400 Subject: [PATCH 04/15] Implement inital hunner art! --- app/__init__.py | 52 ++- app/static/css/taur_app.css | 157 ++++++-- app/static/images/favicon.ico | Bin 176937 -> 0 bytes .../images/taur/TaurCalculatorReference.png | Bin 43238 -> 0 bytes app/static/js/size_diff/compositor.js | 369 ++++++++++++++++++ app/taur_calculator/taur_canvas.js | 225 +++++++++++ app/taur_calculator/taur_data.json | 42 ++ app/templates/taur.html | 91 +++-- app/utils/art_paths.py | 33 ++ app/utils/generate_image.py | 5 + app/utils/taur_data.py | 11 + ..._Hight_Refrence_L_BODY_Volnar_for_Vixi.png | Bin 0 -> 186231 bytes ...t_Refrence_RIDER_Volnar_Felix_for_Vixi.png | Bin 0 -> 321233 bytes ...rence_RIDER_Volnar_Felix_for_Vixi_Mask.png | Bin 0 -> 195347 bytes ...ur_Hight_Refrence_TAIL_Volnar_for_Vixi.png | Bin 0 -> 278847 bytes ..._Hight_Refrence_U_BODY_Volnar_for_Vixi.png | Bin 0 -> 249570 bytes scripts/trim_art.py | 56 ++- 17 files changed, 955 insertions(+), 86 deletions(-) delete mode 100644 app/static/images/favicon.ico delete mode 100644 app/static/images/taur/TaurCalculatorReference.png create mode 100644 app/static/js/size_diff/compositor.js create mode 100644 app/taur_calculator/taur_canvas.js create mode 100644 app/taur_calculator/taur_data.json create mode 100644 app/utils/art_paths.py create mode 100644 app/utils/taur_data.py create mode 100644 art/Hunner/Taur/Side_Taur_Hight_Refrence_L_BODY_Volnar_for_Vixi.png create mode 100644 art/Hunner/Taur/Side_Taur_Hight_Refrence_RIDER_Volnar_Felix_for_Vixi.png create mode 100644 art/Hunner/Taur/Side_Taur_Hight_Refrence_RIDER_Volnar_Felix_for_Vixi_Mask.png create mode 100644 art/Hunner/Taur/Side_Taur_Hight_Refrence_TAIL_Volnar_for_Vixi.png create mode 100644 art/Hunner/Taur/Side_Taur_Hight_Refrence_U_BODY_Volnar_for_Vixi.png diff --git a/app/__init__.py b/app/__init__.py index ad8b838..6637aaf 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,6 +3,7 @@ render_template, request, send_file, + send_from_directory, jsonify, redirect, url_for, @@ -36,9 +37,11 @@ get_default_characters, ) from app.utils.stats import StatsManager -from app.utils.generate_image import render_image +from app.utils.generate_image import render_image, get_dist_art_path +from app.utils.art_paths import layer_asset_urls from app.utils.character import Character from app.utils.taur_calculator import calculate_taur +from app.utils.taur_data import load_taur_data app = Flask(__name__) app.secret_key = os.urandom(24) @@ -86,6 +89,37 @@ def wrapped(*args, **kwargs): ] +TAUR_DATA = load_taur_data() +TAUR_CALCULATOR_DIR = os.path.join(os.path.dirname(__file__), "taur_calculator") + + +def register_taur_calculator_routes(application): + if "taur_calculator_asset" in application.view_functions: + return + + @application.route( + "/taur-calculator/", endpoint="taur_calculator_asset" + ) + def taur_calculator_asset(filename): + return send_from_directory(TAUR_CALCULATOR_DIR, filename) + + +register_taur_calculator_routes(app) + + +@app.route("/art/") +def serve_art(rel_path): + """Serve trimmed art assets from art/dist/.""" + file_path = get_dist_art_path(rel_path) + art_dist_root = os.path.abspath(os.path.join("art", "dist")) + resolved = os.path.abspath(file_path) + + if not resolved.startswith(art_dist_root + os.sep) or not os.path.isfile(resolved): + return "Not found", 404 + + return send_file(resolved, max_age=31536000) + + @app.route("/generate-image") @cache_with_stats(timeout=31536000, query_string=True) def generate_image(): @@ -353,6 +387,7 @@ def taur(): for key in [ "name", "measurement_type", + "species", "anthro_height", "species_height", "species_length", @@ -360,6 +395,10 @@ def taur(): "taur_full_height", "species_weight", "taur_length", + "show_rider", + "rider_height", + "taur_body_color", + "taur_rider_color", ]: value = request.form.get(key, "") if value: @@ -425,15 +464,26 @@ def taur(): calculation_result = None cleaned_calculation_result = None + taur_data = TAUR_DATA return render_template( "taur.html", species=filtered_species, species_data=species_data_map, calculation_result=cleaned_calculation_result, form_data=dict(request.args) if request.args else None, + taur_data=taur_data, + taur_layer_urls={ + key: layer_asset_urls( + layer["path"], + lambda path: url_for("serve_art", rel_path=path), + ) + for key, layer in taur_data["layers"].items() + }, + debug=app.debug or os.getenv("GIT_COMMIT") is None, ) # For WSGI def create_app(): + register_taur_calculator_routes(app) return app diff --git a/app/static/css/taur_app.css b/app/static/css/taur_app.css index 73ea6be..b696202 100644 --- a/app/static/css/taur_app.css +++ b/app/static/css/taur_app.css @@ -1,82 +1,123 @@ -.taur-container { - max-width: 1400px; - margin: 20px auto; - padding: 20px; +.taur-page { + display: block; + height: 100dvh; + min-height: 100dvh; + overflow: hidden; +} + +.taur-page .taur-container { + max-width: 1600px; + height: 100%; + margin: 0 auto; + padding: 8px 10px; display: grid; - grid-template-columns: 1fr 400px; - gap: 30px; - min-height: calc(100vh - 40px); + grid-template-columns: minmax(0, 1fr) minmax(260px, 360px); + gap: 10px; + box-sizing: border-box; } .taur-left-column { display: flex; - align-items: center; + align-items: stretch; justify-content: center; - height: 100vh; - max-height: 100vh; + min-height: 0; + min-width: 0; } .taur-right-column { display: flex; flex-direction: column; - gap: 20px; + gap: clamp(4px, 0.8vh, 10px); + min-height: 0; + min-width: 0; + overflow: hidden; + font-size: clamp(0.72rem, 1.35vh, 0.92rem); } .taur-header { text-align: center; + flex-shrink: 0; } .taur-header h1 { color: #f80; - font-size: 2.5em; - margin-bottom: 10px; + font-size: clamp(1.15rem, 2.8vh, 1.9rem); + margin: 0 0 2px; + line-height: 1.1; } .taur-header p { color: #fff; - font-size: 1.1em; + font-size: clamp(0.65rem, 1.2vh, 0.85rem); + margin: 0; + line-height: 1.25; } .taur-canvas-container { width: 100%; height: 100%; + min-height: 0; display: flex; - align-items: center; + align-items: stretch; justify-content: center; } #taur-canvas { border: 2px solid #f80; border-radius: 8px; - background: #000; - max-width: 50vw; - max-height: 100vh; - width: auto; - height: auto; + background: #fff; + width: 100%; + height: 100%; + display: block; +} + +.taur-results { + flex-shrink: 0; + max-height: 18vh; + overflow-y: auto; + font-size: 0.85em; } .taur-form { - display: flex; - flex-direction: column; - gap: 15px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: clamp(3px, 0.55vh, 7px) clamp(6px, 0.8vw, 10px); + flex: 1; + min-height: 0; + overflow-y: auto; + align-content: start; + padding-right: 2px; +} + +.taur-form > .taur-form-group, +.taur-form > button, +.taur-form > .taur-rider-toggle { + grid-column: 1 / -1; +} + +.taur-form-group.taur-half { + grid-column: span 1; + min-width: 0; } .taur-form-group { display: flex; flex-direction: column; + min-width: 0; } .taur-form-group label { color: #fff; - margin-bottom: 5px; - font-size: 1em; + margin-bottom: 2px; + font-size: 0.92em; + line-height: 1.2; } .info-icon { cursor: help; font-size: 0.9em; opacity: 0.7; - margin-left: 4px; + margin-left: 2px; display: inline-block; vertical-align: middle; } @@ -87,14 +128,15 @@ .taur-form-group input, .taur-form-group select { - padding: 10px; + padding: clamp(3px, 0.55vh, 7px) clamp(5px, 0.6vw, 8px); font-size: 1em; border: 1px solid #444; - border-radius: 5px; + border-radius: 4px; background: #222; color: #fff; width: 100%; box-sizing: border-box; + min-width: 0; } .taur-form-group input:focus, @@ -103,8 +145,35 @@ border-color: #f80; } +.taur-form-group input[type="color"] { + width: 100%; + height: clamp(1.6rem, 3vh, 2.2rem); + padding: 2px; + cursor: pointer; + box-sizing: border-box; +} + +.taur-rider-toggle label { + flex-direction: row; + align-items: center; + gap: 8px; + cursor: pointer; + margin-bottom: 0; +} + +.taur-rider-toggle input[type="checkbox"] { + width: auto; + margin: 0; + transform: scale(1.1); +} + +.taur-rider-height[hidden], +.taur-rider-color[hidden] { + display: none; +} + .taur-form button { - padding: 10px 20px; + padding: clamp(5px, 0.9vh, 9px) 12px; font-size: 1em; background: #f80; color: #000; @@ -112,6 +181,7 @@ border-radius: 5px; cursor: pointer; font-weight: bold; + flex-shrink: 0; } .taur-form button:hover { @@ -120,12 +190,13 @@ .back-link-container { text-align: center; + flex-shrink: 0; } .back-link { color: #78d0ff; text-decoration: none; - font-size: 1.2em; + font-size: clamp(0.8rem, 1.4vh, 1rem); } .back-link:hover { @@ -133,18 +204,28 @@ } @media (max-width: 968px) { - .taur-container { + .taur-page { + height: auto; + min-height: 100dvh; + overflow: auto; + } + + .taur-page .taur-container { grid-template-columns: 1fr; + height: auto; + min-height: 100dvh; } - + .taur-left-column { order: -1; - height: auto; - min-height: 300px; + height: min(42dvh, 360px); } - - #taur-canvas { - max-width: 100vw; - max-height: 50vh; + + .taur-right-column { + overflow: visible; + } + + .taur-form { + overflow: visible; } } diff --git a/app/static/images/favicon.ico b/app/static/images/favicon.ico deleted file mode 100644 index c48f5a8d4eac1935c8887aff0892f13b3ddd9da4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176937 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 DF5_<| diff --git a/app/static/images/taur/TaurCalculatorReference.png b/app/static/images/taur/TaurCalculatorReference.png deleted file mode 100644 index 1ef1e1ff5e66af75bd939b89f7963930a2ac8833..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43238 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 { + function jointLocalPosition(layerDef, jointName) { + const [jx, jy] = layerDef.joints[jointName]; + const trim = layerDef.trim; + return [jx - trim.x, jy - trim.y]; + } + + function jointWorldPosition(layerDef, jointName, placedAtJoint, worldX, worldY) { + const joints = layerDef.joints; + const [jx, jy] = joints[jointName]; + const [px, py] = joints[placedAtJoint]; + return [worldX + (jx - px), worldY + (jy - py)]; + } + + function placementFromJoint(layerDef, image, jointName, targetX, targetY, meta = {}) { + const [localX, localY] = jointLocalPosition(layerDef, jointName); + return { + ...meta, + drawX: targetX - localX, + drawY: targetY - localY, + width: image ? image.width : 0, + height: image ? image.height : 0, + scale: meta.scale ?? 1, + scaleCenter: layerDef.scale_center ?? null, + }; + } + + function layerLocalCoords(worldX, worldY, placement) { + return { + x: Math.round(worldX - placement.drawX), + y: Math.round(worldY - placement.drawY), + }; + } + + function applyPlacementTransform(ctx, placement, draw) { + const scale = placement.scale ?? 1; + const center = placement.scaleCenter; + + ctx.save(); + if (scale === 1 || !center) { + ctx.translate(placement.drawX, placement.drawY); + draw(ctx, 0, 0); + } else { + const [cx, cy] = center; + ctx.translate(placement.drawX + cx, placement.drawY + cy); + ctx.scale(scale, scale); + draw(ctx, -cx, -cy); + } + ctx.restore(); + } + + let colorScratch = null; + + function getColorScratch(width, height) { + if (!colorScratch) { + colorScratch = document.createElement('canvas'); + } + if (colorScratch.width < width) { + colorScratch.width = width; + } + if (colorScratch.height < height) { + colorScratch.height = height; + } + return colorScratch; + } + + function drawLayer(ctx, image, placement, { color = '#000000', mask = null } = {}) { + if (!image) { + return; + } + + if (mask) { + applyPlacementTransform(ctx, placement, (layerCtx, x, y) => { + layerCtx.drawImage(mask, x, y); + }); + } + + const width = image.width; + const height = image.height; + + applyPlacementTransform(ctx, placement, (layerCtx, x, y) => { + const scratch = getColorScratch(width, height); + const scratchCtx = scratch.getContext('2d'); + scratchCtx.setTransform(1, 0, 0, 1, 0, 0); + scratchCtx.clearRect(0, 0, width, height); + scratchCtx.globalCompositeOperation = 'source-over'; + scratchCtx.drawImage(image, 0, 0); + scratchCtx.globalCompositeOperation = 'source-in'; + scratchCtx.fillStyle = color; + scratchCtx.fillRect(0, 0, width, height); + layerCtx.drawImage(scratch, 0, 0, width, height, x, y, width, height); + }); + } + + function computePlacementBounds(placements) { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + for (const placement of placements) { + if (placement.visible === false) { + continue; + } + + const scale = placement.scale ?? 1; + let left = placement.drawX; + let top = placement.drawY; + let right = placement.drawX + placement.width; + let bottom = placement.drawY + placement.height; + + if (scale !== 1 && placement.scaleCenter) { + const [cx, cy] = placement.scaleCenter; + left = placement.drawX + cx * (1 - scale); + top = placement.drawY + cy * (1 - scale); + right = left + placement.width * scale; + bottom = top + placement.height * scale; + } + + minX = Math.min(minX, left); + minY = Math.min(minY, top); + maxX = Math.max(maxX, right); + maxY = Math.max(maxY, bottom); + } + + if (!Number.isFinite(minX)) { + return null; + } + + return { minX, minY, maxX, maxY }; + } + + function maskPathFor(artPath) { + const dot = artPath.lastIndexOf('.'); + if (dot === -1) { + return `${artPath}_Mask`; + } + return `${artPath.slice(0, dot)}_Mask${artPath.slice(dot)}`; + } + + function createCanvasView(canvas, logicalWidth, logicalHeight) { + let viewState = null; + + function update(fitOptions = null) { + const displayW = canvas.clientWidth; + const displayH = canvas.clientHeight; + let scale; + let offsetX; + let offsetY; + + if (fitOptions?.contentBounds) { + const { minX, minY, maxX, maxY } = fitOptions.contentBounds; + const contentW = maxX - minX; + const contentH = maxY - minY; + const padX = fitOptions.paddingX ?? 16; + const padY = fitOptions.paddingY ?? 16; + + scale = Math.min( + (displayW - padX * 2) / contentW, + (displayH - padY * 2) / contentH + ); + + offsetX = (displayW - contentW * scale) / 2 - minX * scale; + if (fitOptions.alignY === 'bottom') { + offsetY = displayH - padY - maxY * scale; + } else { + offsetY = (displayH - contentH * scale) / 2 - minY * scale; + } + } else { + scale = Math.min(displayW / logicalWidth, displayH / logicalHeight); + offsetX = (displayW - logicalWidth * scale) / 2; + offsetY = (displayH - logicalHeight * scale) / 2; + } + + canvas.width = displayW; + canvas.height = displayH; + viewState = { + scale, + offsetX, + offsetY, + logicalWidth, + logicalHeight, + displayW, + displayH, + }; + return viewState; + } + + function getViewState() { + return viewState ?? update(); + } + + function applyWorldTransform(ctx) { + const vs = getViewState(); + ctx.setTransform(vs.scale, 0, 0, vs.scale, vs.offsetX, vs.offsetY); + } + + function clear(ctx, fillStyle = '#fff') { + const vs = getViewState(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = fillStyle; + ctx.fillRect(0, 0, vs.displayW, vs.displayH); + applyWorldTransform(ctx); + ctx.fillStyle = fillStyle; + ctx.fillRect(0, 0, vs.logicalWidth, vs.logicalHeight); + } + + function displayToWorld(displayX, displayY) { + const vs = viewState; + if (!vs) { + return null; + } + return { + x: (displayX - vs.offsetX) / vs.scale, + y: (displayY - vs.offsetY) / vs.scale, + }; + } + + return { + update, + getViewState, + applyWorldTransform, + clear, + displayToWorld, + }; + } + + function drawLayerDebugHud(ctx, view, mouseDisplay, placements) { + if (!mouseDisplay) { + return; + } + + const world = view.displayToWorld(mouseDisplay.x, mouseDisplay.y); + if (!world) { + return; + } + + const lines = placements + .filter((placement) => placement.visible !== false) + .map((placement) => { + const local = layerLocalCoords(world.x, world.y, placement); + const label = placement.label ?? placement.layerKey ?? 'Layer'; + return `${label}: x=${local.x} y=${local.y}`; + }); + + const padding = 10; + const lineHeight = 18; + const boxWidth = 260; + const boxHeight = padding * 2 + lines.length * lineHeight; + + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(8, 8, boxWidth, boxHeight); + ctx.fillStyle = '#8f8'; + ctx.font = '14px monospace'; + ctx.textBaseline = 'top'; + lines.forEach((line, index) => { + ctx.fillText(line, 16, 12 + index * lineHeight); + }); + } + + function attachLayerDebugHud(canvas, { view, getPlacements, enabled }) { + let mouseDisplay = null; + + function redraw() { + if (typeof enabled === 'function' ? !enabled() : !enabled) { + return; + } + mouseDisplay = null; + } + + canvas.addEventListener('mousemove', (event) => { + if (typeof enabled === 'function' ? !enabled() : !enabled) { + return; + } + const rect = canvas.getBoundingClientRect(); + mouseDisplay = { + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }; + canvas.dispatchEvent(new CustomEvent('compositor-redraw')); + }); + + canvas.addEventListener('mouseleave', () => { + mouseDisplay = null; + canvas.dispatchEvent(new CustomEvent('compositor-redraw')); + }); + + return { + draw(ctx) { + if (typeof enabled === 'function' ? !enabled() : !enabled) { + return; + } + drawLayerDebugHud(ctx, view, mouseDisplay, getPlacements()); + }, + clear: redraw, + }; + } + + function loadImages(sources, { onError, onProgress } = {}) { + const jobs = []; + for (const [key, source] of Object.entries(sources)) { + jobs.push({ key, source, url: typeof source === 'string' ? source : source.url }); + if (source.mask?.url) { + jobs.push({ + key: `${key}_mask`, + source: source.mask, + url: source.mask.url, + }); + } + } + + const images = {}; + let loaded = 0; + + return new Promise((resolve) => { + if (jobs.length === 0) { + resolve(images); + return; + } + + for (const job of jobs) { + const img = new Image(); + + img.onload = () => { + images[job.key] = img; + loaded++; + onProgress?.(loaded, jobs.length); + if (loaded === jobs.length) { + resolve(images); + } + }; + + img.onerror = () => { + onError?.(job.key, job.source); + loaded++; + onProgress?.(loaded, jobs.length); + if (loaded === jobs.length) { + resolve(images); + } + }; + + img.src = job.url; + } + }); + } + + return { + jointLocalPosition, + jointWorldPosition, + placementFromJoint, + layerLocalCoords, + computePlacementBounds, + drawLayer, + maskPathFor, + createCanvasView, + attachLayerDebugHud, + loadImages, + }; +})(); + +if (typeof window !== 'undefined') { + window.SizeDiffCompositor = SizeDiffCompositor; +} diff --git a/app/taur_calculator/taur_canvas.js b/app/taur_calculator/taur_canvas.js new file mode 100644 index 0000000..35dfb75 --- /dev/null +++ b/app/taur_calculator/taur_canvas.js @@ -0,0 +1,225 @@ +function initTaurCanvas(config) { + const { + jointWorldPosition, + placementFromJoint, + drawLayer, + createCanvasView, + attachLayerDebugHud, + loadImages, + computePlacementBounds, + } = window.SizeDiffCompositor; + + const canvas = document.getElementById('taur-canvas'); + const ctx = canvas.getContext('2d'); + const { taurData, layerSources, debug = false } = config; + const BODY_LAYER_KEYS = new Set(['lBody', 'uBody', 'tail']); + const DEFAULT_BODY_COLOR = '#ff0000'; // Volnar red + const DEFAULT_RIDER_COLOR = '#ff8800'; // Felix orange + const TRIM_ART_COMMAND = 'python3 scripts/trim_art.py'; + const LAYER_LABELS = { + lBody: 'LowerBody', + uBody: 'UpperBody', + tail: 'Tail', + rider: 'Rider', + }; + + const view = createCanvasView( + canvas, + taurData.canvas.width, + taurData.canvas.height + ); + + let layers = {}; + let latestPlacements = []; + + function layerDef(key) { + return taurData.layers[key]; + } + + function floorY() { + return taurData.canvas.height - taurData.canvas.floor_margin; + } + + function bodyColor() { + return document.getElementById('taur_body_color')?.value || DEFAULT_BODY_COLOR; + } + + function riderColor() { + return document.getElementById('taur_rider_color')?.value || DEFAULT_RIDER_COLOR; + } + + function riderScale() { + const raw = document.getElementById('rider_height')?.value; + const height = parseFloat(raw); + if (!raw || Number.isNaN(height)) { + return 1; + } + return height / 100; + } + + function buildPlacements() { + const centerX = taurData.canvas.width / 2; + const groundY = floorY(); + const showRider = document.getElementById('show_rider')?.checked; + + const [upperX, upperY] = jointWorldPosition( + layerDef('lBody'), + 'upper_attach', + 'floor', + centerX, + groundY + ); + const [tailX, tailY] = jointWorldPosition( + layerDef('lBody'), + 'tail_attach', + 'floor', + centerX, + groundY + ); + + const placements = [ + { + layerKey: 'lBody', + visible: true, + ...placementFromJoint( + layerDef('lBody'), + layers.lBody, + 'floor', + centerX, + groundY, + { layerKey: 'lBody', label: LAYER_LABELS.lBody } + ), + }, + { + layerKey: 'uBody', + visible: true, + ...placementFromJoint( + layerDef('uBody'), + layers.uBody, + 'lower_attach', + upperX, + upperY, + { layerKey: 'uBody', label: LAYER_LABELS.uBody } + ), + }, + { + layerKey: 'tail', + visible: true, + ...placementFromJoint( + layerDef('tail'), + layers.tail, + 'body_attach', + tailX, + tailY, + { layerKey: 'tail', label: LAYER_LABELS.tail } + ), + }, + ]; + + if (showRider) { + const [riderX, riderY] = jointWorldPosition( + layerDef('uBody'), + 'rider_attach', + 'lower_attach', + upperX, + upperY + ); + placements.push({ + layerKey: 'rider', + visible: true, + ...placementFromJoint( + layerDef('rider'), + layers.rider, + 'mount_attach', + riderX, + riderY, + { + layerKey: 'rider', + label: LAYER_LABELS.rider, + scale: riderScale(), + } + ), + }); + } + + return placements; + } + + const debugHud = attachLayerDebugHud(canvas, { + view, + enabled: () => debug, + getPlacements: () => latestPlacements, + }); + + function drawScene() { + latestPlacements = buildPlacements(); + const contentBounds = computePlacementBounds(latestPlacements); + + view.update({ + contentBounds, + paddingX: 12, + paddingY: 12, + alignY: 'bottom', + }); + view.clear(ctx); + + const groundY = floorY(); + const logicalW = taurData.canvas.width; + + ctx.strokeStyle = '#bbb'; + ctx.lineWidth = 3 / view.getViewState().scale; + ctx.beginPath(); + ctx.moveTo(0, groundY); + ctx.lineTo(logicalW, groundY); + ctx.stroke(); + + for (const placement of latestPlacements) { + const image = layers[placement.layerKey]; + const mask = layers[`${placement.layerKey}_mask`] ?? null; + const color = BODY_LAYER_KEYS.has(placement.layerKey) + ? bodyColor() + : riderColor(); + drawLayer(ctx, image, placement, { color, mask }); + } + + debugHud.draw(ctx); + } + + loadImages(layerSources, { + onError: (key, source) => { + const path = typeof source === 'string' ? source : source.path; + console.error( + `Failed to load taur layer "${key}" (art/dist/${path}). ` + + `You must run \`${TRIM_ART_COMMAND}\` first to create dist images!` + ); + }, + }).then((loaded) => { + layers = loaded; + drawScene(); + }); + + canvas.addEventListener('compositor-redraw', drawScene); + + const showRiderCheckbox = document.getElementById('show_rider'); + if (showRiderCheckbox) { + showRiderCheckbox.addEventListener('change', drawScene); + } + + const riderHeightInput = document.getElementById('rider_height'); + if (riderHeightInput) { + riderHeightInput.addEventListener('input', drawScene); + } + + for (const colorInputId of ['taur_body_color', 'taur_rider_color']) { + const colorInput = document.getElementById(colorInputId); + if (colorInput) { + colorInput.addEventListener('input', drawScene); + } + } + + window.addEventListener('resize', drawScene); +} + +if (window.taurCanvasConfig) { + initTaurCanvas(window.taurCanvasConfig); +} diff --git a/app/taur_calculator/taur_data.json b/app/taur_calculator/taur_data.json new file mode 100644 index 0000000..9cddcc1 --- /dev/null +++ b/app/taur_calculator/taur_data.json @@ -0,0 +1,42 @@ +{ + "source_size": [3000, 3000], + "canvas": { + "width": 4000, + "height": 3000, + "floor_margin": 120 + }, + "layers": { + "lBody": { + "path": "Hunner/Taur/Side_Taur_Hight_Refrence_L_BODY_Volnar_for_Vixi.png", + "trim": { "x": 462, "y": 762, "width": 1229, "height": 1726 }, + "joints": { + "floor": [1452, 2487], + "upper_attach": [738, 1481], + "tail_attach": [1386, 1453] + } + }, + "uBody": { + "path": "Hunner/Taur/Side_Taur_Hight_Refrence_U_BODY_Volnar_for_Vixi.png", + "trim": { "x": 137, "y": 616, "width": 722, "height": 1242 }, + "joints": { + "lower_attach": [738, 1481], + "rider_attach": [979, 2017] + } + }, + "tail": { + "path": "Hunner/Taur/Side_Taur_Hight_Refrence_TAIL_Volnar_for_Vixi.png", + "trim": { "x": 1104, "y": 785, "width": 1863, "height": 1461 }, + "joints": { + "body_attach": [1386, 1453] + } + }, + "rider": { + "path": "Hunner/Taur/Side_Taur_Hight_Refrence_RIDER_Volnar_Felix_for_Vixi.png", + "trim": { "x": 852, "y": 637, "width": 970, "height": 1381 }, + "scale_center": [300, 875], + "joints": { + "mount_attach": [979, 2017] + } + } + } +} diff --git a/app/templates/taur.html b/app/templates/taur.html index c8b9efa..8f171fd 100644 --- a/app/templates/taur.html +++ b/app/templates/taur.html @@ -10,7 +10,7 @@ - +
@@ -20,7 +20,7 @@

Taur Calculator

-

By Vixi and Volnar!

+

By Vixi, Volnar and Snow! With art by Hunner!

@@ -79,39 +79,39 @@

Calculation Results

-
+
-
+
-
+
-
+
-
+
-
+
@@ -124,6 +124,29 @@

Calculation Results

placeholder="Leave empty for auto" />
+
+ + +
+ +
+ +
+ + + + + + + + + + + - + \ No newline at end of file diff --git a/app/utils/taur_calculator.py b/app/utils/taur_calculator.py deleted file mode 100644 index a1a70a0..0000000 --- a/app/utils/taur_calculator.py +++ /dev/null @@ -1,98 +0,0 @@ -# Conceived by Volnar <3 -# Implemented by Vixi! - -from typing import Dict, Any, Tuple, Optional - - -def calculate_taur( - anthro_height: float, - species_height: float, - species_length: float, - species_tail_length: float, - taur_full_height: float, - species_weight: float, - taur_length: Optional[float] = None, - measurement_type: str = "limb", # "limb" for mammals, "vitruvian" for reptiles - custom_body_parts: Optional[Dict[str, float]] = None, -) -> Dict[str, Any]: - """ - Calculate taur body dimensions based on provided inputs - using Volnar's Taur Calculator formula <3 - - Args: - anthro_height (float): Anthro height before taurification (AH). - species_height (float): Species height (SH). - species_length (float): Species length w/o tail (SL). - species_tail_length (float): Species tail length (ST). - taur_full_height (float): Taur full height (TFH). - species_weight (float): Species weight (SW). - taur_length (float, optional): Taur length w/o tail (TL). - measurement_type (str, optional): 'limb' (default) or 'vitruvian'. - custom_body_parts (dict, optional): Additional custom body part measurements (CBP). - - Returns: - Dict[str, Any]: Calculated taur results including TH, TFH, TL, etc. - """ - - # Splitting Anthro Height into parts - anthro_legs = anthro_height * 4 / 8 - anthro_torso = anthro_height * 3 / 8 - anthro_head = anthro_height * 1 / 8 - # Here approx: arm_length = anthro_legs - - # TH: Taur Height - if measurement_type == "limb": - # Limb Type: TH = AL - taur_height = anthro_legs - if taur_length is None: - taur_length_calc = species_length - else: - taur_length_calc = taur_length - else: - # Vitruvian Type: TL = (2 * AT) - AHe - taur_length_calc = (2 * anthro_torso) - anthro_head - # In this type, TH meaning is not directly defined; fallback to species scaling - taur_height = species_height * (taur_full_height / anthro_height) - - # Ratios - # TR: Taur Ratio (size change between SH and TH) - taur_ratio = taur_height / species_height if species_height > 0 else 0 - # AR: Anthro Ratio (size change between AH and TFH) - anthro_ratio = taur_full_height / anthro_height if anthro_height > 0 else 0 - - # Apply AR to custom body parts - cbp_result = {} - if custom_body_parts: - for part, value in custom_body_parts.items(): - cbp_result[part] = value * anthro_ratio - - # Taur Torso and Head - taur_torso = anthro_torso * anthro_ratio - taur_head = anthro_head * anthro_ratio - - # Taur Tail - taur_tail = ( - species_tail_length * taur_ratio if species_tail_length is not None else 0 - ) - - # Taur Weight estimate: cube AR, apply to SW - taur_weight = species_weight * (anthro_ratio**3) - taur_weight_minus = taur_weight * 0.9 - taur_weight_plus = taur_weight * 1.1 - - # Compose results - result = { - "TH": taur_height, - "TFH": taur_full_height, - "TL": taur_length_calc, - "TT": taur_tail, - "TTo": taur_torso, - "THe": taur_head, - "TW": taur_weight, - "TW-": taur_weight_minus, - "TW+": taur_weight_plus, - "CBP": cbp_result, - "AR": anthro_ratio, - "TR": taur_ratio, - } - return result From 605ea8231f60da4deef34cab3b4b9f2f6a9d06f6 Mon Sep 17 00:00:00 2001 From: Snowsune Date: Mon, 22 Jun 2026 01:08:29 -0400 Subject: [PATCH 06/15] yayy --- app/static/js/size_diff/compositor.js | 98 ++++++++++++++++--- app/static/js/size_diff/units.js | 19 ++++ app/taur_calculator/taur_algorithms.js | 33 ++++--- app/taur_calculator/taur_canvas.js | 93 +++++++++--------- app/taur_calculator/taur_data.json | 17 +++- app/taur_calculator/taur_form.js | 3 +- app/taur_calculator/taur_measurements.js | 119 +++++++++++++++-------- app/templates/taur.html | 50 +++++----- 8 files changed, 283 insertions(+), 149 deletions(-) diff --git a/app/static/js/size_diff/compositor.js b/app/static/js/size_diff/compositor.js index 8633e64..9e5a19a 100644 --- a/app/static/js/size_diff/compositor.js +++ b/app/static/js/size_diff/compositor.js @@ -26,14 +26,25 @@ const SizeDiffCompositor = (() => { width: image ? image.width : 0, height: image ? image.height : 0, scale: meta.scale ?? 1, - scaleCenter: layerDef.scale_center ?? null, + scaleCenter: meta.scaleCenter ?? layerDef.scale_center ?? null, }; } function layerLocalCoords(worldX, worldY, placement) { + const scale = placement.scale ?? 1; + const center = placement.scaleCenter; + + if (scale === 1 || !center) { + return { + x: Math.round(worldX - placement.drawX), + y: Math.round(worldY - placement.drawY), + }; + } + + const [cx, cy] = center; return { - x: Math.round(worldX - placement.drawX), - y: Math.round(worldY - placement.drawY), + x: Math.round(cx + (worldX - placement.drawX - cx) / scale), + y: Math.round(cy + (worldY - placement.drawY - cy) / scale), }; } @@ -52,6 +63,14 @@ const SizeDiffCompositor = (() => { ]; } + function anchorJointOnPlacement(placement, layerDef, jointName, worldX, worldY) { + const [localX, localY] = jointLocalPosition(layerDef, jointName); + const [currentX, currentY] = placementLocalToWorld(placement, localX, localY); + placement.drawX += worldX - currentX; + placement.drawY += worldY - currentY; + return placement; + } + function applyPlacementTransform(ctx, placement, draw) { const scale = placement.scale ?? 1; const center = placement.scaleCenter; @@ -112,7 +131,7 @@ const SizeDiffCompositor = (() => { }); } - function computePlacementBounds(placements) { + function computePlacementBounds(placements, { ignoreScale = false } = {}) { let minX = Infinity; let minY = Infinity; let maxX = -Infinity; @@ -123,7 +142,7 @@ const SizeDiffCompositor = (() => { continue; } - const scale = placement.scale ?? 1; + const scale = ignoreScale ? 1 : (placement.scale ?? 1); let left = placement.drawX; let top = placement.drawY; let right = placement.drawX + placement.width; @@ -150,6 +169,33 @@ const SizeDiffCompositor = (() => { return { minX, minY, maxX, maxY }; } + function unionBounds(a, b) { + if (!a) { + return b; + } + if (!b) { + return a; + } + return { + minX: Math.min(a.minX, b.minX), + minY: Math.min(a.minY, b.minY), + maxX: Math.max(a.maxX, b.maxX), + maxY: Math.max(a.maxY, b.maxY), + }; + } + + function expandBounds(bounds, { left = 0, top = 0, right = 0, bottom = 0 } = {}) { + if (!bounds) { + return bounds; + } + return { + minX: bounds.minX - left, + minY: bounds.minY - top, + maxX: bounds.maxX + right, + maxY: bounds.maxY + bottom, + }; + } + function maskPathFor(artPath) { const dot = artPath.lastIndexOf('.'); if (dot === -1) { @@ -180,7 +226,20 @@ const SizeDiffCompositor = (() => { (displayH - padY * 2) / contentH ); - offsetX = (displayW - contentW * scale) / 2 - minX * scale; + const centerX = fitOptions.centerX; + if (centerX != null) { + const halfLeft = Math.max(centerX - minX, 1); + const halfRight = Math.max(maxX - centerX, 1); + const scaleAnchor = Math.min( + (displayW / 2 - padX) / halfLeft, + (displayW / 2 - padX) / halfRight + ); + scale = Math.min(scale, scaleAnchor); + offsetX = displayW / 2 - centerX * scale; + } else { + offsetX = (displayW - contentW * scale) / 2 - minX * scale; + } + if (fitOptions.alignY === 'bottom') { offsetY = displayH - padY - maxY * scale; } else { @@ -286,21 +345,19 @@ const SizeDiffCompositor = (() => { /** * Draw a dimension line between world-space start/end. - * Label shows code + formatted length (from pixel distance / pixelsPerInch). + * Pass `inches` for the label when the value comes from the calculator; + * otherwise falls back to pixel distance / pixelsPerInch. */ function drawMeasurement(ctx, view, { label, start, end, - pixelsPerInch, + inches = null, + pixelsPerInch = null, color = '#444', capLength = 8, showValue = true, }) { - if (!pixelsPerInch) { - return; - } - const vs = view.getViewState(); const [x0, y0] = start; const [x1, y1] = end; @@ -311,8 +368,16 @@ const SizeDiffCompositor = (() => { return; } - const inches = SizeDiffUnits.worldToInches(length, pixelsPerInch); - const valueText = SizeDiffUnits.formatInches(inches); + let valueText; + if (inches != null) { + valueText = SizeDiffUnits.formatInches(inches); + } else if (pixelsPerInch) { + valueText = SizeDiffUnits.formatInches( + SizeDiffUnits.worldToInches(length, pixelsPerInch) + ); + } else { + return; + } const text = label ? (showValue ? `${label}: ${valueText}` : label) : valueText; @@ -396,7 +461,7 @@ const SizeDiffCompositor = (() => { .map((placement) => { const local = layerLocalCoords(world.x, world.y, placement); const label = placement.label ?? placement.layerKey ?? 'Layer'; - return `${label}: x=${local.x} y=${local.y}`; + return `${label}: ${local.x}, ${local.y} px`; }); const padding = 10; @@ -507,7 +572,10 @@ const SizeDiffCompositor = (() => { placementFromJoint, layerLocalCoords, placementLocalToWorld, + anchorJointOnPlacement, computePlacementBounds, + unionBounds, + expandBounds, drawLayer, drawMeasurement, drawAnnotation, diff --git a/app/static/js/size_diff/units.js b/app/static/js/size_diff/units.js index 0f953f4..83dff86 100644 --- a/app/static/js/size_diff/units.js +++ b/app/static/js/size_diff/units.js @@ -99,6 +99,23 @@ const SizeDiffUnits = (() => { return Number.isNaN(numeric) ? NaN : numeric; } + /** Read a named form field and parse it as a length in inches. */ + function parseFormLengthInput(form, fieldName) { + const el = form?.elements?.[fieldName] ?? form?.querySelector(`[name="${fieldName}"]`); + const raw = el?.value; + if (raw == null || !String(raw).trim()) { + return null; + } + const inches = parseLengthInput(raw); + return Number.isNaN(inches) ? null : inches; + } + + /** Format a form length field for display; returns empty string when unset. */ + function formatFormLengthInput(form, fieldName, options) { + const inches = parseFormLengthInput(form, fieldName); + return inches == null ? '' : formatInches(inches, options); + } + /** World-pixel distance for a real-world inch count. */ function inchesToWorld(inches, pixelsPerInch) { return inches * pixelsPerInch; @@ -130,6 +147,8 @@ const SizeDiffUnits = (() => { formatInches, formatRatio, parseLengthInput, + parseFormLengthInput, + formatFormLengthInput, inchesToWorld, worldToInches, pixelsPerInchFromReference, diff --git a/app/taur_calculator/taur_algorithms.js b/app/taur_calculator/taur_algorithms.js index 37f641d..80974ab 100644 --- a/app/taur_calculator/taur_algorithms.js +++ b/app/taur_calculator/taur_algorithms.js @@ -72,17 +72,17 @@ const VolnarsAlgorithm = (() => { function parseFormInputs(form) { const formData = new FormData(form); - const parse = SizeDiffUnits.parseLengthInput; + const length = (name) => SizeDiffUnits.parseFormLengthInput(form, name); const taurLengthRaw = formData.get('taur_length'); return { measurementType: formData.get('measurement_type') || 'vitruvian', - anthroHeight: parse(formData.get('anthro_height')), - speciesHeight: parse(formData.get('species_height')), - speciesLength: parse(formData.get('species_length')), - speciesTailLength: parse(formData.get('species_tail_length')), - taurFullHeight: parse(formData.get('taur_full_height')), + anthroHeight: length('anthro_height'), + speciesHeight: length('species_height'), + speciesLength: length('species_length'), + speciesTailLength: length('species_tail_length'), + taurFullHeight: length('taur_full_height'), speciesWeight: parseFloat(formData.get('species_weight')), - taurLength: taurLengthRaw ? parse(taurLengthRaw) : null, + taurLength: taurLengthRaw ? length('taur_length') : null, }; } @@ -104,7 +104,7 @@ const VolnarsAlgorithm = (() => { const inputs = parseFormInputs(form); for (const key of ['anthroHeight', 'speciesHeight', 'speciesLength', 'speciesTailLength', 'taurFullHeight', 'speciesWeight']) { - if (Number.isNaN(inputs[key])) { + if (inputs[key] == null || Number.isNaN(inputs[key])) { throw new Error(`Invalid number for ${key}`); } } @@ -130,11 +130,9 @@ const ManualAlgorithm = (() => { } function parseFormInputs(form) { - const formData = new FormData(form); - const parse = SizeDiffUnits.parseLengthInput; const values = {}; for (const [field, key] of DIMENSION_FIELDS) { - values[key] = parse(formData.get(field)); + values[key] = SizeDiffUnits.parseFormLengthInput(form, field); } return values; } @@ -157,7 +155,7 @@ const ManualAlgorithm = (() => { const values = parseFormInputs(form); for (const [, key] of DIMENSION_FIELDS) { - if (Number.isNaN(values[key])) { + if (values[key] == null || Number.isNaN(values[key])) { throw new Error(`Invalid number for ${key}`); } } @@ -177,7 +175,7 @@ const ManualAlgorithm = (() => { for (const [fieldId, value] of Object.entries(mapping)) { const input = document.getElementById(fieldId); if (input && value != null && !Number.isNaN(value)) { - input.value = value; + input.value = SizeDiffUnits.formatInches(value); } } } @@ -237,7 +235,7 @@ const TaurAlgorithms = (() => { return impl.validateForm(form); } - function formatResults(result, algorithmId = 'volnar') { + function formatResults(result, algorithmId = 'volnar', form = null) { const fmt = SizeDiffUnits.formatInches; const ratio = SizeDiffUnits.formatRatio; @@ -250,6 +248,13 @@ const TaurAlgorithms = (() => { TH: `${fmt(result.TH)} (Taur Height)`, }; + if (form?.elements?.show_rider?.checked) { + const riderHeight = SizeDiffUnits.parseFormLengthInput(form, 'rider_height'); + if (riderHeight != null) { + formatted.RH = `${fmt(riderHeight)} (Rider Height)`; + } + } + if (algorithmId === 'volnar') { formatted.AR = `${ratio(result.AR)} (Anthropic Ratio)`; formatted.TW = `${result.TW.toFixed(2)} lbs (Taur Weight)`; diff --git a/app/taur_calculator/taur_canvas.js b/app/taur_calculator/taur_canvas.js index 22e717e..2743aa8 100644 --- a/app/taur_calculator/taur_canvas.js +++ b/app/taur_calculator/taur_canvas.js @@ -1,25 +1,31 @@ function initTaurCanvas(config) { const { jointWorldPosition, + jointLocalPosition, placementFromJoint, placementLocalToWorld, + anchorJointOnPlacement, drawLayer, drawMeasurement, createCanvasView, - createSceneScale, attachLayerDebugHud, loadImages, computePlacementBounds, + unionBounds, + expandBounds, } = window.SizeDiffCompositor; const canvas = document.getElementById('taur-canvas'); const ctx = canvas.getContext('2d'); + const form = document.getElementById('taur-form'); const { taurData, layerSources, debug = false } = config; const BODY_LAYER_KEYS = new Set(['lBody', 'uBody', 'tail']); - const DEFAULT_BODY_COLOR = '#ff0000'; // Volnar red - const DEFAULT_RIDER_COLOR = '#ff8800'; // Felix orange + const DEFAULT_BODY_COLOR = '#ff0000'; + const DEFAULT_RIDER_COLOR = '#ff8800'; const TRIM_ART_COMMAND = 'python3 scripts/trim_art.py'; + const { placeScaledLayer } = TaurMeasurements; + const view = createCanvasView( canvas, taurData.canvas.width, @@ -45,13 +51,20 @@ function initTaurCanvas(config) { return document.getElementById('taur_rider_color')?.value || DEFAULT_RIDER_COLOR; } - function riderScale() { - const raw = document.getElementById('rider_height')?.value; - const height = parseFloat(raw); - if (!raw || Number.isNaN(height)) { - return 1; - } - return height / 100; + function scaled(layerKey, anchorJoint, worldX, worldY) { + return placeScaledLayer({ + layerKey, + layerDef: layerDef(layerKey), + image: layers[layerKey], + anchorJoint, + worldX, + worldY, + result: window.taurLastResult, + form, + placementFromJoint, + anchorJointOnPlacement, + jointLocalPosition, + }); } function buildPlacements() { @@ -75,36 +88,15 @@ function initTaurCanvas(config) { { layerKey: 'lBody' } ), }, - { - layerKey: 'uBody', - visible: true, - ...placementFromJoint( - layerDef('uBody'), layers.uBody, 'lower_attach', upperX, upperY, - { layerKey: 'uBody' } - ), - }, - { - layerKey: 'tail', - visible: true, - ...placementFromJoint( - layerDef('tail'), layers.tail, 'body_attach', tailX, tailY, - { layerKey: 'tail' } - ), - }, + scaled('uBody', 'lower_attach', upperX, upperY), + scaled('tail', 'body_attach', tailX, tailY), ]; if (showRider) { - const [riderX, riderY] = jointWorldPosition( - layerDef('uBody'), 'rider_attach', 'lower_attach', upperX, upperY - ); - placements.push({ - layerKey: 'rider', - visible: true, - ...placementFromJoint( - layerDef('rider'), layers.rider, 'mount_attach', riderX, riderY, - { layerKey: 'rider', scale: riderScale() } - ), - }); + const lBodyPlacement = placements.find((p) => p.layerKey === 'lBody'); + const [rx, ry] = jointLocalPosition(layerDef('lBody'), 'rider_attach'); + const [riderX, riderY] = placementLocalToWorld(lBodyPlacement, rx, ry); + placements.push(scaled('rider', 'seat_attach', riderX, riderY)); } return placements; @@ -118,12 +110,24 @@ function initTaurCanvas(config) { function drawScene() { latestPlacements = buildPlacements(); - const contentBounds = computePlacementBounds(latestPlacements); + const centerX = taurData.canvas.width / 2; + + let contentBounds = unionBounds( + computePlacementBounds(latestPlacements), + computePlacementBounds(latestPlacements, { ignoreScale: true }) + ); + contentBounds = expandBounds(contentBounds, { + left: 72, + top: 48, + right: 48, + bottom: 8, + }); view.update({ contentBounds, - paddingX: 12, - paddingY: 12, + centerX, + paddingX: 20, + paddingY: 20, alignY: 'bottom', }); view.clear(ctx); @@ -147,7 +151,6 @@ function initTaurCanvas(config) { drawLayer(ctx, image, placement, { color, mask }); } - // Draw measurement lines if the checkbox is checked const showLines = document.getElementById('show_measurements')?.checked ?? false; if (showLines) { const measurementScene = TaurMeasurements.buildScene({ @@ -156,16 +159,12 @@ function initTaurCanvas(config) { groundY, result: window.taurLastResult, placementLocalToWorld, - createSceneScale, }); - if (measurementScene?.scale.pixelsPerInch) { + if (measurementScene) { view.applyWorldTransform(ctx); for (const line of measurementScene.measurements) { - drawMeasurement(ctx, view, { - ...line, - pixelsPerInch: measurementScene.scale.pixelsPerInch, - }); + drawMeasurement(ctx, view, line); } } } diff --git a/app/taur_calculator/taur_data.json b/app/taur_calculator/taur_data.json index 5b90ada..2fc5ac4 100644 --- a/app/taur_calculator/taur_data.json +++ b/app/taur_calculator/taur_data.json @@ -12,20 +12,26 @@ "joints": { "floor": [1452, 2487], "upper_attach": [738, 1481], - "tail_attach": [1386, 1453] + "tail_attach": [1386, 1453], + "rider_attach": [1133, 1543] } }, "uBody": { "path": "Hunner/Taur/Side_Taur_Hight_Refrence_U_BODY_Volnar_for_Vixi.png", "trim": { "x": 137, "y": 616, "width": 722, "height": 1242 }, + "scale_center": [566, 894], + "scale_from": "TFH", + "reference_inches": 108, "joints": { - "lower_attach": [738, 1481], - "rider_attach": [979, 2017] + "lower_attach": [738, 1481] } }, "tail": { "path": "Hunner/Taur/Side_Taur_Hight_Refrence_TAIL_Volnar_for_Vixi.png", "trim": { "x": 1104, "y": 785, "width": 1863, "height": 1461 }, + "scale_center": [440, 705], + "scale_from": "TT", + "reference_inches": 36, "joints": { "body_attach": [1386, 1453] } @@ -33,8 +39,11 @@ "rider": { "path": "Hunner/Taur/Side_Taur_Hight_Refrence_RIDER_Volnar_Felix_for_Vixi.png", "trim": { "x": 852, "y": 637, "width": 970, "height": 1381 }, - "scale_center": [300, 875], + "scale_center": [260, 900], + "scale_from": "form:rider_height", + "reference_inches": 100, "joints": { + "seat_attach": [1112, 1537], "mount_attach": [979, 2017] } } diff --git a/app/taur_calculator/taur_form.js b/app/taur_calculator/taur_form.js index 633febf..b0bbf87 100644 --- a/app/taur_calculator/taur_form.js +++ b/app/taur_calculator/taur_form.js @@ -67,7 +67,7 @@ function initTaurForm(config = {}) { const algorithmId = TaurAlgorithms.currentId(form); const { raw } = TaurAlgorithms.calculateFromForm(form); window.taurLastResult = raw; - renderResults(TaurAlgorithms.formatResults(raw, algorithmId)); + renderResults(TaurAlgorithms.formatResults(raw, algorithmId, form)); redrawCanvas(); } catch { resultsEl.hidden = true; @@ -147,6 +147,7 @@ function initTaurForm(config = {}) { return; } if (name === 'show_rider') { + scheduleApply(); return; } scheduleApply(); diff --git a/app/taur_calculator/taur_measurements.js b/app/taur_calculator/taur_measurements.js index 97e7929..bcf3677 100644 --- a/app/taur_calculator/taur_measurements.js +++ b/app/taur_calculator/taur_measurements.js @@ -1,9 +1,71 @@ /** - * Resolve measurement anchors from taur_data.json onto the canvas. + * Measurement anchors and scaled layer placement. */ const TaurMeasurements = (() => { const ORDER = ['TFH', 'THe', 'TTo', 'TL', 'TT']; - const CALIBRATION_ORDER = ['TFH', 'THe']; + + function scaleValue(layerDef, result, form) { + const source = layerDef.scale_from; + if (source.startsWith('form:')) { + return SizeDiffUnits.parseFormLengthInput(form, source.slice(5)); + } + return result?.[source]; + } + + function scaleForLayer(layerDef, result, form) { + if (!layerDef?.scale_from || !layerDef.reference_inches) { + return 1; + } + const value = scaleValue(layerDef, result, form); + if (value == null || Number.isNaN(value)) { + return 1; + } + return value / layerDef.reference_inches; + } + + /** + * Scale around scale_center (or scale_pivot), then pin anchorJoint to world coords. + * Falls back to the anchor joint when no explicit pivot is defined. + */ + function placeScaledLayer({ + layerKey, + layerDef, + image, + anchorJoint, + worldX, + worldY, + result, + form, + placementFromJoint, + anchorJointOnPlacement, + jointLocalPosition, + }) { + const anchorLocal = jointLocalPosition(layerDef, anchorJoint); + const scaleCenter = layerDef.scale_center ?? layerDef.scale_pivot ?? anchorLocal; + + return anchorJointOnPlacement( + { + layerKey, + visible: true, + ...placementFromJoint( + layerDef, + image, + anchorJoint, + worldX, + worldY, + { + layerKey, + scale: scaleForLayer(layerDef, result, form), + scaleCenter, + } + ), + }, + layerDef, + anchorJoint, + worldX, + worldY + ); + } function resolveLayerPoint(placements, layerKey, localX, localY, placementLocalToWorld) { const placement = placements.find((p) => p.layerKey === layerKey); @@ -25,20 +87,17 @@ const TaurMeasurements = (() => { function fromDef(key, def, placements, groundY, result, placementLocalToWorld) { const refKey = def.reference ?? key; - const referenceInches = result[refKey]; - if (referenceInches == null) { + const inches = result[refKey]; + if (inches == null) { return null; } - const startSpec = def.start; - const endSpec = def.end; - - let start = resolveEndpoint(startSpec, placements, groundY, null, placementLocalToWorld); + let start = resolveEndpoint(def.start, placements, groundY, null, placementLocalToWorld); if (!start) { return null; } - let end = resolveEndpoint(endSpec, placements, groundY, start, placementLocalToWorld); + let end = resolveEndpoint(def.end, placements, groundY, start, placementLocalToWorld); if (!end) { return null; } @@ -48,56 +107,30 @@ const TaurMeasurements = (() => { end = [end[0] + def.offset[0], end[1] + def.offset[1]]; } - return { key, label: key, start, end, referenceInches }; + return { label: key, start, end, inches }; } - function buildScene({ - measurementDefs, - placements, - groundY, - result, - placementLocalToWorld, - createSceneScale, - }) { + function buildScene({ measurementDefs, placements, groundY, result, placementLocalToWorld }) { if (!result) { return null; } - const enabled = []; + const measurements = []; for (const key of ORDER) { const def = measurementDefs[key]; if (!def) { continue; } - const measurement = fromDef( - key, def, placements, groundY, result, placementLocalToWorld - ); - if (measurement) { - enabled.push(measurement); + const line = fromDef(key, def, placements, groundY, result, placementLocalToWorld); + if (line) { + measurements.push(line); } } - if (enabled.length === 0) { - return null; - } - - const calibrator = CALIBRATION_ORDER - .map((key) => enabled.find((m) => m.key === key)) - .find(Boolean) ?? enabled[0]; - - const worldDistance = SizeDiffUnits.worldDistance(calibrator.start, calibrator.end); - const scale = createSceneScale.fromReference( - calibrator.referenceInches, - worldDistance - ); - - return { - scale, - measurements: enabled.map(({ label, start, end }) => ({ label, start, end })), - }; + return measurements.length ? { measurements } : null; } - return { buildScene }; + return { buildScene, scaleForLayer, placeScaledLayer }; })(); if (typeof window !== 'undefined') { diff --git a/app/templates/taur.html b/app/templates/taur.html index 2a9fab1..677604f 100644 --- a/app/templates/taur.html +++ b/app/templates/taur.html @@ -72,34 +72,34 @@

Calculation Results

- + title="Feet and inches (e.g. 6'4") or cm (e.g. 193cm)">ℹ️ +
- + title="Feet and inches (e.g. 3'0") or cm (e.g. 91cm)">ℹ️ +
- + title="Feet and inches (e.g. 4'0") or cm (e.g. 122cm)">ℹ️ +
- + title="Feet and inches (e.g. 2'0") or cm (e.g. 61cm)">ℹ️ +
- + title="Pawpads to top of head, excluding horns/ears/antlers. Feet and inches (e.g. 9'0") or cm (e.g. 274cm)">ℹ️ +
@@ -110,48 +110,48 @@

Calculation Results

- ℹ️ +
- +
@@ -171,9 +171,9 @@

Calculation Results

-

Taur Calculator

+

CalculaTAUR

By Vixi, Volnar and Snow! With art by Hunner!

From a4bc4e5b9f1d13d535d238f7ea2b38208caf69d0 Mon Sep 17 00:00:00 2001 From: Snowsune Date: Mon, 22 Jun 2026 01:31:17 -0400 Subject: [PATCH 08/15] Fix required fields --- app/static/css/taur_app.css | 22 +++++++++++ app/static/js/size_diff/compositor.js | 15 +++++-- app/taur_calculator/taur_algorithms.js | 31 ++++++++++++++- app/taur_calculator/taur_canvas.js | 28 +++++++++---- app/taur_calculator/taur_form.js | 54 ++++++++++++++++++++++++-- app/templates/taur.html | 2 +- 6 files changed, 135 insertions(+), 17 deletions(-) diff --git a/app/static/css/taur_app.css b/app/static/css/taur_app.css index 1d53283..ec896a0 100644 --- a/app/static/css/taur_app.css +++ b/app/static/css/taur_app.css @@ -192,6 +192,18 @@ border-color: #f80; } +.taur-form-group input.taur-field-missing, +.taur-form-group select.taur-field-missing { + border-color: #e55; + background: #2a1818; + box-shadow: 0 0 0 1px rgba(229, 85, 85, 0.35); +} + +.taur-form-group input.taur-field-missing:focus, +.taur-form-group select.taur-field-missing:focus { + border-color: #f88; +} + .taur-form-group input[type="color"] { width: 100%; height: clamp(1.6rem, 3vh, 2.2rem); @@ -208,6 +220,16 @@ margin-bottom: 0; } +.taur-rider-toggle label.taur-toggle-disabled { + color: #666; + cursor: not-allowed; + opacity: 0.55; +} + +.taur-rider-toggle label.taur-toggle-disabled input { + cursor: not-allowed; +} + .taur-rider-toggle input[type="checkbox"] { width: auto; margin: 0; diff --git a/app/static/js/size_diff/compositor.js b/app/static/js/size_diff/compositor.js index 9e5a19a..7f405ab 100644 --- a/app/static/js/size_diff/compositor.js +++ b/app/static/js/size_diff/compositor.js @@ -4,16 +4,23 @@ * Attach to window.SizeDiffCompositor */ const SizeDiffCompositor = (() => { + function jointPoint(layerDef, jointName) { + const point = layerDef?.joints?.[jointName]; + if (!point) { + throw new Error(`Unknown joint "${jointName}"`); + } + return point; + } + function jointLocalPosition(layerDef, jointName) { - const [jx, jy] = layerDef.joints[jointName]; + const [jx, jy] = jointPoint(layerDef, jointName); const trim = layerDef.trim; return [jx - trim.x, jy - trim.y]; } function jointWorldPosition(layerDef, jointName, placedAtJoint, worldX, worldY) { - const joints = layerDef.joints; - const [jx, jy] = joints[jointName]; - const [px, py] = joints[placedAtJoint]; + const [jx, jy] = jointPoint(layerDef, jointName); + const [px, py] = jointPoint(layerDef, placedAtJoint); return [worldX + (jx - px), worldY + (jy - py)]; } diff --git a/app/taur_calculator/taur_algorithms.js b/app/taur_calculator/taur_algorithms.js index 80974ab..064956a 100644 --- a/app/taur_calculator/taur_algorithms.js +++ b/app/taur_calculator/taur_algorithms.js @@ -112,7 +112,7 @@ const VolnarsAlgorithm = (() => { return { raw: calculate(inputs) }; } - return { calculate, calculateFromForm, validateForm }; + return { calculate, calculateFromForm, validateForm, requiredFieldNames: () => [...REQUIRED_FIELDS] }; })(); const ManualAlgorithm = (() => { @@ -180,7 +180,13 @@ const ManualAlgorithm = (() => { } } - return { calculate, calculateFromForm, validateForm, fillFromResult }; + return { + calculate, + calculateFromForm, + validateForm, + fillFromResult, + requiredFieldNames: () => DIMENSION_FIELDS.map(([field]) => field), + }; })(); const TaurAlgorithms = (() => { @@ -235,6 +241,25 @@ const TaurAlgorithms = (() => { return impl.validateForm(form); } + function requiredFieldNames(form) { + const impl = get(currentId(form)); + return impl?.requiredFieldNames?.() ?? []; + } + + function missingRequiredFields(form) { + return requiredFieldNames(form).filter((name) => { + const el = form.elements?.[name] ?? form.querySelector(`[name="${name}"]`); + if (!el) { + return false; + } + const panel = el.closest('.taur-algorithm-panel'); + if (panel?.hidden) { + return false; + } + return !String(el.value ?? '').trim(); + }); + } + function formatResults(result, algorithmId = 'volnar', form = null) { const fmt = SizeDiffUnits.formatInches; const ratio = SizeDiffUnits.formatRatio; @@ -270,6 +295,8 @@ const TaurAlgorithms = (() => { currentId, get, shareFieldNames, + requiredFieldNames, + missingRequiredFields, FORM_FIELDS, }; })(); diff --git a/app/taur_calculator/taur_canvas.js b/app/taur_calculator/taur_canvas.js index 2743aa8..76c8ffa 100644 --- a/app/taur_calculator/taur_canvas.js +++ b/app/taur_calculator/taur_canvas.js @@ -67,16 +67,21 @@ function initTaurCanvas(config) { }); } + function hasJoint(layerKey, jointName) { + return Boolean(taurData.layers[layerKey]?.joints?.[jointName]); + } + function buildPlacements() { const centerX = taurData.canvas.width / 2; const groundY = floorY(); const showRider = document.getElementById('show_rider')?.checked; + const lBodyDef = layerDef('lBody'); const [upperX, upperY] = jointWorldPosition( - layerDef('lBody'), 'upper_attach', 'floor', centerX, groundY + lBodyDef, 'upper_attach', 'floor', centerX, groundY ); const [tailX, tailY] = jointWorldPosition( - layerDef('lBody'), 'tail_attach', 'floor', centerX, groundY + lBodyDef, 'tail_attach', 'floor', centerX, groundY ); const placements = [ @@ -84,7 +89,7 @@ function initTaurCanvas(config) { layerKey: 'lBody', visible: true, ...placementFromJoint( - layerDef('lBody'), layers.lBody, 'floor', centerX, groundY, + lBodyDef, layers.lBody, 'floor', centerX, groundY, { layerKey: 'lBody' } ), }, @@ -92,11 +97,15 @@ function initTaurCanvas(config) { scaled('tail', 'body_attach', tailX, tailY), ]; - if (showRider) { + if (showRider && hasJoint('lBody', 'rider_attach') && hasJoint('rider', 'seat_attach')) { const lBodyPlacement = placements.find((p) => p.layerKey === 'lBody'); - const [rx, ry] = jointLocalPosition(layerDef('lBody'), 'rider_attach'); + const [rx, ry] = jointLocalPosition(lBodyDef, 'rider_attach'); const [riderX, riderY] = placementLocalToWorld(lBodyPlacement, rx, ry); placements.push(scaled('rider', 'seat_attach', riderX, riderY)); + } else if (showRider) { + console.warn( + 'Rider not shown: missing lBody.rider_attach or rider.seat_attach in taur_data.json' + ); } return placements; @@ -109,7 +118,12 @@ function initTaurCanvas(config) { }); function drawScene() { - latestPlacements = buildPlacements(); + try { + latestPlacements = buildPlacements(); + } catch (err) { + console.error('Taur buildPlacements failed:', err); + return; + } const centerX = taurData.canvas.width / 2; let contentBounds = unionBounds( @@ -152,7 +166,7 @@ function initTaurCanvas(config) { } const showLines = document.getElementById('show_measurements')?.checked ?? false; - if (showLines) { + if (showLines && window.taurLastResult) { const measurementScene = TaurMeasurements.buildScene({ measurementDefs: taurData.measurements ?? {}, placements: latestPlacements, diff --git a/app/taur_calculator/taur_form.js b/app/taur_calculator/taur_form.js index b0bbf87..4d0b318 100644 --- a/app/taur_calculator/taur_form.js +++ b/app/taur_calculator/taur_form.js @@ -54,11 +54,30 @@ function initTaurForm(config = {}) { shareUrlInput.select(); } + function updateRequiredFieldHighlights() { + const missing = new Set(TaurAlgorithms.missingRequiredFields(form)); + + for (const name of TaurAlgorithms.requiredFieldNames(form)) { + const input = document.getElementById(name) || form.querySelector(`[name="${name}"]`); + if (!input) { + continue; + } + const panel = input.closest('.taur-algorithm-panel'); + if (panel?.hidden) { + input.classList.remove('taur-field-missing'); + continue; + } + input.classList.toggle('taur-field-missing', missing.has(name)); + } + } + function applyFormState() { const missing = TaurAlgorithms.validateForm(form); if (missing !== null) { resultsEl.hidden = true; window.taurLastResult = null; + updateMeasurementControls(); + updateRequiredFieldHighlights(); redrawCanvas(); return; } @@ -68,10 +87,14 @@ function initTaurForm(config = {}) { const { raw } = TaurAlgorithms.calculateFromForm(form); window.taurLastResult = raw; renderResults(TaurAlgorithms.formatResults(raw, algorithmId, form)); + updateMeasurementControls(); + updateRequiredFieldHighlights(); redrawCanvas(); } catch { resultsEl.hidden = true; window.taurLastResult = null; + updateMeasurementControls(); + updateRequiredFieldHighlights(); redrawCanvas(); } } @@ -89,6 +112,29 @@ function initTaurForm(config = {}) { if (isManual && window.taurLastResult) { ManualAlgorithm.fillFromResult(window.taurLastResult); } + + updateRequiredFieldHighlights(); + } + + function updateMeasurementControls() { + const checkbox = document.getElementById('show_measurements'); + const label = document.getElementById('show_measurements-label'); + const hasDimensions = window.taurLastResult != null; + + if (!checkbox) { + return; + } + + checkbox.disabled = !hasDimensions; + label?.classList.toggle('taur-toggle-disabled', !hasDimensions); + label?.setAttribute( + 'title', + hasDimensions ? '' : 'Fill in dimensions to show measurement lines' + ); + + if (!hasDimensions && checkbox.checked) { + checkbox.checked = false; + } } function updateRiderControls() { @@ -143,10 +189,13 @@ function initTaurForm(config = {}) { form.addEventListener('change', (event) => { const { name } = event.target; if (name === 'show_measurements') { - redrawCanvas(); + if (!event.target.disabled) { + redrawCanvas(); + } return; } if (name === 'show_rider') { + updateRiderControls(); scheduleApply(); return; } @@ -163,8 +212,6 @@ function initTaurForm(config = {}) { scheduleApply(); }); - document.getElementById('show_rider')?.addEventListener('change', updateRiderControls); - shareBtn?.addEventListener('click', shareLink); shareCopyBtn?.addEventListener('click', async () => { shareLink(); @@ -179,6 +226,7 @@ function initTaurForm(config = {}) { const fromUrl = populateFromUrl(); updateAlgorithmControls(); + updateMeasurementControls(); updateRiderControls(); if (!fromUrl) { diff --git a/app/templates/taur.html b/app/templates/taur.html index f977b0e..3deaf49 100644 --- a/app/templates/taur.html +++ b/app/templates/taur.html @@ -156,7 +156,7 @@

Calculation Results

-
@@ -50,17 +51,45 @@

Calculation Results

-
-
- - +
+

+ Thank you snow! +

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
@@ -94,29 +123,9 @@

Calculation Results

- -
- - -
- -
- - -
- -
- - -
- - - - - - -
@@ -137,21 +124,21 @@

Calculation Results

-

Set upper body height, lower body height, and tail length. Rider height is only needed when showing a rider. Feet and inches (e.g. 9'0") or cm (e.g. 274cm).

+

Set lower body height, taur full height, and tail length. Upper body is computed as full height minus lower body. Rider height is only needed when showing a rider. Feet and inches (e.g. 9'0") or cm (e.g. 274cm).

- - + +
- - + +
- +
From 8faf982b7a1b3cdcf7d043732d1240cfb8909032 Mon Sep 17 00:00:00 2001 From: Snowsune Date: Mon, 22 Jun 2026 12:08:19 -0400 Subject: [PATCH 14/15] Calculataurrrr but with fixed scene scaling --- app/taur_calculator/manual_algorithm.js | 26 +++++++++++ app/taur_calculator/taur_canvas.js | 55 ++++++++++++++++-------- app/taur_calculator/taur_data.json | 2 +- app/taur_calculator/taur_form.js | 34 ++++++++++++--- app/taur_calculator/taur_measurements.js | 49 +++++++++++++++------ app/templates/taur.html | 2 +- 6 files changed, 131 insertions(+), 37 deletions(-) diff --git a/app/taur_calculator/manual_algorithm.js b/app/taur_calculator/manual_algorithm.js index e71654b..1947047 100644 --- a/app/taur_calculator/manual_algorithm.js +++ b/app/taur_calculator/manual_algorithm.js @@ -10,6 +10,14 @@ const ManualAlgorithm = (() => { const RIDER_FIELD = ['rider_height', 'RH']; + /** Default inches when entering manual mode for the first time. */ + const DEFAULT_FIELD_INCHES = { + manual_lower_body_height: 54, + manual_taur_full_height: 128, + manual_tail_length: 84, + rider_height: 72, + }; + function activeFields(form) { return form?.elements?.show_rider?.checked ? [...DIMENSION_FIELDS, RIDER_FIELD] @@ -65,9 +73,27 @@ const ManualAlgorithm = (() => { } } + function applyDefaults() { + for (const [fieldId, inches] of Object.entries(DEFAULT_FIELD_INCHES)) { + const input = document.getElementById(fieldId); + if (input) { + input.value = SizeDiffUnits.formatInches(inches); + } + } + } + + function dimensionFieldsEmpty() { + return DIMENSION_FIELDS.every(([fieldId]) => { + const input = document.getElementById(fieldId); + return !String(input?.value ?? '').trim(); + }); + } + return { ...core, fillFromResult, + applyDefaults, + dimensionFieldsEmpty, }; })(); diff --git a/app/taur_calculator/taur_canvas.js b/app/taur_calculator/taur_canvas.js index daea160..8939fb0 100644 --- a/app/taur_calculator/taur_canvas.js +++ b/app/taur_calculator/taur_canvas.js @@ -160,6 +160,11 @@ function initTaurCanvas(config) { y += fonts.heading + fonts.lineHeight * 0.4; for (const { key, valueLines } of rowLayouts) { + const rowColor = key === 'RH' && document.getElementById('show_rider')?.checked + ? riderColor() + : color; + targetCtx.fillStyle = rowColor; + targetCtx.font = `bold ${fonts.body}px sans-serif`; targetCtx.fillText(key, x, y); @@ -200,25 +205,14 @@ function initTaurCanvas(config) { const groundY = floorY(); const showRider = document.getElementById('show_rider')?.checked; const lBodyDef = layerDef('lBody'); - const result = window.taurLastResult; - const calPpi = TaurMeasurements.calibrationPixelsPerInch(taurData.canvas) ?? 1; + const scenePpi = TaurMeasurements.scenePixelsPerInch(taurData.canvas) ?? 1; - // Body layers use the art calibration ruler; tail/rider use the live TFH line. - const lBodyPlacement = scaled('lBody', 'floor', centerX, groundY, calPpi); + const lBodyPlacement = scaled('lBody', 'floor', centerX, groundY, scenePpi); const placements = [lBodyPlacement]; const [lx, ly] = jointLocalPosition(lBodyDef, 'upper_attach'); const [upperX, upperY] = placementLocalToWorld(lBodyPlacement, lx, ly); - placements.push(scaled('uBody', 'lower_attach', upperX, upperY, calPpi)); - - const livePpi = TaurMeasurements.deriveScenePixelsPerInch({ - canvasConfig: taurData.canvas ?? {}, - measurementDefs: taurData.measurements ?? {}, - placements, - groundY, - result, - placementLocalToWorld, - }) ?? calPpi; + placements.push(scaled('uBody', 'lower_attach', upperX, upperY, scenePpi)); function lBodyJointWorld(jointName) { const [jx, jy] = jointLocalPosition(lBodyDef, jointName); @@ -226,11 +220,11 @@ function initTaurCanvas(config) { } const [tailX, tailY] = lBodyJointWorld('tail_attach'); - placements.push(scaled('tail', 'body_attach', tailX, tailY, livePpi)); + placements.push(scaled('tail', 'body_attach', tailX, tailY, scenePpi)); if (showRider && hasJoint('lBody', 'rider_attach') && hasJoint('rider', 'seat_attach')) { const [riderX, riderY] = lBodyJointWorld('rider_attach'); - placements.push(scaled('rider', 'seat_attach', riderX, riderY, livePpi)); + placements.push(scaled('rider', 'seat_attach', riderX, riderY, scenePpi)); } else if (showRider) { console.warn( 'Rider not shown: missing lBody.rider_attach or rider.seat_attach in taur_data.json' @@ -300,7 +294,34 @@ function initTaurCanvas(config) { } } - if (drawDebug) { + if (drawDebug && window.taurLastResult) { + debugHud.draw(targetCtx); + + const scenePpi = TaurMeasurements.scenePixelsPerInch(taurData.canvas); + const measuredTfhPpi = TaurMeasurements.measuredTfhPixelsPerInch({ + canvasConfig: taurData.canvas ?? {}, + measurementDefs: taurData.measurements ?? {}, + placements, + groundY, + result: window.taurLastResult, + placementLocalToWorld, + }); + const ttWorld = TaurMeasurements.measureDefinitionWorldDistance( + taurData.measurements?.TT, + placements, + groundY, + placementLocalToWorld + ); + const { TFH, TT } = window.taurLastResult; + console.debug('Taur scene ruler', { + scenePpi, + measuredTfhPpi, + tfhWorld: measuredTfhPpi != null && TFH != null ? measuredTfhPpi * TFH : null, + tfhExpected: TFH != null && scenePpi != null ? TFH * scenePpi : null, + ttWorld, + ttExpected: TT != null && scenePpi != null ? TT * scenePpi : null, + }); + } else if (drawDebug) { debugHud.draw(targetCtx); } diff --git a/app/taur_calculator/taur_data.json b/app/taur_calculator/taur_data.json index 28c024d..76263b6 100644 --- a/app/taur_calculator/taur_data.json +++ b/app/taur_calculator/taur_data.json @@ -28,7 +28,7 @@ "trim": { "x": 137, "y": 616, "width": 722, "height": 1242 }, "scale_center": [566, 894], "scale_from": "UBH", - "span_pixels": 1885, + "span_pixels": 1401, "joints": { "lower_attach": [738, 1481] } diff --git a/app/taur_calculator/taur_form.js b/app/taur_calculator/taur_form.js index dd62180..6b427b5 100644 --- a/app/taur_calculator/taur_form.js +++ b/app/taur_calculator/taur_form.js @@ -9,6 +9,28 @@ function initTaurForm(config = {}) { const speciesData = config.speciesData ?? {}; let debounceTimer = null; + let manualModeInitialized = false; + let previousAlgorithm = algorithmSelect.value; + + function applyManualEntryDefaults() { + if (manualModeInitialized) { + return; + } + ManualAlgorithm.applyDefaults(); + manualModeInitialized = true; + } + + function onAlgorithmChange() { + const algo = algorithmSelect.value; + if (algo === 'manual' && previousAlgorithm !== 'manual') { + if (!manualModeInitialized) { + applyManualEntryDefaults(); + } else if (window.taurLastResult) { + ManualAlgorithm.fillFromResult(window.taurLastResult); + } + } + previousAlgorithm = algo; + } function redrawCanvas() { document.getElementById('taur-canvas')?.dispatchEvent( @@ -162,10 +184,6 @@ function initTaurForm(config = {}) { ); }); - if (algo === 'manual' && window.taurLastResult) { - ManualAlgorithm.fillFromResult(window.taurLastResult); - } - updateRequiredFieldHighlights(); } @@ -276,7 +294,7 @@ function initTaurForm(config = {}) { if (event.target.name === 'show_measurements') { return; } - if (event.target.name === 'taur_body_color') { + if (event.target.name === 'taur_body_color' || event.target.name === 'taur_rider_color') { redrawCanvas(); return; } @@ -308,6 +326,7 @@ function initTaurForm(config = {}) { return; } if (name === 'algorithm') { + onAlgorithmChange(); updateAlgorithmControls(); updateRiderControls(); scheduleApply(); @@ -336,6 +355,11 @@ function initTaurForm(config = {}) { }); const fromUrl = populateFromUrl(); + if (fromUrl && algorithmSelect.value === 'manual') { + manualModeInitialized = true; + } else if (algorithmSelect.value === 'manual' && ManualAlgorithm.dimensionFieldsEmpty()) { + applyManualEntryDefaults(); + } clearResultsOverlay(); updateAlgorithmControls(); updateMeasurementControls(); diff --git a/app/taur_calculator/taur_measurements.js b/app/taur_calculator/taur_measurements.js index d2a462d..2ac5b0e 100644 --- a/app/taur_calculator/taur_measurements.js +++ b/app/taur_calculator/taur_measurements.js @@ -1,5 +1,13 @@ /** * Measurement anchors and scaled layer placement. + * + * Scene ruler: one world pixels-per-inch value for every layer. + * scale = dimension_inches × scenePixelsPerInch / layer.span_pixels + * + * span_pixels must match the pixel length of that layer's measurement line in + * source art (see measurements.* in taur_data.json). Mixing rulers (e.g. cal ppi + * on body but a live TFH-derived ppi on tail) makes independent dimensions + * drag each other when only one input changes. */ const TaurMeasurements = (() => { const ORDER = ['TFH', 'THe', 'TTo', 'TL', 'TT']; @@ -29,6 +37,11 @@ const TaurMeasurements = (() => { return null; } + /** Canonical scene ruler — same ppi for every layer. */ + function scenePixelsPerInch(canvasConfig = {}) { + return calibrationPixelsPerInch(canvasConfig); + } + /** Layer scale: inches * scene pixels-per-inch / art span in source pixels. */ function scaleForLayer(layerDef, result, pixelsPerInch) { if (!layerDef?.scale_from || pixelsPerInch == null || !layerDef.span_pixels) { @@ -80,8 +93,8 @@ const TaurMeasurements = (() => { return SizeDiffUnits.worldDistance(start, end); } - /** Read the live TFH line length and derive world pixels per inch. */ - function deriveScenePixelsPerInch({ + /** Measured TFH line ÷ stated TFH — for debug/calibration checks only. */ + function measuredTfhPixelsPerInch({ canvasConfig = {}, measurementDefs = {}, placements, @@ -90,19 +103,27 @@ const TaurMeasurements = (() => { placementLocalToWorld, }) { const tfhInches = result?.TFH; - if (tfhInches) { - const tfhDistance = measureDefinitionWorldDistance( - measurementDefs.TFH, - placements, - groundY, - placementLocalToWorld - ); - if (tfhDistance) { - return tfhDistance / tfhInches; - } + if (!tfhInches) { + return null; } - return calibrationPixelsPerInch(canvasConfig); + const tfhDistance = measureDefinitionWorldDistance( + measurementDefs.TFH, + placements, + groundY, + placementLocalToWorld + ); + if (!tfhDistance) { + return null; + } + + return tfhDistance / tfhInches; + } + + /** @deprecated Use scenePixelsPerInch for placement; this is for diagnostics. */ + function deriveScenePixelsPerInch(options) { + return measuredTfhPixelsPerInch(options) + ?? calibrationPixelsPerInch(options.canvasConfig ?? {}); } /** @@ -197,7 +218,9 @@ const TaurMeasurements = (() => { buildScene, scaleForLayer, placeScaledLayer, + scenePixelsPerInch, calibrationPixelsPerInch, + measuredTfhPixelsPerInch, deriveScenePixelsPerInch, measureDefinitionWorldDistance, }; diff --git a/app/templates/taur.html b/app/templates/taur.html index e5aeb3b..5c72394 100644 --- a/app/templates/taur.html +++ b/app/templates/taur.html @@ -124,7 +124,7 @@

CalculaTAUR

-

Set lower body height, taur full height, and tail length. Upper body is computed as full height minus lower body. Rider height is only needed when showing a rider. Feet and inches (e.g. 9'0") or cm (e.g. 274cm).

+

Set all your heights manually! Feet and inches (e.g. 9'0") or cm (e.g. 274cm).

From 33914b7a91967c023919d450d7b0883ccbc330f3 Mon Sep 17 00:00:00 2001 From: Snowsune Date: Mon, 22 Jun 2026 12:49:22 -0400 Subject: [PATCH 15/15] Add some more padding --- app/shares/taur_params.py | 7 +++- app/taur_calculator/manual_algorithm.js | 2 +- app/taur_calculator/taur_algorithms.js | 38 +++++++++++++++---- app/taur_calculator/taur_canvas.js | 48 +++++++++++++++++++----- app/taur_calculator/taur_data.json | 16 ++++++-- app/taur_calculator/taur_form.js | 18 ++++++++- app/taur_calculator/taur_measurements.js | 15 +------- app/templates/taur.html | 20 +++++++++- 8 files changed, 123 insertions(+), 41 deletions(-) diff --git a/app/shares/taur_params.py b/app/shares/taur_params.py index a967c39..46864f6 100644 --- a/app/shares/taur_params.py +++ b/app/shares/taur_params.py @@ -16,6 +16,9 @@ "show_rider", "rider_height", "taur_rider_color", + "show_standing", + "standing_height", + "taur_standing_color", }) ALGORITHM_FIELDS = { @@ -110,12 +113,12 @@ def _validate_field(key: str, value: str, species_names: set[str]) -> str | None return "name too long" return None - if key in {"show_measurements", "show_rider"}: + if key in {"show_measurements", "show_rider", "show_standing"}: if not CHECKBOX_RE.match(value): return f"invalid {key}" return None - if key in {"taur_body_color", "taur_rider_color"}: + if key in {"taur_body_color", "taur_rider_color", "taur_standing_color"}: if not COLOR_RE.match(value): return f"invalid {key}" return None diff --git a/app/taur_calculator/manual_algorithm.js b/app/taur_calculator/manual_algorithm.js index 1947047..8fe26c9 100644 --- a/app/taur_calculator/manual_algorithm.js +++ b/app/taur_calculator/manual_algorithm.js @@ -13,7 +13,7 @@ const ManualAlgorithm = (() => { /** Default inches when entering manual mode for the first time. */ const DEFAULT_FIELD_INCHES = { manual_lower_body_height: 54, - manual_taur_full_height: 128, + manual_taur_full_height: 108, manual_tail_length: 84, rider_height: 72, }; diff --git a/app/taur_calculator/taur_algorithms.js b/app/taur_calculator/taur_algorithms.js index 939c5fb..1f69129 100644 --- a/app/taur_calculator/taur_algorithms.js +++ b/app/taur_calculator/taur_algorithms.js @@ -8,6 +8,7 @@ const TaurAlgorithms = (() => { shared: [ 'name', 'algorithm', 'taur_body_color', 'show_measurements', 'show_rider', 'rider_height', 'taur_rider_color', + 'show_standing', 'standing_height', 'taur_standing_color', ], volnar: [ 'measurement_type', 'species', 'anthro_height', 'species_height', @@ -25,7 +26,7 @@ const TaurAlgorithms = (() => { }; /** Dimensions shown in manual-mode overlay; canvas uses TFH, TH, UBH, TT, RH. */ - const DISPLAY_KEYS = ['UBH', 'TFH', 'TH', 'TT', 'RH']; + const DISPLAY_KEYS = ['UBH', 'TFH', 'TH', 'TT', 'RH', 'StH']; const DISPLAY_LABELS = { UBH: 'Upper Body Height', @@ -33,6 +34,7 @@ const TaurAlgorithms = (() => { TH: 'Lower Body Height', TT: 'Tail Length', RH: 'Rider Height', + StH: 'Standing Height', }; function algorithmMode(algorithmId) { @@ -67,6 +69,10 @@ const TaurAlgorithms = (() => { return Boolean(form?.elements?.show_rider?.checked); } + function showStanding(form) { + return Boolean(form?.elements?.show_standing?.checked); + } + function upperBodyHeight(result) { if (result?.UBH != null) { return result.UBH; @@ -85,16 +91,21 @@ const TaurAlgorithms = (() => { next.UBH = next.UBH ?? upperBodyHeight(next); } - if (!showRider(form)) { - return next; + if (showRider(form)) { + const riderHeight = next.RH ?? SizeDiffUnits.parseFormLengthInput(form, 'rider_height'); + if (riderHeight != null && !Number.isNaN(riderHeight)) { + next = { ...next, RH: riderHeight }; + } } - const riderHeight = next.RH ?? SizeDiffUnits.parseFormLengthInput(form, 'rider_height'); - if (riderHeight == null || Number.isNaN(riderHeight)) { - return next; + if (showStanding(form)) { + const standingHeight = next.StH ?? SizeDiffUnits.parseFormLengthInput(form, 'standing_height'); + if (standingHeight != null && !Number.isNaN(standingHeight)) { + next = { ...next, StH: standingHeight }; + } } - return { ...next, RH: riderHeight }; + return next; } function calculateFromForm(form) { @@ -116,7 +127,14 @@ const TaurAlgorithms = (() => { function requiredFieldNames(form) { const impl = get(currentId(form)); - return impl?.requiredFieldNames?.(form) ?? []; + const names = [...(impl?.requiredFieldNames?.(form) ?? [])]; + if (showRider(form)) { + names.push('rider_height'); + } + if (showStanding(form)) { + names.push('standing_height'); + } + return names; } function panelMatchesAlgorithm(panel, algorithmId) { @@ -179,6 +197,10 @@ const TaurAlgorithms = (() => { formatted.RH = `${fmt(result.RH)} (Rider Height)`; } + if (result.StH != null) { + formatted.StH = `${fmt(result.StH)} (Standing Height)`; + } + if (algorithmId === 'volnar') { formatted.AR = `${ratio(result.AR)} (Anthropic Ratio)`; formatted.TW = `${result.TW.toFixed(2)} lbs (Taur Weight)`; diff --git a/app/taur_calculator/taur_canvas.js b/app/taur_calculator/taur_canvas.js index 8939fb0..883613b 100644 --- a/app/taur_calculator/taur_canvas.js +++ b/app/taur_calculator/taur_canvas.js @@ -21,6 +21,8 @@ function initTaurCanvas(config) { const BODY_LAYER_KEYS = new Set(['lBody', 'uBody', 'tail']); const DEFAULT_BODY_COLOR = '#ff0000'; const DEFAULT_RIDER_COLOR = '#ff8800'; + const DEFAULT_STANDING_COLOR = '#007516'; + const STANDING_CHAR_FLOOR_X = 400; const TRIM_ART_COMMAND = 'python3 scripts/trim_art.py'; const SHARE_EXPORT_WIDTH = 1200; const SHARE_EXPORT_HEIGHT = 900; @@ -52,6 +54,20 @@ function initTaurCanvas(config) { return document.getElementById('taur_rider_color')?.value || DEFAULT_RIDER_COLOR; } + function standingColor() { + return document.getElementById('taur_standing_color')?.value || DEFAULT_STANDING_COLOR; + } + + function placementColor(layerKey) { + if (layerKey === 'standingChar') { + return standingColor(); + } + if (layerKey === 'rider') { + return riderColor(); + } + return bodyColor(); + } + const OVERLAY_CORNER_PAD = 56; function overlayFontSizes(displayH) { @@ -162,7 +178,9 @@ function initTaurCanvas(config) { for (const { key, valueLines } of rowLayouts) { const rowColor = key === 'RH' && document.getElementById('show_rider')?.checked ? riderColor() - : color; + : key === 'StH' && document.getElementById('show_standing')?.checked + ? standingColor() + : color; targetCtx.fillStyle = rowColor; targetCtx.font = `bold ${fonts.body}px sans-serif`; @@ -204,11 +222,19 @@ function initTaurCanvas(config) { const centerX = taurData.canvas.width / 2; const groundY = floorY(); const showRider = document.getElementById('show_rider')?.checked; + const showStanding = document.getElementById('show_standing')?.checked; const lBodyDef = layerDef('lBody'); const scenePpi = TaurMeasurements.scenePixelsPerInch(taurData.canvas) ?? 1; + const placements = []; + + if (showStanding && layerDef('standingChar')?.joints?.floor) { + placements.push( + scaled('standingChar', 'floor', STANDING_CHAR_FLOOR_X, groundY, scenePpi) + ); + } const lBodyPlacement = scaled('lBody', 'floor', centerX, groundY, scenePpi); - const placements = [lBodyPlacement]; + placements.push(lBodyPlacement); const [lx, ly] = jointLocalPosition(lBodyDef, 'upper_attach'); const [upperX, upperY] = placementLocalToWorld(lBodyPlacement, lx, ly); @@ -245,7 +271,7 @@ function initTaurCanvas(config) { contentBounds = expandBounds(contentBounds, { left: 72, top: 48, - right: 48, + right: 72, bottom: 8, }); @@ -259,21 +285,23 @@ function initTaurCanvas(config) { targetView.clear(targetCtx); const groundY = floorY(); - const logicalW = taurData.canvas.width; + const vs = targetView.getViewState(); + const groundScreenY = groundY * vs.scale + vs.offsetY; + targetCtx.save(); + targetCtx.setTransform(1, 0, 0, 1, 0, 0); targetCtx.strokeStyle = '#bbb'; - targetCtx.lineWidth = 3 / targetView.getViewState().scale; + targetCtx.lineWidth = 3; targetCtx.beginPath(); - targetCtx.moveTo(0, groundY); - targetCtx.lineTo(logicalW, groundY); + targetCtx.moveTo(0, groundScreenY); + targetCtx.lineTo(vs.displayW, groundScreenY); targetCtx.stroke(); + targetCtx.restore(); for (const placement of placements) { const image = layers[placement.layerKey]; const mask = layers[`${placement.layerKey}_mask`] ?? null; - const color = BODY_LAYER_KEYS.has(placement.layerKey) - ? bodyColor() - : riderColor(); + const color = placementColor(placement.layerKey); drawLayer(targetCtx, image, placement, { color, mask }); } diff --git a/app/taur_calculator/taur_data.json b/app/taur_calculator/taur_data.json index 76263b6..63a04b9 100644 --- a/app/taur_calculator/taur_data.json +++ b/app/taur_calculator/taur_data.json @@ -28,7 +28,7 @@ "trim": { "x": 137, "y": 616, "width": 722, "height": 1242 }, "scale_center": [566, 894], "scale_from": "UBH", - "span_pixels": 1401, + "span_pixels": 822, "joints": { "lower_attach": [738, 1481] } @@ -53,17 +53,27 @@ "seat_attach": [1112, 1537], "mount_attach": [979, 2017] } + }, + "standingChar": { + "path": "Hunner/Size_Refs_Mouse_for_Vixi (male).png", + "trim": { "x": 799, "y": 210, "width": 1205, "height": 3746 }, + "scale_center": [602, 3744], + "scale_from": "StH", + "span_pixels": 3744, + "joints": { + "floor": [1401, 3954] + } } }, "measurements": { "TFH": { - "start": ["uBody", 417, 80], + "start": ["uBody", 417, 659], "end": { "floor": true, "alignX": "start" }, "offset": [-56, 0], "reference": "TFH" }, "THe": { - "start": ["uBody", 417, 80], + "start": ["uBody", 417, 659], "end": ["uBody", 417, 400], "reference": "THe" }, diff --git a/app/taur_calculator/taur_form.js b/app/taur_calculator/taur_form.js index 6b427b5..beca734 100644 --- a/app/taur_calculator/taur_form.js +++ b/app/taur_calculator/taur_form.js @@ -123,7 +123,7 @@ function initTaurForm(config = {}) { const missing = new Set(TaurAlgorithms.missingRequiredFields(form)); form.querySelectorAll( - '#calculated-inputs input, #calculated-inputs select, #manual-inputs input, #rider-height-group input' + '#calculated-inputs input, #calculated-inputs select, #manual-inputs input, #rider-height-group input, #standing-height-group input' ).forEach((input) => { input.classList.remove('taur-field-missing'); }); @@ -215,6 +215,13 @@ function initTaurForm(config = {}) { redrawCanvas(); } + function updateStandingControls() { + const showStanding = document.getElementById('show_standing')?.checked; + document.getElementById('standing-height-group').hidden = !showStanding; + document.getElementById('standing-color-group').hidden = !showStanding; + redrawCanvas(); + } + const SPECIES_LENGTH_FIELDS = [ ['species_height', 'species_height'], ['species_length', 'species_length'], @@ -294,7 +301,7 @@ function initTaurForm(config = {}) { if (event.target.name === 'show_measurements') { return; } - if (event.target.name === 'taur_body_color' || event.target.name === 'taur_rider_color') { + if (event.target.name === 'taur_body_color' || event.target.name === 'taur_rider_color' || event.target.name === 'taur_standing_color') { redrawCanvas(); return; } @@ -325,10 +332,16 @@ function initTaurForm(config = {}) { scheduleApply(); return; } + if (name === 'show_standing') { + updateStandingControls(); + scheduleApply(); + return; + } if (name === 'algorithm') { onAlgorithmChange(); updateAlgorithmControls(); updateRiderControls(); + updateStandingControls(); scheduleApply(); return; } @@ -364,6 +377,7 @@ function initTaurForm(config = {}) { updateAlgorithmControls(); updateMeasurementControls(); updateRiderControls(); + updateStandingControls(); updateSpeciesControls(); applyFormState(); diff --git a/app/taur_calculator/taur_measurements.js b/app/taur_calculator/taur_measurements.js index 2ac5b0e..a996bfe 100644 --- a/app/taur_calculator/taur_measurements.js +++ b/app/taur_calculator/taur_measurements.js @@ -26,7 +26,7 @@ const TaurMeasurements = (() => { return result?.[source]; } - function calibrationPixelsPerInch(canvasConfig = {}) { + function scenePixelsPerInch(canvasConfig = {}) { const calibration = canvasConfig.calibration ?? {}; if (calibration.pixels_per_inch) { return calibration.pixels_per_inch; @@ -37,11 +37,6 @@ const TaurMeasurements = (() => { return null; } - /** Canonical scene ruler — same ppi for every layer. */ - function scenePixelsPerInch(canvasConfig = {}) { - return calibrationPixelsPerInch(canvasConfig); - } - /** Layer scale: inches * scene pixels-per-inch / art span in source pixels. */ function scaleForLayer(layerDef, result, pixelsPerInch) { if (!layerDef?.scale_from || pixelsPerInch == null || !layerDef.span_pixels) { @@ -120,12 +115,6 @@ const TaurMeasurements = (() => { return tfhDistance / tfhInches; } - /** @deprecated Use scenePixelsPerInch for placement; this is for diagnostics. */ - function deriveScenePixelsPerInch(options) { - return measuredTfhPixelsPerInch(options) - ?? calibrationPixelsPerInch(options.canvasConfig ?? {}); - } - /** * Scale around scale_center (or scale_pivot), then pin anchorJoint to world coords. */ @@ -219,9 +208,7 @@ const TaurMeasurements = (() => { scaleForLayer, placeScaledLayer, scenePixelsPerInch, - calibrationPixelsPerInch, measuredTfhPixelsPerInch, - deriveScenePixelsPerInch, measureDefinitionWorldDistance, }; })(); diff --git a/app/templates/taur.html b/app/templates/taur.html index 5c72394..36b3902 100644 --- a/app/templates/taur.html +++ b/app/templates/taur.html @@ -69,7 +69,7 @@

CalculaTAUR

+ value="9'0"" placeholder="e.g., 9'0"" />
@@ -173,6 +173,24 @@

CalculaTAUR

+
+ +
+ + + + +