archematics/src/adapptlet.civet

548 lines
22 KiB
Plaintext

import https://code.jquery.com/jquery-3.7.1.js
import type {AppletObject} from ./deps/geogebra/api.ts
colorsea from ./deps/colorsea.js
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',
-enableRightClick,
// +showMenuBar,
jApp.width,
jApp.height,
appletOnLoad: (api: AppletObject) =>
elements: JoyceElements := {}
backgroundRGB := [255, 255, 255] as RGB
for child of jApp.children
dispatchJcommand api, child, elements, backgroundRGB
api.setCoordSystem -10, 10 + jApp.width, -10, 10 + jApp.height
api.setAxesVisible false, false
api.setGridVisible false
} 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
type RGB = [number, number, number]
type XYZ = RGB
// 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
backgroundRGB: RGB): void
val := param.getAttribute 'value'
unless val return
attr := param.getAttribute 'name'
switch attr
'background'
backgroundHex := `#${val}`
api.setGraphicsOptions 1, bgColor: backgroundHex
newback := colorsea(backgroundHex).rgb()
for i of [0..2]
backgroundRGB[i] = newback[i]
'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, backgroundRGB
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,
backgroundRGB: RGB): 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) => invisible color
cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false
else // we have to decorate
dimension .= cmdr.parts.findLastIndex .includes name
cmdr.callbacks.push (api: AppletObject, parts: DimParts) =>
trace := false // e.g., klass is 'polygon'
// Operate in order faces, lines, point, caption so that
// we can adjust components after setting overall color, etc.
// Color the "Faces"; they default to 'brighter':
if invisible colors[3]
for each face of parts[2]
if face is name
console.log 'Fading out interior of', face if trace
// hide the interior by making it transparent
api.setFilling face, 0
else if face not in elements
console.log 'Hiding face', face if trace
api.setVisible face, false
else
faceRGB := joyce2rgb(colors[3] or 'brighter', backgroundRGB)
deep := ['circle', 'polygon', 'sector']
filling := deep.includes(klass) ? 0.7 : 0.2
for each face of parts[2]
console.log 'Coloring face', face, 'to', colors[3] if trace
api.setVisible face, true
api.setFilling face, filling
api.setColor face, ...faceRGB
// Lines default to black:
if invisible colors[2]
for each line of parts[1]
unless line in elements
console.log 'Hiding line', line if trace
api.setVisible line, false
else
lineRGB := joyce2rgb(colors[2] or 'black', backgroundRGB)
for each line of parts[1]
console.log 'Coloring line', line, 'to', colors[2] if trace
api.setVisible line, true
api.setColor line, ...lineRGB
// Now color the points:
if invisible colors[1]
// Hide all the dim-0 elements that are not their own independent
// items:
for each point of parts[0]
unless point in elements
console.log 'Hiding point', point if trace
api.setVisible point, false
else if dimension is 0 or colors[1] // Need to color the points
ptRGB := colors[1] ? joyce2rgb colors[1], backgroundRGB
: pointDefaultRGB name, method
for each point of parts[0]
console.log 'Coloring point', point, 'to', colors[1] if trace
api.setVisible point, true
api.setColor point, ...ptRGB
// Make the caption the correct color
if invisible colors[0]
console.log 'Hiding label', name if trace
api.setLabelVisible name, false
else if colors[dimension] and colors[dimension] is not colors[0]
// Have to make a text to provide the caption, since GeoGebra
// doesn't allow caption different color from entity.
textName := 'GeoText' + index
locationExpr := switch klass
when 'point' then `1.02${name})`
when 'line'
(`Midpoint(${name}) + `
+ `Rotate(Direction(${name})*Length(${name})*0.02, pi/2)`)
when 'circle'
`Center(${name}) + Radius(${name})*Vector((12/13,5/13))*1.03`
when 'polygon' then `Centroid(${name})`
when 'sector'
`(5*Center(${name}) - ${cmdr.ends?[0]} - ${cmdr.ends?[1]})/3`
when 'plane'
`Intersect(${name}, PerpendicularLine((0, 0, 0), ${name}))`
when 'sphere'
`Center(${name})+Radius(${name})*Vector((12/13,0,5/13))*1.03`
when 'polyhedron'
// The "ends" are faces or vertices roughly opposite
// from each other
[ex1, ex2] .= cmdr.ends ?? ['', '']
unless parts[0].includes ex1 then ex1 = `Centroid(${ex1})`
unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})`
`(4*${ex1}+${ex2})/5`
api.evalCommand `${textName} = Text("${jname}", ${locationExpr})`
api.setColor textName, ...joyce2rgb colors[0], backgroundRGB
// and hide the underlying GeoGebra label
api.setLabelVisible name, false
else if colors[0]
// Label gets the correct color from element
// but we had better make sure it is visible:
console.log 'Showing label', name if trace
api.setLabelVisible name, true
else
// label color is defaulting. Same as element for points, invisible
// otherwise:
show := klass is 'point'
console.log 'Setting label vis of', name, 'to', show if trace
api.setLabelVisible name, show
// window[hideListener] = (arg) =>
// api.setVisible name, false
// api.registerObjectUpdateListener name, hideListener
if cmdr.ends // line or sector
elements[jname] =
{otherName: name, usesCaptions, klass, cmdr.ends}
elements[name] =
{otherName: jname, usesCaptions, klass, cmdr.ends}
else // any other geometry
elements[jname] = {otherName: name, usesCaptions, klass}
elements[name] = {otherName: jname, usesCaptions, klass}
cmdr
function invisible(cname: string): boolean
cname is '0' or cname is 'none'
function joyce2rgb(cname: string, backgroundRGB?: RGB): RGB
whiteRGB: RGB := [255, 255, 255]
bg: RGB := backgroundRGB or whiteRGB
switch cname
// RGB values from Duck Duck Go search on "[COLOR] rgb"
/black/i
[0,0,0]
/blue/i
[0,0,255]
/cyan/i
[0,255,255]
/darkgray/i
[128,128,128]
/^gray/i
[169,169,169]
/green/i
[0,255,0]
/lightgray/i
[211,211,211]
/magenta/i
[255,0,255]
/orange/i
[255,165,0]
/pink/i
[255,192,203]
/red/i
[255,0,0]
/white/i
[255,255,255]
/yellow/i
[255,255,0]
/random/i
colorsea.random().lighten(40).rgb()
/background/i
bg
/brighter/i
colorsea(bg).lighten(30).rgb()
/darker/i
colorsea(bg).darken(20).rgb()
/^[0-9A-F]{6}$/i
colorsea(`#${cname}`).rgb()
/^\d+,\d+,\d+$/
// HSB specification
[H,S,B] := cname.split(',').map (s) => parseInt s
colorsea.hsv(H, S, B).rgb()
else
console.log 'Could not parse color:', cname
[128, 128, 128]
function pointDefaultRGB(name: string, method: string): RGB
// Need to short-circuit with green for pivot point, once that is implemented
switch method
'free'
joyce2rgb 'red'
/.*[Ss]lider$/
joyce2rgb 'orange'
else joyce2rgb 'black'
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 XYZ
'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()