@@ -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+
177222const 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
255326const 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
0 commit comments