Skip to content

Commit 2756682

Browse files
authored
Merge pull request #7 from CodeSignal/cursor/triangle-three-labeled-sides
Add triangle shape with all three sides labeled.
2 parents c8711b8 + 050a29c commit 2756682

2 files changed

Lines changed: 126 additions & 1 deletion

File tree

client/geometry-explorer.js

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,51 @@ function getLogicalCanvasSize(canvas) {
174174
};
175175
}
176176

177+
function isValidTriangleSides(a, b, c) {
178+
return a + b > c && a + c > b && b + c > a;
179+
}
180+
181+
function triangleAreaFromSides(a, b, c) {
182+
if (!isValidTriangleSides(a, b, c)) return 0;
183+
const s = (a + b + c) / 2;
184+
const radicand = s * (s - a) * (s - b) * (s - c);
185+
return radicand > 0 ? Math.sqrt(radicand) : 0;
186+
}
187+
188+
function triangleHeightOnBase(sideA, sideB, base) {
189+
const x = (sideA * sideA + base * base - sideB * sideB) / (2 * base);
190+
return Math.sqrt(Math.max(0, sideA * sideA - x * x));
191+
}
192+
193+
function repairTriangleSides(values, rangesByKey) {
194+
let sideA = values.sideA;
195+
let sideB = values.sideB;
196+
let sideC = values.sideC;
197+
const clampSide = (key, val, maxAllowed) => {
198+
const ranges = rangesByKey[key];
199+
const step = ranges?.step ?? 1;
200+
const capped = Math.min(ranges.max, Math.max(ranges.min, Math.min(val, maxAllowed)));
201+
const snappedValue = snapToStep(capped, ranges.min, step);
202+
const upperSnap = snapToStep(Math.min(maxAllowed, ranges.max), ranges.min, step);
203+
return Math.min(snappedValue, upperSnap);
204+
};
205+
206+
for (let attempt = 0; attempt < 24 && !isValidTriangleSides(sideA, sideB, sideC); attempt += 1) {
207+
if (sideC >= sideA && sideC >= sideB) {
208+
const step = rangesByKey.sideC?.step ?? 1;
209+
sideC = clampSide('sideC', sideC, sideA + sideB - step);
210+
} else if (sideA >= sideB) {
211+
const step = rangesByKey.sideA?.step ?? 1;
212+
sideA = clampSide('sideA', sideA, sideB + sideC - step);
213+
} else {
214+
const step = rangesByKey.sideB?.step ?? 1;
215+
sideB = clampSide('sideB', sideB, sideA + sideC - step);
216+
}
217+
}
218+
219+
return { sideA, sideB, sideC };
220+
}
221+
177222
const SHAPES_2D = {
178223
rectangle: {
179224
label: 'Rectangle',
@@ -250,6 +295,32 @@ const SHAPES_2D = {
250295
];
251296
},
252297
},
298+
triangle: {
299+
label: 'Triangle',
300+
keys: ['sideA', 'sideB', 'sideC'],
301+
paramLabels: ['Side a', 'Side b', 'Side c'],
302+
ranges: { min: 1, max: 14, step: 1 },
303+
defaults: { sideA: 5, sideB: 6, sideC: 7 },
304+
compute(v) {
305+
const { sideA: a, sideB: b, sideC: c } = v;
306+
return {
307+
perimeter: a + b + c,
308+
area: triangleAreaFromSides(a, b, c),
309+
};
310+
},
311+
metricRows(m, units) {
312+
const perimeter = formatMeasurement(m.perimeter, 'linear', units);
313+
const area = formatMeasurement(m.area, 'area', units);
314+
return [
315+
{
316+
name: 'Perimeter',
317+
...perimeter,
318+
hint: 'P = a + b + c (sum of all three sides)',
319+
},
320+
{ name: 'Area', ...area, hint: "Heron's formula from three side lengths" },
321+
];
322+
},
323+
},
253324
};
254325

255326
const SHAPES_3D = {
@@ -449,7 +520,10 @@ function normalizeInitialState(config) {
449520
step: ranges.step,
450521
};
451522
});
452-
const values = normalizeValues(def, initial.values, displayRanges);
523+
let values = normalizeValues(def, initial.values, displayRanges);
524+
if (shape === 'triangle') {
525+
values = repairTriangleSides(values, displayRanges);
526+
}
453527

