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:
parent
550ce0168c
commit
bab48b25ad
5 changed files with 448 additions and 63 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue