Not everything is tested, but tried to capture all of the structure of the original, except for pivot points green, since there are no pivot points yet. Also many of the color specifications are missing. Resolves #8.
512 lines
21 KiB
Text
512 lines
21 KiB
Text
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 := {}
|
|
for child of jApp.children
|
|
dispatchJcommand api, child, elements
|
|
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): void
|
|
val := param.getAttribute 'value'
|
|
unless val return
|
|
attr := param.getAttribute 'name'
|
|
backgroundHex .= '#FFFFFF'
|
|
switch attr
|
|
'background'
|
|
backgroundHex = `#${val}`
|
|
api.setGraphicsOptions 1, bgColor: backgroundHex
|
|
'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, backgroundHex
|
|
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,
|
|
backgroundHex: string): 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 := 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', backgroundHex)
|
|
for each face of parts[2]
|
|
console.log 'Coloring face', face, 'to', colors[3] if trace
|
|
api.setVisible face, true
|
|
api.setFilling face, 0.3
|
|
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
|
|
black: RGB := [0, 0, 0]
|
|
lineRGB := colors[2] ? joyce2rgb colors[2], backgroundHex : black
|
|
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], backgroundHex
|
|
: 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], backgroundHex
|
|
// 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) =>
|
|
// console.log('Hello', arg, 'disappearing', name)
|
|
// 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, backgroundHex: string): RGB
|
|
switch cname
|
|
/black/i
|
|
[0,0,0]
|
|
/cyan/i
|
|
[0,255,255]
|
|
/lightgray/i
|
|
[211,211,211]
|
|
/pink/i
|
|
[255,192,203]
|
|
/red/i
|
|
[255,0,0]
|
|
/brighter/i
|
|
colorsea(backgroundHex).lighten(20).rgb()
|
|
/darker/i
|
|
colorsea(backgroundHex).darken(20).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'
|
|
[255, 0, 0]
|
|
/.*[Ss]lider$/
|
|
[255, 165, 0]
|
|
else [0,0,0]
|
|
|
|
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()
|