454528
return {
455529
mode: requestedMode,
@@ -839,6 +913,46 @@ function draw2D(ctx, canvas, shapeKey, values, rangesByKey = {}, units = DEFAULT
839913
ctx.font = canvasFont('--Fonts-Body-Default-xs');
840914
ctx.fillText(`a = ${formatDimensionLabel(a, units)}`, x0 + bx / 2 - 10, y0 + 18);
841915
ctx.fillText(`b = ${formatDimensionLabel(b, units)}`, x0 - 28, y0 - ay / 2);
916+
} else if (shapeKey === 'triangle') {
917+
const a = values.sideA;
918+
const b = values.sideB;
919+
const c = values.sideC;
920+
const xFrac = (a * a + c * c - b * b) / (2 * c);
921+
const height = triangleHeightOnBase(a, b, c);
922+
const leftmost = Math.min(0, xFrac);
923+
const rightmost = Math.max(c, xFrac);
924+
const span = rightmost - leftmost;
925+
const scale = Math.min(innerW / span, innerH / Math.max(height, 1)) * 0.85;
926+
const basePx = c * scale;
927+
const heightPx = height * scale;
928+
const topOffsetPx = xFrac * scale;
929+
const x0 = cx - (span * scale) / 2 - leftmost * scale;
930+
const y0 = cy + heightPx / 2;
931+
const v0 = { x: x0, y: y0 };
932+
const v1 = { x: x0 + basePx, y: y0 };
933+
const v2 = { x: x0 + topOffsetPx, y: y0 - heightPx };
934+
935+
ctx.beginPath();
936+
ctx.moveTo(v0.x, v0.y);
937+
ctx.lineTo(v1.x, v1.y);
938+
ctx.lineTo(v2.x, v2.y);
939+
ctx.closePath();
940+
ctx.fill();
941+
ctx.stroke();
942+
943+
const edgeLabel = (from, to, text, dx, dy) => {
944+
ctx.fillText(
945+
text,
946+
(from.x + to.x) / 2 + dx,
947+
(from.y + to.y) / 2 + dy,
948+
);
949+
};
950+
951+
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--Colors-Text-Body-Default') || '#334155';
952+
ctx.font = canvasFont('--Fonts-Body-Default-xs');
953+
edgeLabel(v0, v1, `c = ${formatDimensionLabel(c, units)}`, -12, 18);
954+
edgeLabel(v0, v2, `a = ${formatDimensionLabel(a, units)}`, -34, 0);
955+
edgeLabel(v1, v2, `b = ${formatDimensionLabel(b, units)}`, 18, 0);
842956
}
843957
}
844958

@@ -981,6 +1095,15 @@ async function initGeometryExplorer() {
9811095
}
9821096
out[key] = snapped;
9831097
});
1098+
if (getCurrentShapeKey(state) === 'triangle') {
1099+
const repaired = repairTriangleSides(out, rangesByKey);
1100+
def.keys.forEach((key, i) => {
1101+
if (out[key] !== repaired[key] && sliders[i]) {
1102+
sliders[i].setValue(repaired[key], null, false);
1103+
}
1104+
out[key] = repaired[key];
1105+
});
1106+
}
9841107
return out;
9851108
}
9861109

examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ For `"2d"`:
6363
- `"rectangle"`
6464
- `"circle"`
6565
- `"rightTriangle"`
66+
- `"triangle"`
6667

6768
For `"3d"`:
6869

@@ -80,6 +81,7 @@ back to the selected shape's defaults. Out-of-range values are clamped.
8081
| `rectangle` | `width`, `height` | `1` to `16` |
8182
| `circle` | `radius` | `1` to `12` |
8283
| `rightTriangle` | `legA`, `legB` | `1` to `14` |
84+
| `triangle` | `sideA`, `sideB`, `sideC` | `1` to `14` |
8385
| `prism` | `length`, `width`, `height` | `1` to `12` |
8486
| `cylinder` | `radius`, `height` | `1` to `12` |
8587
| `sphere` | `radius` | `1` to `12` |

0 commit comments

Comments
 (0)