feat: Improve construction element handling #32

Merged
glen merged 1 commits from name_v_caption into main 2023-09-25 00:47:36 +00:00
5 changed files with 448 additions and 63 deletions
Showing only changes of commit f1696120dc - Show all commits

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