feat: Implement 3D Joyce Applets via JSXGraph

This is a stub/very preliminary implementation of calling JSXGraph for
  3D Joyce applets. The only element/construction method implemented so far
  is a free point.

  NOTE: This implementation is so far extremely buggy. Loading a page with a
  3D applet such as
  http://aleph0.clarku.edu/~djoyce/java/elements/bookXI/defXI9.html
  appears to enter a loop in which the div containing the JSXGraph Board
  progressively grows larger and larger, while using a tremendous amount
  of cpu.
This commit is contained in:
Glen Whitney 2024-06-09 22:07:13 -07:00
parent e742ef3460
commit 4e2375b709
45 changed files with 23637 additions and 20477 deletions

View file

@ -2,6 +2,12 @@ import ./deps/jquery.js
import type {AppletObject} from ./deps/geotypes/api.ts
import {AppletDescription, AdapParams, params, contains3d} from ./adapptypes.ts
colorsea from ./deps/colorsea.js
JXG from ./deps/jsxgraphcore.mjs
/* NOTE that the actual actions of the script are at the very bottom,
* so that all of the lexical variables will have been initialized by
* the time that those actions execute.
*/
joyceApplets: AppletDescription[] := []
$('applet[code="Geometry"]').before (i, html) ->
@ -20,22 +26,25 @@ 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
// from the Joyce commands (which are used as captions in the dynamic geometry
// 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 DyName = string // and this to indicate where dynamic geometry identifiers go
type AnyName = DyName | JoyceName // and this for slots that can be either
type Description
type Description // the semantic information needed about a Joyce geometry elt
otherName: AnyName
jsxElement?: JXG.GeometryElement // only JSX exposes JavaScript entities
usesCaptions: JoyceName[]
klass: JoyceClass
ends?: [GeoName, GeoName]
parts?: string[][]
ends?: [DyName, DyName]
parts?: string[][] // FIXME: Should be DyName[][], I think
// We put both JoyceNames and GeoNames in here, pointing to each other
// We put both JoyceNames and DyNames in here, pointing to each other
// with the otherName property:
type JoyceElements = Record<AnyName, Description>
type DynApp = AppletObject | JXG.Board | JXG.GeometryElement
adapptScript := findAdappt() as HTMLScriptElement
function findAdappt()
@ -57,19 +66,33 @@ function vertFlipped(coords: number[], cdata: ConstructionData): XYZ
coords = coords.slice()
if cdata.is3d
if coords[Z] then coords[Z] = -coords[Z]
while coords.length < 3
coords.push 0
coords[Y] = cdata.height - coords[Y]
return coords as XYZ
type RGB = [number, number, number]
type ConstructionData
id: string
bg: RGB
is3d: boolean
isJSX: boolean
width: number
height: number
labelOffset?: [number, number]
elements: JoyceElements
pivot: string
title?: string
// Helper function to deal with typing the various apis
type DiscriminatedAPI
jsxApi?: JXG.Board | JXG.View3D
geoApi?: AppletObject
function getApis(api: DynApp, cdata: ConstructionData) : DiscriminatedAPI
if cdata.isJSX then return {jsxApi: api as JXG.Board | JXG.View3D}
else return {geoApi: api as AppletObject}
// Global data setup for pivoting (ugh, but necessary because the api
// is not passed to the callback, so we have to look up the slider in
// a global list):
@ -86,6 +109,45 @@ pivotData: Record<string, PivotData> := {}
function postApplets(jApplets: AppletDescription[], codebase = '')
for each jApp of jApplets
is3d := contains3d jApp.params
isJSX := is3d // For now; will eventually just be always true
elements := {}
pivot .= ''
cdata: ConstructionData := {
bg: ([255, 255, 255] as RGB),
is3d, isJSX, jApp.id, jApp.width, jApp.height, elements, pivot}
if 'pivot' in jApp.params
if is3d
console.warn('Geometry Applet "pivot" only supported for '
+ '2D constructions. Ignoring.')
// FIXME: JSXGraph might well support "pivot" for 3D, by moving
// the implicit centerpoint of the "trackball navigation" mode of
// dragging to the pivot point.
else
pivot = dyname(jApp.id, cdata, 'point') + 'Pivot'
cdata.pivot = pivot
if isJSX
// for now, only use JSXGraph for 3D constructions
board := JXG.JSXGraph.initBoard jApp.id, {
boundingbox:
[-jApp.width-10, jApp.height+10, jApp.width+10, -jApp.height-10],
+zoom,
pan: {+enabled, -needShift},
drag: {+enabled},
-grid }
if is3d // redundant for now, but won't be when we use JSXGraph always
depth .= jApp.width
if jApp.height > depth then depth = jApp.height
depth /= 8
view := board.create 'view3d',
[ [-jApp.width, -jApp.height],
[2*jApp.width, 2*jApp.height],
[ [-10 + jApp.width/6, 10 + 4*jApp.width/6],
[-10 + jApp.height/6, 10 + 4*jApp.height/6],
[-2*depth - 10, depth + 10]]]
for name, value in jApp.params
dispatchJcommand view, name, value, cdata
continue
params := {
appName: 'classic',
-enableRightClick,
@ -93,7 +155,6 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
jApp.width,
jApp.height,
appletOnLoad: (api: AppletObject) =>
is3d := contains3d jApp.params
if is3d
api.enable3D true
api.setPerspective 'T'
@ -105,20 +166,10 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
api.setPerspective 'G'
if adapParams.config?.algebra
api.setPerspective '+A'
elements := {}
pivot .= ''
if 'pivot' in jApp.params
if is3d
console.warn('Geometry Applet "pivot" only supported for '
+ '2D constrcutions. Ignoring.')
else
pivot = geoname(jApp.id, elements, 'point') + 'Pivot'
pivotData[pivot] = {
api, jApp.params.pivot,
lastAngle: 0, rotatable: [], maybeRotatable: {}, fixed: {}}
cdata: ConstructionData := {
bg: ([255, 255, 255] as RGB),
is3d, jApp.id, jApp.width, jApp.height, elements, pivot}
if pivot
pivotData[pivot] = {
api, jApp.params.pivot,
lastAngle: 0, rotatable: [], maybeRotatable: {}, fixed: {}}
for name, value in jApp.params
dispatchJcommand
api, name, value, cdata
@ -147,22 +198,19 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
if codebase then geoApp.setHTML5Codebase codebase
geoApp.inject jApp.id
// Always use the final joyceApplets if there are any:
if joyceApplets.length
adapParams.joyceApplets = joyceApplets
if adapParams.joyceApplets.length
if adapParams.loader
jQuery.getScript adapParams.loader, =>
postApplets adapParams.joyceApplets
else
postApplets adapParams.joyceApplets, adapParams.codebase
/* That's all of the actions of this script. All of the remainder is
* implementation.
*/
type DimParts = [string[], string[], string[]] // Gives GeoNames
type JoyceArguments =
Partial<Record<JoyceClass|'subpoints', DyName[]> & {scalar: number[]}>
type Element // the syntactic specfication of a Joyce construction element
index: number
name: DyName
jname: JoyceName
klass: JoyceClass
method: string
args: JoyceArguments
usesCaptions: JoyceName[]
colors: string[]
type DimParts = [string[], string[], string[]] // Gives DyNames
// or expressions for 0-, 1-, and 2-dimensional parts for coloring
// need to pass the parts into the callbacks because sometimes the parts
@ -172,8 +220,8 @@ type Commander
commands: string[]
callbacks: GeogebraCallback[]
parts: DimParts
auxiliaries: GeoName[] // extra entities needed in GeoGebra
ends?: [GeoName, GeoName]
auxiliaries: DyName[] // extra entities needed in JavaScript dynamic app
ends?: [DyName, DyName]
function freshCommander(): Commander
commands: []
@ -181,265 +229,79 @@ function freshCommander(): Commander
parts: [[], [], []]
auxiliaries: []
type JoyceArguments =
Partial<Record<JoyceClass|'subpoints', GeoName[]> & {scalar: number[]}>
type ClassHandler = (
name: GeoName,
method: string,
args: JoyceArguments,
index: number,
cdata: ConstructionData,
colors: string[],
jname: string) => Commander
type RGB = [number, number, number]
type ClassHandler = (elt: Element, cdata: ConstructionData) => Commander
type XYZ = RGB
// For interpreting Joyce applet `align` parameter
alignTranslation: Record<string, [number, number]|undefined> := {
above: [0,10]
right: [10, 0]
below: [0,-10]
left: [-10, 0]
central: undefined }
// 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,
api: DynApp,
name: string,
value: string,
cdata: ConstructionData): void
{geoApi, jsxApi} := getApis api, cdata
switch name
'background'
cdata.bg = joyce2rgb value, cdata.bg
if adapParams.config?.commands
console.log 'Setting background to', value, 'interpreted as',
cdata.bg
api.setGraphicsOptions 1, bgColor: colorsea(cdata.bg).hex()
col := colorsea(cdata.bg).hex()
if cdata.isJSX // just take advantage of JSXGraph transparency
domElt := document.getElementById(cdata.id)
if domElt then domElt.style.background = col
else if geoApi then geoApi.setGraphicsOptions 1, bgColor: col
'title'
if adapParams.config?.commands
console.log 'Setting title to', value
cdata.title = value
api.evalCommand `TitlePoint = Corner(1,1)
Text("${value}", TitlePoint + (2,5))`
if cdata.isJSX // Currently no good way to make titles :-(
console.log('Title of Joyce applet', cdata.id, 'is', value)
else if geoApi
geoApi.evalCommand `TitlePoint = Corner(1,1)
Text("${value}", TitlePoint + (2,5))`
'pivot'
return // already handled in postApplets
'align'
console.warn
if cdata.isJSX // need to change the default offset of labels
cdata.labelOffset = alignTranslation[value.toLowerCase()]
// FIXME: need to actually read this when labeling
else console.warn
'Label alignment is not available in GeoGebra'
'translation, as there is no facility for automatically'
'positioning labels. However, they can be dragged manually.'
return
/e\[\d+\]/
num := parseInt(name.slice(2))
{commands, callbacks, parts} :=
jToG value, num, cdata
if commands.length
lastTried .= 0
if commands.filter((&)).every (cmd) =>
if adapParams.config?.commands
console.log 'Translated to:', cmd
api.evalCommand(cmd) and ++lastTried
callbacks.forEach &(api, parts)
else console.warn
`Geogebra command '${commands[lastTried]}'
(part of translation of '${value}')
failed.`
else console.warn `Could not parse command '${value}'`
if jsxApi
jAsJSX value, num, jsxApi, cdata
else if geoApi
{commands, callbacks, parts} := jToG value, num, cdata
if commands.length
lastTried .= 0
if commands.filter(&).every (cmd) =>
if adapParams.config?.commands
console.log 'Translated to:', cmd
geoApi.evalCommand(cmd) and ++lastTried
callbacks.forEach &(geoApi, parts)
else console.warn
`Geogebra command '${commands[lastTried]}'
(part of translation of '${value}')
failed.`
else console.warn `Could not parse command '${value}'`
else console.warn `Unknown param ${name} = ${value}`
// Parses a Joyce element-creating command, extending the elements
// by side effect:
function jToG(
jCom: string,
index: number,
cdata: ConstructionData): Commander
[jname, klass, method, data, ...colors] := jCom.split ';'
if adapParams.config?.commands
console.log 'Defining', jname, 'as a', klass, 'constructed by',
method, 'from', data, 'colored as', colors
cmdr .= freshCommander()
unless klass in classHandler
console.warn `Unknown entity class ${klass}`
return cmdr
assertJoyceClass klass // shouldn't need to do that :-/
isPivot := !!cdata.pivot and jname is pivotData[cdata.pivot].pivot
name := geoname jname, cdata.elements, klass
if isPivot then pivotData[cdata.pivot].pivot = name
args: JoyceArguments := {}
usesCaptions := []
for each jdep of data.split ','
scalar := Number jdep
if scalar is scalar // not NaN
(args.scalar ?= []).push scalar
continue
if jdep is 'screen'
// special case for Joyce; assume xOyPlane
(args.plane ?= []).push 'xOyPlane'
else
unless jdep in cdata.elements
console.warn
`Reference to unknown geometric entity ${jdep} in ${jCom}`
return cmdr
usesCaptions.push jdep
{klass: depKlass, otherName: depGeo, ends} := cdata.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, cdata, colors, jname
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 and not adapParams.config?.showaux
cmdr.callbacks.push (api: AppletObject) =>
for each aux of cmdr.auxiliaries
api.setAuxiliary aux, true
api.setLabelVisible aux, false
api.setVisible aux,false
// set up the pivot if there is one
if isPivot
unless klass is 'point'
console.warn(`Can only pivot around a point, not the ${klass}`
+ `named ${jname}. Ignoring.`)
cdata.pivot = ''
cmdr.commands.push(`${cdata.pivot} = `
+ `Slider(0°,360°,1°,1,${cdata.width/3},true,true,false,false)`)
cmdr.callbacks.push (api: AppletObject) =>
api.setCaption cdata.pivot, 'Rotate Display'
api.setLabelStyle cdata.pivot, 3
api.setCoords(cdata.pivot, 2*cdata.width/3, cdata.height-10)
// Not sure how to let TypeScript deal with putting a new function
// on the global window object, so punting at least for now:
// @ts-ignore
window.pivotListener = pivotListener
api.registerObjectUpdateListener(cdata.pivot, 'pivotListener')
// Create callback to assign colors
traceC := adapParams.config?.color
console.log 'Considering coloring', name, 'with', colors if traceC
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) =>
// 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]) and (klass !== 'sphere' or invisible colors[2])
for each face of parts[2]
if face is name
console.log 'Fading out interior of', face if traceC
// hide the interior by making it transparent
api.setFilling face, 0
else if face not in cdata.elements
console.log 'Hiding face', face if traceC
api.setVisible face, false
else
surface .= colors[3]
if klass is 'sphere' and invisible surface
surface = colors[2] // for Joyce, spheres had one circular "edge"
faceRGB := joyce2rgb(surface or 'brighter', cdata.bg)
deep := ['circle', 'polygon', 'sector']
filling := deep.includes(klass) ? 0.7 : 0.2
for each face of parts[2]
if traceC
console.log 'Coloring face', face, 'to',
surface, '=', faceRGB
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]
if line is name or line not in cdata.elements
console.log 'Hiding line', line if traceC
api.setVisible line, false
else
lineRGB := joyce2rgb(colors[2] or 'black', cdata.bg)
if traceC
console.log 'Need to color lines', parts[1], 'with', lineRGB
for each line of parts[1]
console.log 'Coloring line', line, 'to', colors[2] if traceC
api.setVisible line, true
api.setColor line, ...lineRGB
// Now color the points:
if traceC
console.log
'Considering point colors for', name, 'of dimension', dimension
if invisible colors[1]
// Hide all the dim-0 elements that are not distinct independent
// items:
for each point of parts[0]
if point is name or point not in cdata.elements
console.log 'Hiding point', point if traceC
api.setVisible point, false
else if dimension is 0 or colors[1] // Need to color the points
if not colors[1]
colors[1] = pointDefaultColorName name, method, isPivot
ptRGB := joyce2rgb colors[1], cdata.bg
for each point of parts[0]
console.log 'Coloring point', point, 'to', colors[1] if traceC
api.setVisible point, true
api.setColor point, ...ptRGB
// Make the caption the correct color
if invisible colors[0]
console.log 'Hiding label', name if traceC
api.setLabelVisible name, false
else if colors[0]
if colors[dimension+1] and colors[dimension+1] 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.025/Length(Direction(${name})),`
+ `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`
textCmd := `${textName} = Text("${jname}", ${locationExpr})`
textCol := joyce2rgb colors[0], cdata.bg
if traceC
console.log `Making text '${textCmd}' colored`, textCol
api.evalCommand textCmd
api.setColor textName, ...textCol
// and hide the underlying GeoGebra label
api.setLabelVisible name, false
else // specified label color matches the entity color
// So label gets the correct color from element
// but we had better make sure it is visible:
console.log 'Showing label', name if traceC
api.setLabelVisible name, true
else // label color is defaulting
// Make it same as the element for points, invisible otherwise:
show := klass is 'point'
console.log 'Setting label vis of', name, 'to', show if traceC
api.setLabelVisible name, show
cdata.elements[jname] =
{otherName: name, usesCaptions, klass, cmdr.ends, cmdr.parts}
cdata.elements[name] =
{otherName: jname, usesCaptions, klass, cmdr.ends, cmdr.parts}
cmdr
function pivotListener(slider: string)
pd := pivotData[slider]
@ -527,8 +389,9 @@ function pointDefaultColorName(
'orange'
else 'black'
function geoname(
jname: JoyceName, elements: JoyceElements, klass: JoyceClass): GeoName
function dyname(
jname: JoyceName, cd: ConstructionData, klass: JoyceClass): DyName
if cd.isJSX then return jname
unless jname is 'floor' or jname.substring(0,3) is 'Geo' // those might clash
// Names with word characters starting with a capital are always good:
if /^[A-Z]['\w]*$/.test jname then return jname
@ -538,7 +401,7 @@ function 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
return += '1' while return.value in cd.elements
// Helpers for some corresponding point/line functions:
function cutoffExtend(
@ -583,7 +446,8 @@ function proportionSimilar(
// All of the detailed semantics of each available command lies in this
// function.
classHandler: Record<JoyceClass, ClassHandler> :=
point: (name, method, args, index, cdata, colors, jname): Commander =>
point: (elt, cdata): Commander =>
{name, jname, method, args, colors} .= elt // Mutable for hack patches
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
zeroVector := cdata.is3d ? 'Vector((0,0,0))' : 'Vector((0,0))'
@ -801,7 +665,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
else console.warn 'Unknown point method:', method
line: (name, method, args, index, cdata) =>
line: (elt, cdata) =>
{name, method, args} := elt
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
@ -951,7 +816,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
parts[0].push ...ends
circle: (name, method, args, index, cdata) =>
circle: (elt, cdata) =>
{name, method, args} := elt
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
@ -987,11 +853,12 @@ classHandler: Record<JoyceClass, ClassHandler> :=
makeLinesInvisible callbacks, aux
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
polygon: (name, method, args, index, cdata) =>
polygon: (elt, cdata) =>
{name, method, args, index} := elt
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name
aux := geoname name + 'aUx', cdata.elements, 'point'
aux := dyname name + 'aUx', cdata, 'point'
switch method
/equilateralTriangle|square|regularPolygon/
pt := args.subpoints
@ -1077,7 +944,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
api.renameObject obj, newObj
moreParts[1].push newObj
sector: (name, method, args, index, cdata) =>
sector: (elt, cdata) =>
{name, method, args} := elt
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
@ -1108,7 +976,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
auxiliaries.push aux + 1
makeLinesInvisible callbacks, name
plane: (name, method, args, index, cdata) =>
plane: (elt, cdata) =>
{name, method, args} := elt
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name
@ -1126,7 +995,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
commands.push
`${name} = PerpendicularPlane(${thru}, Line(${thru}, ${perp}))`
sphere: (name, method, args) =>
sphere: (elt, cdata) =>
{name, method, args} := elt
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
@ -1144,11 +1014,12 @@ classHandler: Record<JoyceClass, ClassHandler> :=
radius := `Distance(${pt[1]}, ${pt[2]})`
commands.push `${name} = Sphere(${center}, ${radius})`
polyhedron: (name, method, args, index, cdata) =>
polyhedron: (elt, cdata) =>
{name, method, args, index} := elt
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
aux := geoname name + 'aUx', cdata.elements, 'point'
aux := dyname name + 'aUx', cdata, 'point'
switch method
'parallelepiped'
pt .= args.subpoints
@ -1260,6 +1131,217 @@ classHandler: Record<JoyceClass, ClassHandler> :=
'triangle'
moreParts[2].push newObj
// Parses a Joyce element-creating command
function parseElement(
jCom: string,
index: number,
cdata: ConstructionData): Element | undefined
[jname, klass, method, data, ...colors] := jCom.split ';'
if adapParams.config?.commands
console.log 'Defining', jname, 'as a', klass, 'constructed by',
method, 'from', data, 'colored as', colors
unless klass in classHandler
console.warn `Unknown entity class ${klass}`
return undefined
assertJoyceClass klass // shouldn't need to do that :-/
name := dyname jname, cdata, klass
args: JoyceArguments := {}
usesCaptions: JoyceName[] := []
for each jdep of data.split ','
scalar := Number jdep
if scalar is scalar // not NaN
(args.scalar ?= []).push scalar
continue
if jdep is 'screen'
// special case for Joyce; assume xOyPlane
(args.plane ?= []).push 'xOyPlane'
else
unless jdep in cdata.elements
console.warn
`Reference to unknown geometric entity ${jdep} in ${jCom}`
return undefined
usesCaptions.push jdep
{klass: depKlass, otherName: depDyn, ends} := cdata.elements[jdep]
(args[depKlass] ?= []).push depDyn
if depKlass is 'point'
(args.subpoints ?= []).push depDyn
else if depKlass is 'line'
(args.subpoints ?= []).push ...ends ?? []
return {index, name, jname, klass, method, args, usesCaptions, colors}
// Parses a Joyce element-creating command, extending the elements
// by side effect:
function jToG(
jCom: string,
index: number,
cdata: ConstructionData): Commander
elt := parseElement jCom, index, cdata
cmdr .= freshCommander()
unless elt then return cmdr
{name, jname, klass, method, usesCaptions, colors} := elt
isPivot := !!cdata.pivot and elt.jname is pivotData[cdata.pivot].pivot
if isPivot then pivotData[cdata.pivot].pivot = elt.name
cmdr = classHandler[klass] elt, cdata
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 and not adapParams.config?.showaux
cmdr.callbacks.push (api: AppletObject) =>
for each aux of cmdr.auxiliaries
api.setAuxiliary aux, true
api.setLabelVisible aux, false
api.setVisible aux,false
// set up the pivot if there is one
if isPivot
unless klass is 'point'
console.warn(`Can only pivot around a point, not the ${klass}`
+ `named ${jname}. Ignoring.`)
cdata.pivot = ''
cmdr.commands.push(`${cdata.pivot} = `
+ `Slider(0°,360°,1°,1,${cdata.width/3},true,true,false,false)`)
cmdr.callbacks.push (api: AppletObject) =>
api.setCaption cdata.pivot, 'Rotate Display'
api.setLabelStyle cdata.pivot, 3
api.setCoords(cdata.pivot, 2*cdata.width/3, cdata.height-10)
// Not sure how to let TypeScript deal with putting a new function
// on the global window object, so punting at least for now:
// @ts-ignore
window.pivotListener = pivotListener
api.registerObjectUpdateListener(cdata.pivot, 'pivotListener')
// Create callback to assign colors
traceC := adapParams.config?.color
console.log 'Considering coloring', name, 'with', colors if traceC
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) =>
// 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]) and (klass !== 'sphere' or invisible colors[2])
for each face of parts[2]
if face is name
console.log 'Fading out interior of', face if traceC
// hide the interior by making it transparent
api.setFilling face, 0
else if face not in cdata.elements
console.log 'Hiding face', face if traceC
api.setVisible face, false
else
surface .= colors[3]
if klass is 'sphere' and invisible surface
surface = colors[2] // for Joyce, spheres had one circular "edge"
faceRGB := joyce2rgb(surface or 'brighter', cdata.bg)
deep := ['circle', 'polygon', 'sector']
filling := deep.includes(klass) ? 0.7 : 0.2
for each face of parts[2]
if traceC
console.log 'Coloring face', face, 'to',
surface, '=', faceRGB
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]
if line is name or line not in cdata.elements
console.log 'Hiding line', line if traceC
api.setVisible line, false
else
lineRGB := joyce2rgb(colors[2] or 'black', cdata.bg)
if traceC
console.log 'Need to color lines', parts[1], 'with', lineRGB
for each line of parts[1]
console.log 'Coloring line', line, 'to', colors[2] if traceC
api.setVisible line, true
api.setColor line, ...lineRGB
// Now color the points:
if traceC
console.log
'Considering point colors for', name, 'of dimension', dimension
if invisible colors[1]
// Hide all the dim-0 elements that are not distinct independent
// items:
for each point of parts[0]
if point is name or point not in cdata.elements
console.log 'Hiding point', point if traceC
api.setVisible point, false
else if dimension is 0 or colors[1] // Need to color the points
if not colors[1]
colors[1] = pointDefaultColorName name, method, isPivot
ptRGB := joyce2rgb colors[1], cdata.bg
for each point of parts[0]
console.log 'Coloring point', point, 'to', colors[1] if traceC
api.setVisible point, true
api.setColor point, ...ptRGB
// Make the caption the correct color
if invisible colors[0]
console.log 'Hiding label', name if traceC
api.setLabelVisible name, false
else if colors[0]
if colors[dimension+1] and colors[dimension+1] 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.025/Length(Direction(${name})),`
+ `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`
textCmd := `${textName} = Text("${jname}", ${locationExpr})`
textCol := joyce2rgb colors[0], cdata.bg
if traceC
console.log `Making text '${textCmd}' colored`, textCol
api.evalCommand textCmd
api.setColor textName, ...textCol
// and hide the underlying GeoGebra label
api.setLabelVisible name, false
else // specified label color matches the entity color
// So label gets the correct color from element
// but we had better make sure it is visible:
console.log 'Showing label', name if traceC
api.setLabelVisible name, true
else // label color is defaulting
// Make it same as the element for points, invisible otherwise:
show := klass is 'point'
console.log 'Setting label vis of', name, 'to', show if traceC
api.setLabelVisible name, show
cdata.elements[jname] =
{otherName: name, usesCaptions, klass, cmdr.ends, cmdr.parts}
if name !== jname
cdata.elements[name] =
{otherName: jname, usesCaptions, klass, cmdr.ends, cmdr.parts}
cmdr
// Helper for dividing an angle
function makeAngDiv(
method:string,
@ -1303,3 +1385,93 @@ function makeLinesInvisible(callbacks: GeogebraCallback[], name: string)
// evaluating the modified XML created a sort of second
// copy of the entity, and so we have to hide the original one
api.setVisible name, false
// ------------------------------------------------------------------------
// JSXGraph translation below this line, up to the actual actions of script
// ------------------------------------------------------------------------
type AuxiliaryData
jsxElement?: JXG.GeometryElement
auxiliaries: JXG.GeometryElement[]
ends?: [DyName, DyName]
parts: [DyName[], DyName[], DyName[]]
function freshAuxEndsParts(): AuxiliaryData
auxiliaries: []
parts: [[], [], []]
// Execute the commands needed to create the element encoded by jCom
// against the given api entity
function jAsJSX(
jCom: string,
index: number,
api: JXG.Board | JXG.View3D,
cdata: ConstructionData)
elt := parseElement jCom, index, cdata
unless elt then return
{jsxElement, auxiliaries, ends, parts} :=
jsxHandler[elt.klass] elt, api, cdata
unless jsxElement then return
cdata.elements[elt.name] = {
otherName: elt.jname,
jsxElement, elt.usesCaptions, elt.klass, ends, parts}
if elt.name !== elt.jname
cdata.elements[elt.jname] =
Object.assign {}, cdata.elements[elt.name], {otherName: elt.name}
type JSXHandler = (
elt: Element,
api: JXG.Board | JXG.View3D,
cdata: ConstructionData) => AuxiliaryData
// Helper function to deal with TypeScript typing of JSX classes
type JSX_APIS
jsx2d?: JXG.Board
jsx3d?: JXG.View3D
function getJSXapi(
api: JXG.Board | JXG.View3D,
cdata: ConstructionData): JSX_APIS
if cdata.is3d then return {jsx3d: api as JXG.View3D}
else return {jsx2d: api as JXG.Board}
jsxHandler: Record<JoyceClass, JSXHandler> :=
point: (elt, api, cdata) =>
return .= freshAuxEndsParts()
{jsxElement, auxiliaries, ends, parts} .= return.value
parts[0].push elt.name
{jname, args} := elt
{jsx2d, jsx3d} := getJSXapi api, cdata
// FIXME: put Joyce hacks here
switch elt.method
'free'
unless args.scalar then return
coords := vertFlipped args.scalar, cdata
// FIXME: Is something needed here for supporting pivot?
if jsx3d
jsx3d.create 'point3d', coords, {name: elt.jname}
// FIXME: Handle 2d
return.value = {jsxElement, auxiliaries, ends, parts}
line: (elt, api, cdata) => freshAuxEndsParts()
polygon: (elt, api, cdata) => freshAuxEndsParts()
circle: (elt, api, cdata) => freshAuxEndsParts()
sector: (elt, api, cdata) => freshAuxEndsParts()
plane: (elt, api, cdata) => freshAuxEndsParts()
polyhedron: (elt, api, cdata) => freshAuxEndsParts()
sphere: (elt, api, cdata) => freshAuxEndsParts()
// ------------------------------------------------------------------------
// The actual actions of the script follow:
// ------------------------------------------------------------------------
// Always use the final joyceApplets if there are any:
if joyceApplets.length
adapParams.joyceApplets = joyceApplets
if adapParams.joyceApplets.length
if adapParams.loader
jQuery.getScript adapParams.loader, =>
postApplets adapParams.joyceApplets
else
postApplets adapParams.joyceApplets, adapParams.codebase

View file

@ -27,9 +27,9 @@ export function generateVRML(n: number, d: number, chk: boolean[])
unless 1 <= d < n
return err: 'Denominator should be strictly between 1 and numerator.'
if d > n/2 then d = n-d // equivalent
which := chk.findIndex (&)
which := chk.findIndex &
chk[which] = false
if chk.some (&) return err: 'Currently only one shape option may be checked.'
if chk.some & return err: 'Currently only one shape option may be checked.'
name .= d > 1 ? `${n}/${d}-gonal` :
switch n
when 3: 'triangular'