archematics/src/adapptlet.civet

372 lines
15 KiB
Plaintext

import https://code.jquery.com/jquery-3.7.1.js
import type {AppletObject} from ./deps/geogebra/api.ts
type AppletDescription
html: string
children: HTMLCollection
id: string
width: number
height: number
joyceApplets: AppletDescription[] := []
$('applet[code="Geometry"]').before (i, html) ->
id := `joyceApplet${i}`
joyceApplets.push { html, this.children, id,
width: parseInt(this.getAttribute('width') ?? '200'),
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 := {
appName: 'classic',
jApp.width,
jApp.height,
appletOnLoad: (api: AppletObject) =>
elements: JoyceElements := {}
for child of jApp.children
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 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
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: GeoName, m: string, args: JoyceArguments, index: number) => Commander
// 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
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+\]/
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 myListener(...args: unknown[]) {
// console.log 'In my listener with', args
// }
// 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/
commands.push `${name} = (${args.scalar?.join ','})`
if method is 'fixed'
callbacks.push (api: AppletObject) => api.setFixed(name, true)
'perpendicular'
// 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'
// 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'
// 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, 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'
unless args.subpoints and args.subpoints.length is 2 then return
ends[0] = args.subpoints[0]
ends[1] = args.subpoints[1]
'parallel'
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, args) =>
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name
parts[1].push name
switch method
'radius'
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()