feat: Improve construction element handling

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.
This commit is contained in:
Glen Whitney 2023-09-24 17:42:02 -07:00
parent 550ce0168c
commit f1696120dc
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> <h2>Joyce Geometry Applet</h2>
<ul> <ul>
<li> <a href="inscribed-equilateral.html">Before</a> </li> <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> <li> <a href="inscribed-modified.html">After</a> </li>
</ul> </ul>
<h2>WRL Files</h2> <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') } height: parseInt(this.getAttribute('height') ?? '200') }
`<div id="${id}"></div>` `<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', => jQuery.getScript 'https://www.geogebra.org/apps/deployggb.js', =>
for each jApp of joyceApplets for each jApp of joyceApplets
params := { params := {
@ -23,100 +47,325 @@ jQuery.getScript 'https://www.geogebra.org/apps/deployggb.js', =>
jApp.width, jApp.width,
jApp.height, jApp.height,
appletOnLoad: (api: AppletObject) => appletOnLoad: (api: AppletObject) =>
elements: JoyceElements := {}
for child of jApp.children for child of jApp.children
dispatchJcommand api, child dispatchJcommand api, child, elements
api.setCoordSystem(-10, 10 + jApp.width, -10, 10 + jApp.height) api.setCoordSystem(-10, 10 + jApp.width, -10, 10 + jApp.height)
} as const } as const
geoApp := new GGBApplet params geoApp := new GGBApplet params
geoApp.inject jApp.id 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 type Commander
command: string commands: string[]
callbacks: GeogebraCallback[] 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 = ( 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' val := param.getAttribute 'value'
unless val return unless val return
switch param.getAttribute 'name' attr := param.getAttribute 'name'
switch attr
'background' 'background'
api.setGraphicsOptions 1, bgColor: `#${val}` api.setGraphicsOptions 1, bgColor: `#${val}`
'title' 'title'
api.evalCommand `TitlePoint = Corner(1,1) api.evalCommand `TitlePoint = Corner(1,1)
Text("${val}", TitlePoint + (2,5))` Text("${val}", TitlePoint + (2,5))`
/e\[\d+\]/ /e\[\d+\]/
{command, callbacks} := jToG val num := parseInt(attr.slice(2))
if command {commands, callbacks, parts} := jToG val, elements, num
if api.evalCommand command if commands.length
for each cb of callbacks lastTried .= 0
cb(api) if commands.filter((&)).every (cmd) =>
else api.evalCommand(cmd) and ++lastTried
console.log `Geogebra command '${command}' translated from`, callbacks.forEach &(api, parts)
val, 'failed.' else console.log
else `Geogebra command '${commands[lastTried]}'
console.log `Could not parse command '${val}'` (part of translation of '${val}')
else failed.`
console.log `Unkown param ${param}` else console.log `Could not parse command '${val}'`
else console.log `Unkown param ${param}`
function jToG(jCom: string): Commander // function myListener(...args: unknown[]) {
[name, klass, method, data, ...colors] := jCom.split(';') // console.log 'In my listener with', args
if klass in classHandler // }
return classHandler[klass] name, method, data, colors
// 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}` console.log `Unknown entity class ${klass}`
command: '', callbacks: [] 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
classHandler: Record<string, ClassHandler> := function geoname(jname: JoyceName, elements: JoyceElements): GeoName
point: (name, method, data, colors) => numCode .= 0n
command .= '' numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname
callbacks: GeogebraCallback[] .= [] return .= 'Geo' + numCode.toString(36);
args := data.split(',') 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 switch method
/free|fixed/ /free|fixed/
command += `${name} = (${data})` commands.push `${name} = (${args.scalar?.join ','})`
if method is 'fixed' if method is 'fixed'
callbacks.push (api: AppletObject) => api.setFixed(name, true) callbacks.push (api: AppletObject) => api.setFixed(name, true)
'perpendicular' 'perpendicular'
[center, direction] := args // Note only the two-point option implemented so far
command += `${name} = Rotate(${direction}, 3*pi/2, ${center})` 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' 'angleDivider'
n .= -1 // Note doesn't yet handle plane argument
// use the fact that NaN doesn't equal itself: unless args.subpoints return
nLoc := args.findIndex((arg) => (n = parseInt arg) is n) [start, center, end] := args.subpoints
if n >= 0 // see if we need to make the destination segment from start to end
args.splice(nLoc) destination .= ''
[center, start, end] := args unless args.line?.length is 1 and args.point?[0] is center
command += `${name}aUx1 = Segment(${start}, ${end}) destination = aux + '1'
${name}aUx2 = Angle(${start}, ${center}, ${end}) auxiliaries.push destination
${name}aUx2a = If(${name}aUx2 > pi, ${name}aUx2 - 2*pi, ${name}aUx2) commands.push `${destination} = Segment(${start}, ${end})`
${name}aUx3 = Rotate(${start}, ${name}aUx2a/${n}, ${center}) else destination = args.line[0]
${name}aUx4 = Ray(${center}, ${name}aUx3) n := args.scalar?[0]
${name} = Intersect(${name}aUx1, ${name}aUx4)` 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' 'intersection'
command += `${name} = Intersect(${data})` // Checking Joyce source, means intersection of lines, not
return {command, callbacks} // 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) => line: (name, method, args) =>
command .= '' return := freshCommander()
callbacks: GeogebraCallback[] .= [] return.value.ends = ['', '']
args := data.split(',') {commands, callbacks, parts, auxiliaries, ends} := return.value
aux := name + 'aUx'
parts[1].push name
madeSegment .= false
switch method switch method
'connect' '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' 'parallel'
[newStart, oldStart, oldEnd] := args unless args.subpoints then return
command += `${name}aUx1 = Vector(${oldStart}, ${newStart}) [newStart, oldStart, oldEnd] := args.subpoints
${name}aUx2 = Translate(${oldEnd}, ${name}aUx1) commands.push `${aux}1 = Vector(${oldStart}, ${newStart})`
${name} = Segment(${newStart}, ${name}aUx2)` auxiliaries.push aux + 1, aux + 2
return {command, callbacks} 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) => circle: (name, method, args) =>
command .= '' return := freshCommander()
callbacks: GeogebraCallback[] .= [] {commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name
parts[1].push name
switch method switch method
'radius' 'radius'
[center, point] := data.split(',') unless args.subpoints then return
command += `${name} = Circle(${center}, ${point})` [center, point] := args.subpoints
return {command, callbacks} 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 canvas
// Put eye icons after all of the eligible links // 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 -> links.after ->
newSpan := $('<span>👁</span>') newSpan := $('<span>👁</span>')
newSpan.hover newSpan.hover