Precision Architect is a free browser tool for builders who want precise dimensions on their canvas objects.
You enter your target dimension in centimeters and it sweeps through every possible scale value (0.01 to 1000.00) to find the exact scale + slider combo that gets you closest. Results for both Width/Height and Depth show side by side, ranked by accuracy — just click any value to copy it.
The input field supports math expressions, so you can type things like sqrt(50^2 + 50^2) or 1500 - 327.4 instead of calculating by hand.
The Coordinate Helper lets you enter the coordinates of two objects and instantly get the distance, rotation angle, and division points between them — midpoints, thirds, quarters, whatever you need. You can send any result straight into the calculator with one click.
Link: https://localityhd.github.io/precision-architect/
No install, works in any browser. If the link doesn’t work, copy the code below into a text file and save it as PrecisionArchitect.html — then just open it in your browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Precision Architect</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
background-color: #111118;
color: #c8cfe6;
min-height: 100vh;
padding: 1.5rem;
}
.app {
max-width: 1100px;
margin: 0 auto;
}
/* ── Header ── */
header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.75rem;
}
header .icon {
width: 40px; height: 40px;
background: linear-gradient(135deg, #c084fc, #818cf8);
border-radius: 10px;
display: grid; place-items: center;
font-size: 1.25rem;
flex-shrink: 0;
}
header h1 {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, #e0b0ff, #93a4f8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
header p {
font-size: 0.8rem;
color: #6b7394;
margin-top: 0.1rem;
}
/* ── Card ── */
.card {
background: #181a25;
border: 1px solid #252838;
border-radius: 14px;
padding: 1.5rem;
margin-bottom: 1rem;
}
/* ── Input Row ── */
.input-row {
display: flex;
gap: 0.625rem;
flex-wrap: wrap;
align-items: flex-end;
}
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
flex: 1;
min-width: 180px;
}
.field label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #6b7394;
}
.field .hint {
font-size: 0.65rem;
color: #4b5578;
margin-top: 0.1rem;
}
input, select {
padding: 0.6rem 0.75rem;
border-radius: 8px;
border: 1px solid #2d3148;
background: #12131d;
color: #dde2f0;
font-size: 0.95rem;
transition: border-color 0.15s;
width: 100%;
}
input:focus, select:focus {
outline: none;
border-color: #818cf8;
box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15);
}
input.error {
border-color: #f87171;
box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.15);
}
.eval-preview {
font-size: 0.72rem;
color: #818cf8;
min-height: 1.1em;
margin-top: 0.2rem;
font-variant-numeric: tabular-nums;
}
.eval-preview.err { color: #f87171; }
/* ── Buttons ── */
.btn {
padding: 0.6rem 1.25rem;
border-radius: 8px;
border: none;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.btn-primary {
background: linear-gradient(135deg, #a78bfa, #818cf8);
color: #0f0f1a;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(129, 140, 248, 0.35);
}
.btn-primary:active { transform: translateY(0); }
.btn-sm {
padding: 0.35rem 0.7rem;
font-size: 0.75rem;
border-radius: 6px;
}
.btn-ghost {
background: rgba(129, 140, 248, 0.1);
color: #a5b4fc;
border: 1px solid rgba(129, 140, 248, 0.2);
}
.btn-ghost:hover {
background: rgba(129, 140, 248, 0.2);
}
.btn-send {
background: rgba(110, 231, 160, 0.1);
color: #6ee7a0;
border: 1px solid rgba(110, 231, 160, 0.2);
}
.btn-send:hover {
background: rgba(110, 231, 160, 0.2);
}
/* ── Dual columns ── */
.dual {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
/* ── Section label ── */
.section-label {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.6rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
.section-label.wh { color: #818cf8; }
.section-label.depth { color: #c084fc; }
.section-dot {
width: 8px; height: 8px;
border-radius: 50%;
display: inline-block;
}
.section-dot.wh { background: #818cf8; }
.section-dot.depth { background: #c084fc; }
/* ── Coord helper ── */
.coord-section {
margin-bottom: 1.5rem;
}
.coord-section .card-title {
font-size: 0.95rem;
font-weight: 700;
color: #e8ecf8;
margin-bottom: 0.2rem;
}
.coord-section .card-subtitle {
font-size: 0.72rem;
color: #6b7394;
margin-bottom: 1rem;
}
.coord-row {
display: flex;
gap: 0.5rem;
align-items: center;
margin-bottom: 0.6rem;
}
.coord-row .point-label {
font-size: 0.7rem;
font-weight: 700;
color: #6b7394;
width: 20px;
flex-shrink: 0;
text-align: center;
}
.coord-row input {
flex: 1;
min-width: 0;
font-size: 0.88rem;
padding: 0.5rem 0.6rem;
}
.coord-row .axis-label {
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
color: #4b5578;
width: 14px;
text-align: center;
flex-shrink: 0;
}
.coord-controls {
display: flex;
gap: 0.5rem;
align-items: center;
margin-top: 0.75rem;
flex-wrap: wrap;
}
.coord-controls label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #6b7394;
}
.coord-controls input {
width: 55px;
font-size: 0.88rem;
padding: 0.5rem 0.6rem;
text-align: center;
}
/* ── Coord results ── */
.coord-results {
display: none;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #252838;
}
.coord-results.show { display: block; }
.coord-result-group {
margin-bottom: 0.75rem;
}
.coord-result-group .group-label {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #6b7394;
margin-bottom: 0.35rem;
}
.coord-result-items {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.coord-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: #12131d;
border: 1px solid #2d3148;
border-radius: 8px;
padding: 0.4rem 0.6rem;
font-size: 0.82rem;
font-variant-numeric: tabular-nums;
transition: all 0.15s;
}
.coord-chip .chip-label {
font-size: 0.62rem;
font-weight: 600;
text-transform: uppercase;
color: #4b5578;
margin-right: 0.15rem;
}
.coord-chip .chip-value {
font-weight: 600;
color: #e8ecf8;
cursor: pointer;
}
.coord-chip .chip-value:hover { color: #a5b4fc; }
.coord-chip .chip-value.flash { color: #6ee7a0; }
.coord-chip .chip-send {
cursor: pointer;
font-size: 0.65rem;
color: #6ee7a0;
opacity: 0.6;
transition: opacity 0.15s;
padding: 0.1rem 0.2rem;
border-radius: 3px;
}
.coord-chip .chip-send:hover { opacity: 1; background: rgba(110, 231, 160, 0.1); }
.divider-line {
height: 1px;
background: #252838;
margin: 1.5rem 0;
}
/* ── Hero result ── */
.hero {
background: linear-gradient(135deg, rgba(129, 140, 248, 0.08), rgba(167, 139, 250, 0.06));
border: 1px solid rgba(129, 140, 248, 0.2);
border-radius: 12px;
padding: 1rem 1.25rem;
animation: fadeIn 0.3s ease;
}
.hero.depth-hero {
background: linear-gradient(135deg, rgba(192, 132, 252, 0.08), rgba(167, 139, 250, 0.06));
border-color: rgba(192, 132, 252, 0.2);
}
.hero-label {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #818cf8;
margin-bottom: 0.5rem;
}
.hero.depth-hero .hero-label { color: #c084fc; }
.hero-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.hero-stat .value {
font-size: 1.15rem;
font-weight: 700;
color: #e8ecf8;
font-variant-numeric: tabular-nums;
}
.hero-stat .label {
font-size: 0.68rem;
color: #6b7394;
margin-top: 0.1rem;
}
.hero-stat .value.perfect { color: #6ee7a0; }
/* ── Results table ── */
.table-card {
background: #181a25;
border: 1px solid #252838;
border-radius: 14px;
overflow: hidden;
}
.table-card .table-header {
padding: 1rem 1rem 0;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-variant-numeric: tabular-nums;
font-size: 0.82rem;
}
thead th {
position: sticky;
top: 0;
background: #1c1e2c;
padding: 0.55rem 0.7rem;
text-align: left;
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #6b7394;
border-bottom: 1px solid #252838;
}
tbody td {
padding: 0.5rem 0.7rem;
border-bottom: 1px solid #1e2030;
}
tbody tr { transition: background 0.1s; }
tbody tr:hover { background: rgba(129, 140, 248, 0.04); }
tbody tr:last-child td { border-bottom: none; }
/* ── Copyable cell ── */
.copyable {
cursor: pointer;
position: relative;
border-radius: 4px;
transition: background 0.15s, color 0.15s;
padding: 0.15rem 0.3rem;
margin: -0.15rem -0.3rem;
display: inline-block;
}
.copyable:hover {
background: rgba(129, 140, 248, 0.1);
color: #a5b4fc;
}
.copyable.flash {
background: rgba(110, 231, 160, 0.2);
color: #6ee7a0;
}
/* ── Error bar ── */
.error-cell {
display: flex;
align-items: center;
gap: 0.4rem;
}
.error-bar-bg {
width: 40px;
height: 5px;
background: #1e2030;
border-radius: 3px;
overflow: hidden;
flex-shrink: 0;
}
.error-bar-fill {
height: 100%;
border-radius: 3px;
}
/* ── Row rank badge ── */
.rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px; height: 20px;
border-radius: 5px;
font-size: 0.65rem;
font-weight: 700;
background: #1e2030;
color: #6b7394;
}
.rank.gold { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
.rank.silver { background: rgba(148, 163, 184, 0.12); color: #94a3b8; }
.rank.bronze { background: rgba(217, 119, 6, 0.12); color: #d97706; }
/* ── Perfect match ── */
tr.perfect td { color: #6ee7a0; }
tr.perfect .rank { background: rgba(110, 231, 160, 0.15); color: #6ee7a0; }
/* ── Empty state ── */
.empty-state {
text-align: center;
padding: 2rem 1rem;
color: #4b5578;
font-size: 0.85rem;
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: #2d3148;
color: #dde2f0;
padding: 0.55rem 1.1rem;
border-radius: 8px;
font-size: 0.82rem;
font-weight: 500;
opacity: 0;
pointer-events: none;
transition: all 0.25s ease;
z-index: 100;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ── Footer stats ── */
.footer-stats {
display: flex;
justify-content: space-between;
font-size: 0.68rem;
color: #4b5578;
padding: 0.5rem 0.7rem;
}
/* ── Loading spinner ── */
.loading {
display: none;
text-align: center;
padding: 1.5rem;
color: #6b7394;
font-size: 0.85rem;
}
.loading.active { display: block; }
.spinner {
display: inline-block;
width: 22px; height: 22px;
border: 3px solid #252838;
border-top-color: #818cf8;
border-radius: 50%;
animation: spin 0.7s linear infinite;
margin-bottom: 0.4rem;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Responsive ── */
@media (max-width: 700px) {
body { padding: 1rem 0.75rem; }
.dual { grid-template-columns: 1fr; }
.card { padding: 1rem; }
.hero-stat .value { font-size: 1rem; }
}
</style>
</head>
<body>
<div class="app">
<header>
<div class="icon">◍</div>
<div>
<h1>Precision Architect</h1>
<p>Find the best scale & slider to hit your target dimension</p>
</div>
</header>
<!-- ══════════ Coordinate Helper ══════════ -->
<div class="card coord-section">
<div class="card-title">Coordinate Helper</div>
<div class="card-subtitle">Paste two points to get distance, division points, and rotation (Yaw + Pitch). Leave axes blank if unused.</div>
<div class="coord-row">
<div class="point-label">A</div>
<span class="axis-label">X</span>
<input type="text" id="ax" placeholder="X" autocomplete="off">
<span class="axis-label">Y</span>
<input type="text" id="ay" placeholder="Y" autocomplete="off">
<span class="axis-label">Z</span>
<input type="text" id="az" placeholder="Z" autocomplete="off">
</div>
<div class="coord-row">
<div class="point-label">B</div>
<span class="axis-label">X</span>
<input type="text" id="bx" placeholder="X" autocomplete="off">
<span class="axis-label">Y</span>
<input type="text" id="by" placeholder="Y" autocomplete="off">
<span class="axis-label">Z</span>
<input type="text" id="bz" placeholder="Z" autocomplete="off">
</div>
<div class="coord-controls">
<label for="divisions">Divide into</label>
<input type="number" id="divisions" value="2" min="2" max="100">
<label style="margin-left:0.75rem;"><input type="checkbox" id="cubeMod" style="width:auto;margin-right:0.3rem;vertical-align:middle"> Cube reduce (mod 90°)</label>
<button class="btn btn-primary btn-sm" onclick="calcCoords()">Compute</button>
</div>
<div class="coord-results" id="coordResults">
<div class="coord-result-group">
<div class="group-label">Distance</div>
<div class="coord-result-items" id="distResults"></div>
</div>
<div class="coord-result-group">
<div class="group-label" id="divisionLabel">Division points</div>
<div class="coord-result-items" id="divResults"></div>
</div>
<div class="coord-result-group" id="rotationGroup">
<div class="group-label" id="rotationLabel">Rotation (degrees)</div>
<div class="coord-result-items" id="rotResults"></div>
</div>
</div>
</div>
<div class="divider-line"></div>
<!-- ══════════ Scale Calculator ══════════ -->
<div class="card">
<div class="input-row">
<div class="field">
<label for="targetCm">Target (cm)</label>
<input type="text" id="targetCm" placeholder="e.g. 2.94 or sqrt(50^2 + 50^2)" value="2.94" autocomplete="off" spellcheck="false">
<div class="eval-preview" id="evalPreview"></div>
<div class="hint">Supports math: sqrt(x), pow(x,y), x^y, +, -, *, /, parentheses</div>
</div>
<div style="flex: 0 0 auto; align-self: flex-start; padding-top: 1.45rem;">
<button class="btn btn-primary" onclick="calculatePrecision()">Calculate</button>
</div>
</div>
</div>
<!-- Loading -->
<div class="loading" id="loading">
<div class="spinner"></div>
<div>Sweeping 100,000 scale values...</div>
</div>
<!-- Results: dual columns -->
<div id="resultsArea" style="display:none;">
<!-- Hero cards -->
<div class="dual" style="margin-bottom: 1rem;">
<div class="hero" id="heroWH">
<div class="hero-label">Best match — Width / Height</div>
<div class="hero-grid">
<div class="hero-stat">
<div class="value copyable" id="heroScaleWH" onclick="copyEl(this)">-</div>
<div class="label">Scale</div>
</div>
<div class="hero-stat">
<div class="value copyable" id="heroSliderWH" onclick="copyEl(this)">-</div>
<div class="label">Slider</div>
</div>
<div class="hero-stat">
<div class="value copyable" id="heroCalcWH" onclick="copyEl(this)">-</div>
<div class="label">Result (cm)</div>
</div>
<div class="hero-stat">
<div class="value" id="heroErrorWH">-</div>
<div class="label">Error</div>
</div>
</div>
</div>
<div class="hero depth-hero" id="heroDepth">
<div class="hero-label">Best match — Depth</div>
<div class="hero-grid">
<div class="hero-stat">
<div class="value copyable" id="heroScaleD" onclick="copyEl(this)">-</div>
<div class="label">Scale</div>
</div>
<div class="hero-stat">
<div class="value copyable" id="heroSliderD" onclick="copyEl(this)">-</div>
<div class="label">Slider</div>
</div>
<div class="hero-stat">
<div class="value copyable" id="heroCalcD" onclick="copyEl(this)">-</div>
<div class="label">Result (cm)</div>
</div>
<div class="hero-stat">
<div class="value" id="heroErrorD">-</div>
<div class="label">Error</div>
</div>
</div>
</div>
</div>
<!-- Tables -->
<div class="dual">
<div class="table-card">
<div class="table-header">
<div class="section-label wh"><span class="section-dot wh"></span> Width / Height (X/Y)</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:30px">#</th>
<th>Scale</th>
<th>Slider</th>
<th>Result</th>
<th>Error</th>
</tr>
</thead>
<tbody id="bodyWH"></tbody>
</table>
</div>
<div class="footer-stats" id="footerWH"></div>
</div>
<div class="table-card">
<div class="table-header">
<div class="section-label depth"><span class="section-dot depth"></span> Depth (Z)</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:30px">#</th>
<th>Scale</th>
<th>Slider</th>
<th>Result</th>
<th>Error</th>
</tr>
</thead>
<tbody id="bodyDepth"></tbody>
</table>
</div>
<div class="footer-stats" id="footerDepth"></div>
</div>
</div>
</div>
</div>
<div class="toast" id="toast">Copied!</div>
<script>
// ── Safe math expression evaluator ──
function evalMath(expr) {
if (typeof expr !== 'string') return NaN;
let s = expr.trim();
if (!s) return NaN;
// Convert ^ to pow()
function convertCaret(str) {
let prev;
do {
prev = str;
str = str.replace(/(\([^()]*\)|[\d.]+)\s*\^\s*(\([^()]*\)|[\d.]+)/, 'pow($1,$2)');
} while (str !== prev);
return str;
}
s = convertCaret(s);
s = s.replace(/\bsqrt\b/g, 'Math.sqrt');
s = s.replace(/\bpow\b/g, 'Math.pow');
s = s.replace(/\bcbrt\b/g, 'Math.cbrt');
s = s.replace(/\babs\b/g, 'Math.abs');
s = s.replace(/\bceil\b/g, 'Math.ceil');
s = s.replace(/\bfloor\b/g, 'Math.floor');
s = s.replace(/\bround\b/g, 'Math.round');
s = s.replace(/\blog10\b/g, 'Math.log10');
s = s.replace(/\blog2\b/g, 'Math.log2');
s = s.replace(/\blog\b/g, 'Math.log');
s = s.replace(/\bsin\b/g, 'Math.sin');
s = s.replace(/\bcos\b/g, 'Math.cos');
s = s.replace(/\btan\b/g, 'Math.tan');
s = s.replace(/\bpi\b/gi, 'Math.PI');
s = s.replace(/\bmin\b/g, 'Math.min');
s = s.replace(/\bmax\b/g, 'Math.max');
const sanitized = s.replace(/Math\.\w+/g, '').replace(/[0-9+\-*/().,%^\s]/g, '');
if (sanitized.length > 0) return NaN;
try {
const result = Function('"use strict"; return (' + s + ')')();
if (typeof result !== 'number' || !isFinite(result)) return NaN;
return parseFloat(result.toPrecision(12));
} catch {
return NaN;
}
}
// ── Clean float for display ──
function cleanNum(n) {
return parseFloat(n.toPrecision(12));
}
// ── Live preview ──
const targetInput = document.getElementById('targetCm');
const evalPreview = document.getElementById('evalPreview');
function updatePreview() {
const raw = targetInput.value.trim();
if (!raw || /^-?\d*\.?\d+$/.test(raw)) {
evalPreview.textContent = '';
evalPreview.className = 'eval-preview';
targetInput.classList.remove('error');
return;
}
const val = evalMath(raw);
if (isNaN(val)) {
evalPreview.textContent = 'Invalid expression';
evalPreview.className = 'eval-preview err';
targetInput.classList.add('error');
} else {
evalPreview.textContent = '= ' + val;
evalPreview.className = 'eval-preview';
targetInput.classList.remove('error');
}
}
targetInput.addEventListener('input', updatePreview);
// ── Enter key ──
targetInput.addEventListener('keydown', e => {
if (e.key === 'Enter') calculatePrecision();
});
// Enter in coord fields
document.querySelectorAll('.coord-row input, #divisions').forEach(el => {
el.addEventListener('keydown', e => {
if (e.key === 'Enter') calcCoords();
});
});
// Cube-reduce checkbox: re-render rotations live
document.getElementById('cubeMod').addEventListener('change', () => {
if (document.getElementById('coordResults').classList.contains('show')) calcCoords();
});
// ── Copy helpers ──
function copyText(text) {
const clean = text.trim();
navigator.clipboard.writeText(clean).then(() => {
showToast('Copied: ' + clean);
}).catch(() => {
const ta = document.createElement('textarea');
ta.value = clean;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showToast('Copied: ' + clean);
});
}
function copyEl(el) {
const text = el.textContent.trim();
if (text === '-' || text === 'Perfect') return;
copyText(text);
el.classList.add('flash');
setTimeout(() => el.classList.remove('flash'), 400);
}
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
clearTimeout(toast._t);
toast._t = setTimeout(() => toast.classList.remove('show'), 1800);
}
// ── Send value to calculator ──
function sendToCalc(val) {
targetInput.value = val;
updatePreview();
calculatePrecision();
// Scroll to calculator
targetInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
showToast('Sent to calculator: ' + val);
}
// ══════════ Rotation math (Unreal/TU convention) ══════════
// Conventions per user:
// - Cube's local +X edge points along world +X at zero rotation
// - Z rotation (Yaw) is clockwise positive looking down +Z
// - Editor takes degrees, normalised to (-180, 180]
// - Rotation matrix R = R_yaw(γ) * R_pitch(β) * R_roll(α)
// Forward (local +X mapped to world) = (cos γ cos β, sin γ cos β, sin β)
// Inversion:
// pitch β = arcsin(R[2,0])
// yaw γ = atan2(R[1,0], R[0,0])
// roll α = atan2(-R[2,1], R[2,2])
function normAngle(a) {
a = ((a + 180) % 360 + 360) % 360 - 180;
return a === -180 ? 180 : a;
}
function cubeReduce(a) {
// Map any angle to its smallest-magnitude equivalent under 4-fold symmetry (mod 90°), in (-45, 45]
let r = ((a + 45) % 90 + 90) % 90 - 45;
return r === -45 ? 45 : r;
}
function computeRotation(A, B, C) {
const dx = B.x - A.x, dy = B.y - A.y, dz = B.z - A.z;
const L = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (L < 1e-9) return null;
const ax = [dx/L, dy/L, dz/L]; // local +X in world
const sb = Math.max(-1, Math.min(1, ax[2]));
const pitch = Math.asin(sb);
const cb = Math.cos(pitch);
let yaw, roll = 0;
let rollKnown = false, rollNote = null;
const gimbal = Math.abs(cb) < 1e-6;
if (gimbal) {
// AB is straight up/down — yaw is undetermined, fold into roll if C provided
yaw = 0;
} else {
yaw = Math.atan2(ax[1], ax[0]);
}
if (C) {
const acRaw = [C.x - A.x, C.y - A.y, C.z - A.z];
const dot = acRaw[0]*ax[0] + acRaw[1]*ax[1] + acRaw[2]*ax[2];
const acP = [acRaw[0] - dot*ax[0], acRaw[1] - dot*ax[1], acRaw[2] - dot*ax[2]];
const lp = Math.sqrt(acP[0]**2 + acP[1]**2 + acP[2]**2);
if (lp < 1e-9) {
rollNote = 'C is colinear with AB — roll undetermined';
} else {
const ay = [acP[0]/lp, acP[1]/lp, acP[2]/lp];
// az = ax × ay (algebraic; in Unreal LH this gives the local +Z = up out of plane ABC)
const az = [
ax[1]*ay[2] - ax[2]*ay[1],
ax[2]*ay[0] - ax[0]*ay[2],
ax[0]*ay[1] - ax[1]*ay[0]
];
// R columns are [ax, ay, az]. R[i][j] = (i-th component of j-th column).
// R[2][1] = ay.z, R[2][2] = az.z
if (gimbal) {
// Re-extract: at pitch=±90, yaw collapses with roll. Set yaw=0 and read roll from R[0][1], R[1][1].
// R[0][1] = ay.x, R[1][1] = ay.y when γ=0
roll = Math.atan2(ay[0] * Math.sign(sb || 1), ay[1]);
} else {
roll = Math.atan2(-ay[2], az[2]);
}
rollKnown = true;
}
}
return {
pitch: normAngle(pitch * 180/Math.PI),
yaw: normAngle(yaw * 180/Math.PI),
roll: normAngle(roll * 180/Math.PI),
rollKnown,
rollNote,
gimbal,
length: L
};
}
// ══════════ Coordinate Helper ══════════
function calcCoords() {
const ax = parseFloat(document.getElementById('ax').value);
const ay = parseFloat(document.getElementById('ay').value);
const az = parseFloat(document.getElementById('az').value);
const bx = parseFloat(document.getElementById('bx').value);
const by = parseFloat(document.getElementById('by').value);
const bz = parseFloat(document.getElementById('bz').value);
const divs = parseInt(document.getElementById('divisions').value) || 2;
const cubeOn = document.getElementById('cubeMod').checked;
const resultsDiv = document.getElementById('coordResults');
const distDiv = document.getElementById('distResults');
const divDiv = document.getElementById('divResults');
const divLabel = document.getElementById('divisionLabel');
const rotDiv = document.getElementById('rotResults');
const rotGroup = document.getElementById('rotationGroup');
const rotLabel = document.getElementById('rotationLabel');
// Figure out which axes are used
const hasX = !isNaN(ax) && !isNaN(bx);
const hasY = !isNaN(ay) && !isNaN(by);
const hasZ = !isNaN(az) && !isNaN(bz);
if (!hasX && !hasY && !hasZ) {
resultsDiv.classList.remove('show');
return;
}
distDiv.innerHTML = '';
divDiv.innerHTML = '';
rotDiv.innerHTML = '';
// Per-axis distances
let sumSq = 0;
const axes = [];
if (hasX) { const d = cleanNum(Math.abs(bx - ax)); axes.push({ name: 'X', a: ax, b: bx, dist: d }); sumSq += (bx - ax) ** 2; }
if (hasY) { const d = cleanNum(Math.abs(by - ay)); axes.push({ name: 'Y', a: ay, b: by, dist: d }); sumSq += (by - ay) ** 2; }
if (hasZ) { const d = cleanNum(Math.abs(bz - az)); axes.push({ name: 'Z', a: az, b: bz, dist: d }); sumSq += (bz - az) ** 2; }
// Show per-axis distances
axes.forEach(a => {
distDiv.appendChild(makeChip(a.name + ' dist', a.dist));
});
// Total distance if more than one axis
if (axes.length > 1) {
const total = cleanNum(Math.sqrt(sumSq));
distDiv.appendChild(makeChip('Total', total));
}
// Division points
const divNames = {
2: 'Midpoint',
3: 'Thirds',
4: 'Quarters',
5: 'Fifths',
6: 'Sixths',
7: 'Sevenths',
8: 'Eighths',
9: 'Ninths',
10: 'Tenths'
};
divLabel.textContent = (divNames[divs] || divs + '-way division') + ' (' + (divs - 1) + ' point' + (divs - 1 === 1 ? '' : 's') + ')';
for (let i = 1; i < divs; i++) {
const frac = i / divs;
const fracLabel = i + '/' + divs;
axes.forEach(a => {
const val = cleanNum(a.a + (a.b - a.a) * frac);
divDiv.appendChild(makeChip(a.name + ' @' + fracLabel, val));
});
// If multi-axis, also show as coordinate tuple
if (axes.length > 1) {
const parts = axes.map(a => cleanNum(a.a + (a.b - a.a) * frac));
const tupleStr = parts.join(', ');
divDiv.appendChild(makeChip('(' + axes.map(a => a.name).join('') + ') @' + fracLabel, tupleStr, true));
}
}
// ── Rotation ──
const A = { x: isNaN(ax) ? 0 : ax, y: isNaN(ay) ? 0 : ay, z: isNaN(az) ? 0 : az };
const B = { x: isNaN(bx) ? 0 : bx, y: isNaN(by) ? 0 : by, z: isNaN(bz) ? 0 : bz };
const rot = computeRotation(A, B, null);
if (rot) {
rotGroup.style.display = '';
const fmt = v => cleanNum(cubeOn ? cubeReduce(v) : v);
rotLabel.textContent = 'Rotation (degrees)' + (cubeOn ? ' — cube reduced (mod 90°)' : '');
rotDiv.appendChild(makeChip('Y (Pitch)', fmt(rot.pitch), true));
rotDiv.appendChild(makeChip('Z (Yaw)', fmt(rot.yaw), true));
const tup = ['0', fmt(rot.pitch), fmt(rot.yaw)].join(', ');
rotDiv.appendChild(makeChip('(X,Y,Z)', tup, true));
if (rot.gimbal) {
rotDiv.appendChild(makeChip('!', 'gimbal lock (AB ∥ Z) — Yaw=0 fixed', true));
}
} else {
rotGroup.style.display = 'none';
}
resultsDiv.classList.add('show');
}
function makeChip(label, value, isTuple) {
const chip = document.createElement('div');
chip.className = 'coord-chip';
const valStr = typeof value === 'number' ? String(value) : value;
chip.innerHTML = `
<span class="chip-label">${label}</span>
<span class="chip-value" onclick="copyEl(this)">${valStr}</span>
${!isTuple ? `<span class="chip-send" onclick="sendToCalc('${valStr}')" title="Send to calculator">▶ calc</span>` : ''}
`;
return chip;
}
// ══════════ Scale Calculator ══════════
const SCALE_MIN = 1;
const SCALE_MAX = 100000;
const SD = 7;
function compute(scaleInt, target, axis) {
const scale = scaleInt / 100;
let slider;
if (axis === 'wh') {
slider = ((target / scale) - 12.5) / 37.5;
} else {
slider = ((target / scale) - 5) / 45.0;
}
if (slider < 0.0 || slider > 1.0) return null;
const sr = parseFloat(slider.toFixed(SD));
if (sr < 0.0 || sr > 1.0) return null;
let calc;
if (axis === 'wh') {
calc = scale * (37.5 * sr + 12.5);
} else {
calc = scale * (45.0 * sr + 5);
}
return { scale, slider: sr, calculated: calc, error: Math.abs(calc - target) };
}
function sweepAxis(target, axis) {
let results = [];
let count = 0;
for (let s = SCALE_MIN; s <= SCALE_MAX; s++) {
count++;
const res = compute(s, target, axis);
if (res) results.push(res);
}
results.sort((a, b) => a.error - b.error);
return { results, count };
}
function renderTable(results, tbody, footer, count, elapsed) {
tbody.innerHTML = '';
const topN = 25;
const displayed = results.slice(0, topN);
const maxErr = displayed.length > 0 ? Math.max(...displayed.map(r => r.error), 1e-10) : 1;
if (displayed.length === 0) {
tbody.innerHTML = '<tr><td colspan="5"><div class="empty-state">No valid combinations found.</div></td></tr>';
footer.innerHTML = `<span>${count.toLocaleString()} checked</span><span>0 valid</span>`;
return null;
}
displayed.forEach((res, idx) => {
const tr = document.createElement('tr');
const perf = res.error < 5e-8;
if (perf) tr.className = 'perfect';
let rc = '';
if (idx === 0) rc = ' gold';
else if (idx === 1) rc = ' silver';
else if (idx === 2) rc = ' bronze';
const ratio = maxErr > 1e-10 ? res.error / maxErr : 0;
let bc;
if (perf) bc = '#6ee7a0';
else if (ratio < 0.25) bc = '#4ade80';
else if (ratio < 0.5) bc = '#facc15';
else if (ratio < 0.75) bc = '#fb923c';
else bc = '#f87171';
const bw = perf ? 0 : Math.max(2, ratio * 100);
tr.innerHTML = `
<td><span class="rank${rc}">${idx + 1}</span></td>
<td><span class="copyable" onclick="copyEl(this)">${res.scale.toFixed(2)}</span></td>
<td><span class="copyable" onclick="copyEl(this)" style="font-weight:600">${res.slider.toFixed(SD)}</span></td>
<td><span class="copyable" onclick="copyEl(this)">${res.calculated.toFixed(SD)}</span></td>
<td>
<div class="error-cell">
<div class="error-bar-bg"><div class="error-bar-fill" style="width:${bw}%;background:${bc}"></div></div>
<span>${perf ? 'Perfect' : res.error.toFixed(SD)}</span>
</div>
</td>
`;
tbody.appendChild(tr);
});
footer.innerHTML = `<span>${count.toLocaleString()} checked · ${elapsed} ms</span><span>${results.length.toLocaleString()} valid · top ${displayed.length}</span>`;
return displayed[0];
}
function fillHero(best, prefix) {
const perf = best.error < 5e-8;
const scaleEl = document.getElementById('heroScale' + prefix);
const sliderEl = document.getElementById('heroSlider' + prefix);
const calcEl = document.getElementById('heroCalc' + prefix);
const errorEl = document.getElementById('heroError' + prefix);
scaleEl.textContent = best.scale.toFixed(2);
sliderEl.textContent = best.slider.toFixed(SD);
sliderEl.className = 'value copyable' + (perf ? ' perfect' : '');
calcEl.textContent = best.calculated.toFixed(SD);
errorEl.textContent = perf ? 'Perfect' : best.error.toFixed(SD);
errorEl.className = 'value' + (perf ? ' perfect' : '');
}
function calculatePrecision() {
const rawInput = targetInput.value.trim();
const isPlain = /^-?\d*\.?\d+$/.test(rawInput);
let target = isPlain ? parseFloat(rawInput) : evalMath(rawInput);
const resultsArea = document.getElementById('resultsArea');
const loading = document.getElementById('loading');
resultsArea.style.display = 'none';
if (isNaN(target) || target <= 0) {
document.getElementById('bodyWH').innerHTML = '<tr><td colspan="5"><div class="empty-state">Enter a positive target dimension.</div></td></tr>';
document.getElementById('bodyDepth').innerHTML = '<tr><td colspan="5"><div class="empty-state">Enter a positive target dimension.</div></td></tr>';
resultsArea.style.display = 'block';
return;
}
loading.classList.add('active');
requestAnimationFrame(() => {
setTimeout(() => {
const t0wh = performance.now();
const wh = sweepAxis(target, 'wh');
const elWH = (performance.now() - t0wh).toFixed(1);
const t0d = performance.now();
const d = sweepAxis(target, 'depth');
const elD = (performance.now() - t0d).toFixed(1);
loading.classList.remove('active');
resultsArea.style.display = 'block';
const bestWH = renderTable(wh.results, document.getElementById('bodyWH'), document.getElementById('footerWH'), wh.count, elWH);
const bestD = renderTable(d.results, document.getElementById('bodyDepth'), document.getElementById('footerDepth'), d.count, elD);
if (bestWH) fillHero(bestWH, 'WH');
if (bestD) fillHero(bestD, 'D');
}, 10);
});
}
// Run on load
calculatePrecision();
</script>
</body>
</html>
hopefully someone finds this useful