feat: Improve construction element handling (#32)

This change implements several additional construction methods,
  including the first polygon ones. In particular, it now allows
  arbitrary strings as entity names, even ones that are not allowed
  as GeoGebra identifiers, using captions to show the original
  entity names. In addition, line arguments are interpreted as a pair
  of point arguments as needed.

  Resolves #6.
  Resolves #30.
  Resolves #31.

Reviewed-on: #32
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
Glen Whitney 2023-09-25 00:47:35 +00:00 committed by Glen Whitney
parent 550ce0168c
commit bab48b25ad
5 changed files with 448 additions and 63 deletions

BIN
public/Geometry.zip Normal file

Binary file not shown.

View File

@ -8,6 +8,7 @@
<h2>Joyce Geometry Applet</h2>
<ul>
<li> <a href="inscribed-equilateral.html">Before</a> </li>
<li> <a href="inscribed-revived.html">Revived</a> </li>
<li> <a href="inscribed-modified.html">After</a> </li>
</ul>
<h2>WRL Files</h2>

View File

@ -0,0 +1,135 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<!-- fix buggy IE8, especially for mathjax -->
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>An equilateral triangle inscribed in a rectangle</title>
<link rel="stylesheet" type="text/css" media="screen" href="style.css">
<script type="text/javascript"
src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML,http://userpages.umbc.edu/~rostamia/mathjax-config.js">
MathJax.Hub.Queue( function() {document.body.style.visibility="visible"} );
</script>
</head>
<body style="visibility:hidden">
<h1>An equilateral triangle inscribed in a rectangle</h1>
<table class="centered">
<tr><td align="center">
<applet code="Geometry" archive="Geometry.zip" width="410" height="370">
<param name="background" value="ffffff">
<param name="title" value="An equilateral triangle inscribed in a rectangle">
<!-- the moving mechanism -->
<param name="e[1]" value="O;point;fixed;290,320">
<param name="e[2]" value="U1;point;fixed;510,320">
<param name="e[3]" value="V1;point;perpendicular;O,U1">
<param name="e[4]" value="U;point;angleDivider;U1,O,V1,3">
<param name="e[5]" value="V;point;angleDivider;V1,O,U1,3">
<param name="e[6]" value="circ1;circle;radius;O,U">
<param name="e[7]" value="li1;line;parallel;U,O,U1">
<param name="e[8]" value="li2;line;parallel;V,O,V1">
<param name="e[9]" value="W;point;intersection;li1,li2">
<param name="e[10]" value="VW;line;connect;V,W;0;0;lightGray">
<param name="e[11]" value="@;point;lineSegmentSlider;V,W,0,220;red;red">
<param name="e[12]" value="li3;line;parallel;@,O,U1">
<param name="e[13]" value="li4;line;chord;circ1,li3">
<param name="e[14]" value="X1;point;first;li4">
<!-- the triangle -->
<param name="e[15]" value="A;point;fixed;50,320">
<param name="e[16]" value="V2;point;perpendicular;A,U1">
<param name="e[17]" value="li5;line;parallel;A,O,X1">
<param name="e[18]" value="X2;point;last;li5">
<param name="e[19]" value="X;point;extend;A,X2,A,X2">
<param name="e[20]" value="tri1;polygon;equilateralTriangle;X,A">
<param name="e[21]" value="Y;point;vertex;tri1,3">
<param name="e[22]" value="B;point;midpoint;X,Y">
<param name="e[23]" value="ABC;polygon;equilateralTriangle;A,B">
<param name="e[24]" value="C;point;vertex;ABC,3">
<!-- the rectangle -->
<param name="e[25]" value="D;point;foot;B,A,U1">
<param name="e[26]" value="F;point;foot;C,A,V2">
<param name="e[27]" value="FE;line;parallel;F,A,D">
<param name="e[28]" value="E;point;last;FE">
<param name="e[29]" value="rect;polygon;quadrilateral;A,D,E,F;0;0;black;0">
<param name="e[30]" value="ADB;polygon;triangle;A,D,B;0;0;0;pink">
<param name="e[31]" value="ACF;polygon;triangle;A,C,F;0;0;0;pink">
<param name="e[32]" value="BCE;polygon;triangle;B,C,E;0;0;0;cyan">
</applet>
</td></tr>
<tr><td>
<b>
Slide the &ldquo;@&rdquo; up and down to change the geometry.<br>
Press &ldquo;r&rdquo; to reset the diagram to its initial state.<br>
Proposition: The blue area equals the sum of the two pink areas.
</b>
</td></tr></table>
<h2>Problem statement</h2>
<p>
The diagram above shows an equilateral triangle inscribed in a rectangle
in such a way that the two have a vertex in common. This subdivides the
rectangle into four disjoint triangles.
The original equilateral triangle is shown in white
in the diagram; the other three are shown in color.
<p>
<b>Proposition</b>
<em>
The area of the blue triangle equals the sum
of the areas of the two pink triangles.
</em>
<p>
The trigonometric proof is quite straightforward. I don't
know of a classical proof <i>a la</i> <span class="name">Euclid</span>.
(Well, actually I haven't tried much.)
If you can think of a neat non-trigonometric proof, let me know. I will
put it here with due credit.
<p>
This problem appeared as a conjecture
<a href="http://mathforum.org/kb/thread.jspa?forumID=129&amp;messageID=1083967">in an article</a>
in the <code>geometry.puzzles</code> newsgroup on March 15, 1997.
<p>
<b>Note added January 8, 2017:</b>
Here is a
<a href="inscribed-equilateral-solution.html">clever solution</a>
that <b>Peter Renz</b> sent me a in December 2016. Thanks, Peter!
<hr width="60%">
<p>
<em>This applet was created by
<a href="http://userpages.umbc.edu/~rostamia">Rouben Rostamian</a>
using
<a href="http://aleph0.clarku.edu/~djoyce/home.html">David Joyce</a>'s
<a href="http://aleph0.clarkU.edu/~djoyce/java/Geometry/Geometry.html">Geometry
Applet</a>
on July 2, 2010.
</em>
<p>
<table width="100%">
<tr>
<td valign="top">Go to <a href="index.html">Geometry Problems and Puzzles</a></td>
<td align="right" style="width:200px;">
<a href="http://validator.w3.org/check?uri=referer">
<img src="/~rostamia/images/valid-html401.png" class="noborder" width="88" height="31" alt="Valid HTML"></a>
<a href="http://jigsaw.w3.org/css-validator/check/referer">
<img src="/~rostamia/images/valid-css.png" class="noborder" width="88" height="31" alt="Valid CSS"></a>
</td></tr>
</table>
</body>
</html>

View File

@ -16,6 +16,30 @@ $('applet[code="Geometry"]').before (i, html) ->
height: parseInt(this.getAttribute('height') ?? '200') }
`<div id="${id}"></div>`
type Split<S extends string>
S extends `${infer W} ${infer R}` ? (W | Split<R>) : S
classes := 'point line circle polygon sector plane sphere polyhedron'
type JoyceClass = Split<typeof classes>
function assertJoyceClass(s: string): asserts s is JoyceClass
unless classes.includes s then throw new Error `Oops ${s} slipped through`
type JoyceName = string // we use this to indicate where the names
// from the Joyce commands (which are used as captions in the GeoGebra
// applet) go.
type GeoName = string // and this to indicate where GeoGebra identifiers go
type AnyName = GeoName | JoyceName // and this for slots that can be either
type Description
otherName: AnyName
usesCaptions: JoyceName[]
klass: JoyceClass
ends?: [GeoName, GeoName]
// We put both JoyceNames and GeoNames in here, pointing to each other
// with the otherName property:
type JoyceElements = Record<AnyName, Description>
jQuery.getScript 'https://www.geogebra.org/apps/deployggb.js', =>
for each jApp of joyceApplets
params := {
@ -23,100 +47,325 @@ jQuery.getScript 'https://www.geogebra.org/apps/deployggb.js', =>
jApp.width,
jApp.height,
appletOnLoad: (api: AppletObject) =>
elements: JoyceElements := {}
for child of jApp.children
dispatchJcommand api, child
dispatchJcommand api, child, elements
api.setCoordSystem(-10, 10 + jApp.width, -10, 10 + jApp.height)
} as const
geoApp := new GGBApplet params
geoApp.inject jApp.id
type GeogebraCallback = (api: AppletObject) => void
type DimParts = [string[], string[], string[]] // Gives GeoNames
// or expressions for 0-, 1-, and 2-dimensional parts for coloring
// need to pass the parts into the callbacks because sometimes the parts
// are not generated until callback time
type GeogebraCallback = (api: AppletObject, parts: DimParts) => void
type Commander
command: string
commands: string[]
callbacks: GeogebraCallback[]
parts: DimParts
auxiliaries: GeoName[] // extra entities needed in GeoGebra
ends?: [GeoName, GeoName]
function freshCommander(): Commander
commands: []
callbacks: []
parts: [[], [], []]
auxiliaries: []
type JoyceArguments =
Partial<Record<JoyceClass|'subpoints', GeoName[]> & {scalar: number[]}>
type ClassHandler = (
name: string, m: string, data: string, colors: string[]) => Commander
name: GeoName, m: string, args: JoyceArguments, index: number) => Commander
function dispatchJcommand(api: AppletObject, param: Element): void
// Executes the command corresponding to param against the GeoGebra applet
// api, consulting and extending by side effect the elements that are
// present in that applet
function dispatchJcommand(
api: AppletObject, param: Element, elements: JoyceElements): void
val := param.getAttribute 'value'
unless val return
switch param.getAttribute 'name'
attr := param.getAttribute 'name'
switch attr
'background'
api.setGraphicsOptions 1, bgColor: `#${val}`
'title'
api.evalCommand `TitlePoint = Corner(1,1)
Text("${val}", TitlePoint + (2,5))`
/e\[\d+\]/
{command, callbacks} := jToG val
if command
if api.evalCommand command
for each cb of callbacks
cb(api)
else
console.log `Geogebra command '${command}' translated from`,
val, 'failed.'
else
console.log `Could not parse command '${val}'`
else
console.log `Unkown param ${param}`
num := parseInt(attr.slice(2))
{commands, callbacks, parts} := jToG val, elements, num
if commands.length
lastTried .= 0
if commands.filter((&)).every (cmd) =>
api.evalCommand(cmd) and ++lastTried
callbacks.forEach &(api, parts)
else console.log
`Geogebra command '${commands[lastTried]}'
(part of translation of '${val}')
failed.`
else console.log `Could not parse command '${val}'`
else console.log `Unkown param ${param}`
function jToG(jCom: string): Commander
[name, klass, method, data, ...colors] := jCom.split(';')
if klass in classHandler
return classHandler[klass] name, method, data, colors
console.log `Unknown entity class ${klass}`
command: '', callbacks: []
// function myListener(...args: unknown[]) {
// console.log 'In my listener with', args
// }
classHandler: Record<string, ClassHandler> :=
point: (name, method, data, colors) =>
command .= ''
callbacks: GeogebraCallback[] .= []
args := data.split(',')
// window.myListener = myListener
// Parses a Joyce element-creating command, extending the elements
// by side effect:
function jToG(jCom: string, elements: JoyceElements, index: number): Commander
[jname, klass, method, data, ...colors] := jCom.split(';')
cmdr .= freshCommander()
unless klass in classHandler
console.log `Unknown entity class ${klass}`
return cmdr
assertJoyceClass klass // shouldn't need to do that :-/
name := if /^\p{L}\w*$/u.test jname then jname else geoname jname, elements
args: JoyceArguments := {}
usesCaptions := []
for each jdep of data.split ','
scalar := parseFloat jdep
if scalar is scalar // not NaN
(args.scalar ?= []).push scalar
continue
unless jdep in elements
console.log `Reference to unknown geometric entity ${jdep} in $jCom}`
return cmdr
usesCaptions.push jdep
{klass: depKlass, otherName: depGeo, ends} := elements[jdep]
(args[depKlass] ?= []).push depGeo
if depKlass is 'point'
(args.subpoints ?= []).push depGeo
else if depKlass is 'line'
(args.subpoints ?= []).push ...ends ?? []
cmdr = classHandler[klass] name, method, args, index
unless name is jname then cmdr.callbacks.push (api: AppletObject) =>
api.setCaption(name, jname)
api.setLabelStyle(name, 3) // style CAPTION = 3
if cmdr.auxiliaries.length
cmdr.callbacks.push (api: AppletObject) =>
for each aux of cmdr.auxiliaries
api.setAuxiliary(aux, true)
api.setVisible(aux,false)
// Create callback to assign colors
if colors.length is 4 and colors.every (color) =>
false and color is '0' or color is 'none'
cmdr.callbacks.push (api: AppletObject) =>
api.setVisible(name, false)
// window[hideListener] = (arg) =>
// console.log('Hello', arg, 'disappearing', name)
// api.setVisible(name, false)
api.registerObjectUpdateListener(name, hideListener)
if cmdr.ends or klass is 'line'
elements[jname] =
{otherName: name, usesCaptions, klass: 'line', cmdr.ends}
elements[name] =
{otherName: jname, usesCaptions, klass: 'line', cmdr.ends}
else // any other geometry
elements[jname] = {otherName: name, usesCaptions, klass}
elements[name] = {otherName: jname, usesCaptions, klass}
cmdr
function geoname(jname: JoyceName, elements: JoyceElements): GeoName
numCode .= 0n
numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname
return .= 'Geo' + numCode.toString(36);
return += '1' while return.value in elements
// All of the detailed semantics of each available command lies in this
// function.
classHandler: Record<JoyceClass, ClassHandler> :=
point: (name, method, args): Commander =>
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
aux := name + 'aUx'
parts[0].push name
switch method
/free|fixed/
command += `${name} = (${data})`
commands.push `${name} = (${args.scalar?.join ','})`
if method is 'fixed'
callbacks.push (api: AppletObject) => api.setFixed(name, true)
'perpendicular'
[center, direction] := args
command += `${name} = Rotate(${direction}, 3*pi/2, ${center})`
// Note only the two-point option implemented so far
unless args.subpoints return
[center, direction] := args.subpoints
// Note clockwise 90° rotation (3π/2) confirmed in Joyce source
commands.push `${name} = Rotate(${direction}, 3*pi/2, ${center})`
'angleDivider'
n .= -1
// use the fact that NaN doesn't equal itself:
nLoc := args.findIndex((arg) => (n = parseInt arg) is n)
if n >= 0
args.splice(nLoc)
[center, start, end] := args
command += `${name}aUx1 = Segment(${start}, ${end})
${name}aUx2 = Angle(${start}, ${center}, ${end})
${name}aUx2a = If(${name}aUx2 > pi, ${name}aUx2 - 2*pi, ${name}aUx2)
${name}aUx3 = Rotate(${start}, ${name}aUx2a/${n}, ${center})
${name}aUx4 = Ray(${center}, ${name}aUx3)
${name} = Intersect(${name}aUx1, ${name}aUx4)`
// Note doesn't yet handle plane argument
unless args.subpoints return
[start, center, end] := args.subpoints
// see if we need to make the destination segment from start to end
destination .= ''
unless args.line?.length is 1 and args.point?[0] is center
destination = aux + '1'
auxiliaries.push destination
commands.push `${destination} = Segment(${start}, ${end})`
else destination = args.line[0]
n := args.scalar?[0]
commands.push
`${aux}2 = Angle(${start}, ${center}, ${end})`
`${aux}3 = If(${aux}2 > pi, ${aux}2 - 2*pi, ${aux}2)`
`${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center})`
`${name} = Intersect(${destination}, Ray(${center}, ${aux}4))`
auxiliaries.push ...[2..4].map (i) => `${aux}${i}`
'intersection'
command += `${name} = Intersect(${data})`
return {command, callbacks}
// Checking Joyce source, means intersection of lines, not
// intersection of line segments
unless args.subpoints then return
l1 := `Line(${args.subpoints[0]},${args.subpoints[1]})`
l2 := `Line(${args.subpoints[2]},${args.subpoints[3]})`
commands.push `${name} = Intersect(${l1},${l2})`
'lineSegmentSlider'
segment .= args.line?[0]
unless segment
unless args.point then return
commands.push `${aux} = Segment(${args.point.join ','})`
auxiliaries.push aux
segment = aux
commands.push `${name} = Point(${segment})`
if args.scalar and args.scalar.length
callbacks.push (api: AppletObject) =>
api.setCoords name,
...args.scalar as [number, number, number]
'first'
unless args.subpoints then return
commands.push `${name} = ${args.subpoints[0]}`
'last'
unless args.subpoints then return
commands.push `${name} = ${args.subpoints.at(-1)}`
'extend'
unless args.subpoints then return
sp := args.subpoints
direction .= `UnitVector(Vector(${sp[0]},${sp[1]}))`
if args.line and (
not args.point or args.point[0] !== args.subpoints[0])
direction = `UnitVector(${args.line[0]})`
displacement := `Distance(${sp[2]}, ${sp[3]})*${direction}`
commands.push `${name} = Translate(${sp[1]}, ${displacement})`
'vertex'
commands.push
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
'midpoint'
if args.line
commands.push `${name} = Midpoint(${args.line[0]})`
else
commands.push
`${name} = Midpoint(${args.point?[0]},${args.point?[1]})`
'foot'
pt := args.subpoints
unless pt then return
commands.push
`${name} = ClosestPoint(Line(${pt[1]},${pt[2]}), ${pt[0]})`
line: (name, method, data, colors) =>
command .= ''
callbacks: GeogebraCallback[] .= []
args := data.split(',')
line: (name, method, args) =>
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
aux := name + 'aUx'
parts[1].push name
madeSegment .= false
switch method
'connect'
command += `${name} = Segment(${data})`
unless args.subpoints and args.subpoints.length is 2 then return
ends[0] = args.subpoints[0]
ends[1] = args.subpoints[1]
'parallel'
[newStart, oldStart, oldEnd] := args
command += `${name}aUx1 = Vector(${oldStart}, ${newStart})
${name}aUx2 = Translate(${oldEnd}, ${name}aUx1)
${name} = Segment(${newStart}, ${name}aUx2)`
return {command, callbacks}
unless args.subpoints then return
[newStart, oldStart, oldEnd] := args.subpoints
commands.push `${aux}1 = Vector(${oldStart}, ${newStart})`
auxiliaries.push aux + 1, aux + 2
ends[0] = newStart
ends[1] = aux + 2
if args.line?.length is 1 and args.point?[0] is args.subpoints[0]
// In this case we are translating an existing segment
commands.push
`${name} = Translate(${args.line[0]}, ${aux}1)`
`${aux}2 = Vertex(${name}, 2)`
madeSegment = true
else
commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)`
'chord'
// To match Joyce, we need to get the ordering here correct.
// ends[0] should be the one closer to args.subpoints[0]
unless args.subpoints and args.circle then return
line := `Line(${args.subpoints.join ','})`
pt := args.subpoints[0]
commands.push ...[1..2].map (n) =>
`${aux}${n} = Intersect(${args.circle}, ${line}, ${n})`
condition := `Distance(${aux}2,${pt}) < Distance(${aux}1,${pt})`
// NOTE: Joyce's code has special case for when pt is almost
// at midpoint of chord; in that case, it starts at endpoint
// closer to the second subpoint... postponing that nicety
commands.push
`${aux}3 = If(${condition}, ${aux}2, ${aux}1)`
`${aux}4 = If(${condition}, ${aux}1, ${aux}2)`
ends[0] = aux + 3
ends[1] = aux + 4
auxiliaries.push ...[1..4].map (n) => aux + n
unless madeSegment
commands.push `${name} = Segment(${ends[0]},${ends[1]})`
callbacks.push (api: AppletObject) =>
api.setLabelVisible(name, true)
parts[0].push ...ends
circle: (name, method, data, colors) =>
command .= ''
callbacks: GeogebraCallback[] .= []
circle: (name, method, args) =>
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name
parts[1].push name
switch method
'radius'
[center, point] := data.split(',')
command += `${name} = Circle(${center}, ${point})`
return {command, callbacks}
unless args.subpoints then return
[center, point] := args.subpoints
commands.push `${name} = Circle(${center}, ${point})`
callbacks.push (api: AppletObject) => api.setLabelVisible(name, true)
polygon: (name, method, args, index) =>
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name
// what to push for edges?
switch method
'equilateralTriangle'
pt := args.subpoints
unless pt then return
commands.push '' // hack, make sure there is a command
parts[0].push pt[0], pt[1]
callbacks.push (api: AppletObject, moreParts: DimParts) =>
made:= api.evalCommandGetLabels
`${name} = Polygon(${pt[1]},${pt[0]}, 3)`
if not made return
for each obj of made.split ','
if obj is name continue
newObj := 'GeoAux' + index + obj
api.renameObject obj, newObj
switch api.getObjectType newObj
'segment'
parts[1].push newObj
'point'
parts[0].push newObj
api.setVisible(newObj, false)
/triangle|quadrilateral/
pt := args.subpoints
unless pt then return
commands.push ''
parts[0].push ...pt
callbacks.push (api: AppletObject, moreParts: DimParts) =>
made := api.evalCommandGetLabels
`${name} = Polygon(${pt.join ','})`
if not made return
for each obj of made.split ','
if obj is name continue
newObj := 'GeoAux' + index + obj
api.renameObject obj, newObj
parts[1].push newObj
sector: (name, method, args) => freshCommander()
plane: (name, method, args) => freshCommander()
sphere: (name, method, args) => freshCommander()
polyhedron: (name, method, args) => freshCommander()

View File

@ -47,7 +47,7 @@ function makeBrowser(url: string)
canvas
// Put eye icons after all of the eligible links
links := $('a').filter -> !!@.getAttribute('href')?.match knownExtensions
links := $('a').filter -> knownExtensions.test @.getAttribute('href') ?? ''
links.after ->
newSpan := $('<span>👁</span>')
newSpan.hover