archematics/src/adapptlet.civet

1306 lines
54 KiB
Plaintext

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
joyceApplets: AppletDescription[] := []
$('applet[code="Geometry"]').before (i, html) ->
id := `joyceApplet${i}`
joyceApplets.push { html, params(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]
parts?: string[][]
// We put both JoyceNames and GeoNames in here, pointing to each other
// with the otherName property:
type JoyceElements = Record<AnyName, Description>
adapptScript := findAdappt() as HTMLScriptElement
function findAdappt()
scripts := document.querySelectorAll 'script'
for scrip of scripts
src := scrip.getAttribute 'src'
if src and src.includes 'adapptlet'
return scrip
adapParams: AdapParams :=
typeof GGBApplet is 'undefined'
? {loader: 'https://www.geogebra.org/apps/deployggb.js', joyceApplets: []}
: JSON.parse(adapptScript.dataset.params ?? '') as AdapParams
// For accessing/setting the y coordinate of a set of coordinates:
Y := 1
Z := 2
function vertFlipped(coords: number[], cdata: ConstructionData): XYZ
coords = coords.slice()
if cdata.is3d
if coords[Z] then coords[Z] = -coords[Z]
coords[Y] = cdata.height - coords[Y]
return coords as XYZ
type ConstructionData
id: string
bg: RGB
is3d: boolean
width: number
height: number
elements: JoyceElements
pivot: string
title?: string
// 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):
type PivotData
api: AppletObject
pivot: string
lastAngle: number
rotatable: string[]
maybeRotatable: Record<string, string>
fixed: Record<string, boolean>
pivotData: Record<string, PivotData> := {}
function postApplets(jApplets: AppletDescription[], codebase = '')
for each jApp of jApplets
params := {
appName: 'classic',
-enableRightClick,
// +showMenuBar,
jApp.width,
jApp.height,
appletOnLoad: (api: AppletObject) =>
is3d := contains3d jApp.params
if is3d
api.enable3D true
api.setPerspective 'T'
// Get rid of the xy-plane indicator
xml .= api.getXML()
xml = xml.replace /plate.show="\w+"/, 'plate show="false"'
api.setXML xml
else if codebase.includes 'web3d'
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}
for name, value in jApp.params
dispatchJcommand
api, name, value, cdata
if pivot
pd := pivotData[pivot]
for entity, circ in pd.maybeRotatable
if cdata.elements[circ].ends?[0] is pd.pivot
pd.rotatable.push entity
if is3d
depth .= jApp.width
if jApp.height > depth then depth = jApp.height
depth /= 8
api.setCoordSystem
-10 + jApp.width/6, 10 + 4*jApp.width/6,
-10 + jApp.height/6, 10 + 4*jApp.height/6,
-2*depth - 10, depth + 10,
true
api.setAxesVisible 3, false, false, false
api.setGridVisible 3, false
else
api.setCoordSystem -10, 10 + jApp.width, -10, 10 + jApp.height
api.setAxesVisible false, false
api.setGridVisible false
} as const
geoApp := new GGBApplet params
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
// 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,
method: string,
args: JoyceArguments,
index: number,
cdata: ConstructionData,
colors: string[],
jname: string) => 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,
name: string,
value: string,
cdata: ConstructionData): void
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()
'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))`
'pivot'
return // already handled in postApplets
'align'
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}'`
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]
newval := pd.api.getValue slider
if newval is pd.lastAngle then return
rotation := newval - pd.lastAngle
pd.lastAngle = newval
pX := pd.api.getXcoord pd.pivot
pY := pd.api.getYcoord pd.pivot
relX: Record<string, number> := {}
relY: Record<string, number> := {}
for each anchor of pd.rotatable
relX[anchor] = pd.api.getXcoord(anchor) - pX
relY[anchor] = pd.api.getYcoord(anchor) - pY
pd.api.setFixed anchor, false
ct := Math.cos rotation
st := Math.sin rotation
for each anchor of pd.rotatable
rX := relX[anchor]*ct - relY[anchor]*st
rY := relY[anchor]*ct + relX[anchor]*st
pd.api.setCoords(anchor, rX + pX, rY + pY)
if pd.fixed[anchor]
pd.api.setFixed anchor, true
function invisible(cname: string): boolean
if adapParams.config?.showall then return false
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(30).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.warn 'Could not parse color:', cname
[128, 128, 128]
function pointDefaultColorName(
name: string, method: string, isPivot: boolean): string
if isPivot then return 'green'
switch method
'free'
'red'
/.*[Ss]lider$/
'orange'
else 'black'
function geoname(
jname: JoyceName, elements: JoyceElements, klass: JoyceClass): GeoName
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
// If it's not a point, can start with any letter:
if klass !== 'point' and /^\p{L}['\w]*$/u.test jname then return jname
// GeoGebra won't deal with this name, so hash it:
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
// Helpers for some corresponding point/line functions:
function cutoffExtend(
method: string, pt: string[], point?: string[], line?: string[]
): [string, string]
direction .= `UnitVector(Vector(${pt[0]},${pt[1]}))`
if line and (not point or point[0] !== pt[0])
direction = `UnitVector(${line[0]})`
displacement := `Distance(${pt[2]}, ${pt[3]})*${direction}`
source := method is 'cutoff' ? pt[0] : pt[1]
[source, displacement]
function proportionSimilar(
method: string, args: JoyceArguments, cdata: ConstructionData,
aux: string, commands: string[], auxiliaries: string[]
): readonly [string, string]
bad := ['', ''] as const
pt .= args.subpoints
unless pt then return bad
// reduce the similar case to general proportion
if method is 'similar'
unless pt.length is 5 then return bad
sourcePlane .= ''
destPlane .= ''
if cdata.is3d
unless args.plane then return bad
destPlane = `, ${args.plane[0]}`
if args.plane.length > 1
sourcePlane = `, ${args.plane[1]}`
else
sourcePlane = `, Plane(${pt[2]}, ${pt[3]}, ${pt[4]})`
angle := `Angle(${pt[3]}, ${pt[2]}, ${pt[4]}${sourcePlane})`
commands.push
`${aux} = Rotate(${pt[1]}, ${angle}, ${pt[0]}${destPlane})`
auxiliaries.push aux
pt = [pt[2], pt[3], pt[2], pt[4], pt[0], pt[1], pt[0], aux]
len := `Distance(${pt[2]},${pt[3]})*Distance(${pt[4]},${pt[5]})`
+ `/ Distance(${pt[0]},${pt[1]})`
direction := `UnitVector(Vector(${pt[6]}, ${pt[7]}))`
return [pt[6], `${len}*${direction}`]
// 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 =>
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
zeroVector := cdata.is3d ? 'Vector((0,0,0))' : 'Vector((0,0))'
defaultPlane := cdata.is3d ? ', xOyPlane' : ''
aux := name + 'aUx'
pivotable := cdata.pivot and name !== pivotData[cdata.pivot].pivot
parts[0].push name
// HACK: Special-case corrections for Joyce Elements Bk XI -- XIII
if cdata.title is 'XI.4'
if name is 'Z' and method is 'fixed' and args.scalar?.length is 3
method = 'perpendicular'
args.subpoints = ['E','P2','A']
args.plane = ['baseplane']
colors[0] = '0'
colors[1] = '0'
if name is 'F' and method is 'lineSlider' and args.subpoints?[0] is 'E'
args.scalar = [160,40,60]
if cdata.title is 'XI.5'
if name is 'A' and method is 'free'
method = 'perpendicular'
args.subpoints = ['B','P1','P3']
args.plane = ['xOyPlane']
commands.push 'B=(80,140)'
if cdata.title is 'XII.7'
if (name is 'E' and method is 'lineSlider'
and args.subpoints?[1] is cdata.elements['1Y'].otherName)
args.subpoints[1] = cdata.elements['1Z'].otherName
if cdata.title is 'XII.8'
if (name is 'M' and method is 'lineSlider'
and args.subpoints?[1] is cdata.elements['1Y'].otherName)
args.subpoints[1] = cdata.elements['1Z'].otherName
if cdata.title is 'XII.9'
if (jname is 'a' and method is 'lineSlider'
and args.subpoints?[1] is cdata.elements['1Y'].otherName)
args.subpoints[1] = cdata.elements['1Z'].otherName
if cdata.title is 'XIII.17'
// Joyce's point perpendicular to a plane is ambiguous between
// two possible positions, and Geogebra's choice does not
// always agree with what Joyce's code did. So we just have to flip
// one of his choices in the dodecahedron
if name is 'U' and method is 'extend' and args.subpoints?[0] is "U'"
method = 'midpoint'
args.point = ["U'", "U'"] // hacky way to make U = U'
if cdata.title is 'Dodecahedron and cube'
// similar issues to XIII.17
if (name is 'VAB' or name is 'VBC') and method is 'perpendicular'
args.scalar = [1]
if cdata.title is 'XIII.18'
// Joyce just mistook where N is
if name is 'N' and method is 'intersection'
method = 'golden'
args.subpoints = ['B', 'F']
switch method
/angle(?:Bisector|Divider)/
{center, foot} :=
makeAngDiv(method, args, cdata, aux, auxiliaries, commands)
unless foot return
commands.push `${name} = ${foot}`
'center'
entity := args.circle ? args.circle[0] : args.sphere?[0]
if entity
commands.push `${name} = Center(${entity})`
entityEnds := cdata.elements[entity].ends
if entityEnds and not entityEnds[0]
entityEnds[0] = name // labeled the center
else
console.warn 'Nothing to produce center point of in',
name, method, args
'circleSlider'
unless args.circle then return
circ := args.circle[0]
commands.push `${name} = Point(${circ})`
if pivotable
maybeCenter := cdata.elements[circ].ends?[0]
if maybeCenter is pivotData[cdata.pivot].pivot
pivotData[cdata.pivot].rotatable.push name
if not maybeCenter
pivotData[cdata.pivot].maybeRotatable[name] = circ
if args.scalar and args.scalar.length
callbacks.push (api: AppletObject) =>
api.setCoords name, ...vertFlipped(args.scalar or [], cdata)
'circumcenter'
unless args.subpoints?.length is 3 then return
commands.push
`${aux} = Circle(${args.subpoints.join ','})`
`${name} = Center(${aux})`
auxiliaries.push aux
/cutoff|extend/
pt := args.subpoints
unless pt and pt.length is 4 then return
[source, displacement] :=
cutoffExtend method, pt, args.point, args.line
commands.push `${name} = Translate(${source}, ${displacement})`
'first'
unless args.subpoints then return
index .= 0
// HACK: Special-case correction for Joyce Elements Bk II, prop 14
if cdata.title is 'II.14' and name is 'H' and args.line?[0] is 'HH2'
index = 1
commands.push `${name} = ${args.subpoints[index]}`
/fixed|free/
unless args.scalar then return
coords := vertFlipped(args.scalar, cdata)
scoord := coords.join ','
if pivotable then pivotData[cdata.pivot].rotatable.push name
commands.push `${name} = (${scoord})`
if method is 'fixed'
if pivotable then pivotData[cdata.pivot].fixed[name] = true
callbacks.push (api: AppletObject) => api.setFixed name, true
'foot'
pt := args.subpoints
unless pt then return
destination := args.plane
? args.plane[0]
: `Line(${pt[1]},${pt[2]})`
commands.push
`${name} = ClosestPoint(${destination}, ${pt[0]})`
'golden'
// Added in this implementation to ease fix of XIII.18
pt := args.subpoints
unless pt and pt.length is 2 then return
commands.push
`${name} = ${pt[0]} + ((sqrt(5)-1)/2)*(${pt[1]}-${pt[0]})`
'intersection'
// Checking Joyce source, means intersection of lines, not
// intersection of line segments
pt := args.subpoints
unless pt and pt.length > 1 then return
l1 := `Line(${pt[0]},${pt[1]})`
e2 := pt.length < 3 ? args.plane?[0] : `Line(${pt[2]},${pt[3]})`
commands.push `${name} = Intersect(${l1},${e2})`
'last'
unless args.subpoints then return
commands.push
`${name} = ${args.subpoints.at(-1)}`
'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, ...vertFlipped(args.scalar or [], cdata)
'lineSlider'
pt := args.subpoints
unless pt and pt.length is 2 then return
commands.push `${name} = Point(Line(${pt[0]}, ${pt[1]}))`
if args.scalar and args.scalar.length
callbacks.push (api: AppletObject) =>
api.setCoords name, ...vertFlipped(args.scalar or [], cdata)
'meanProportional'
pt := args.subpoints
unless pt and pt.length is 6 then return
direction := `UnitVector(Vector(${pt[4]},${pt[5]}))`
lensq := `Distance(${pt[0]},${pt[1]})*Distance(${pt[2]},${pt[3]})`
disp := `sqrt(${lensq})*${direction}`
commands.push `${name} = Translate(${pt[4]},${disp})`
'midpoint'
if args.line
commands.push `${name} = Midpoint(${args.line[0]})`
else
commands.push
`${name} = Midpoint(${args.point?[0]},${args.point?[1]})`
'parallelogram'
pt := args.subpoints
unless pt then return
commands.push `${name} = ${pt[0]} + ${pt[2]} - ${pt[1]}`
'perpendicular'
pt := args.subpoints
unless pt return
inPlane := args.plane ? `,${args.plane[0]}` : defaultPlane
center := pt[0]
switch pt.length
when 2
commands.push
`${name} = Rotate(${pt[1]}, pi/2, ${center}${inPlane})`
when 3 // perpendicular **to** the plane
radius := `Distance(${pt[1]}, ${pt[2]})`
which := args.scalar ? args.scalar[0] : 2 //HACK: 2 vs 1??
commands.push
`${aux}1 = Sphere(${center}, ${radius})`
`${aux}2 = PerpendicularLine(${center}${inPlane})`
`${name} = Intersect(${aux}2,${aux}1,${which})`
auxiliaries.push aux+1, aux+2
when 4
commands.push
`${aux}1 = Ray(${center}, Rotate(${pt[1]}, pi/2, ${center}${inPlane}))`
`${aux}2 = Circle(${center}, Distance(${pt[2]},${pt[3]})${inPlane})`
`${name} = Intersect(${aux}1, ${aux}2)`
auxiliaries.push aux+1, aux+2
'planeSlider'
pln := args.plane?[0]
unless pln then return
commands.push `${name} = PointIn(${pln})`
if args.scalar and args.scalar.length
callbacks.push (api: AppletObject) =>
api.setCoords name, ...vertFlipped(args.scalar or [], cdata)
/proportion|similar/
[source, displacement] :=
proportionSimilar method, args, cdata, aux, commands, auxiliaries
unless source then return
commands.push `${name} = Translate(${source}, ${displacement})`
'sphereSlider'
sph := args.sphere?[0]
unless sph then return
commands.push `${name} = PointIn(${sph})`
if args.scalar and args.scalar.length
callbacks.push (api: AppletObject) =>
api.setCoords name, ...vertFlipped(args.scalar or [], cdata)
'vertex'
commands.push
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
else console.warn 'Unknown point method:', method
line: (name, method, args, index, cdata) =>
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
aux := name + 'aUx'
parts[1].push name
madeSegment .= false
switch method
/angle(?:Bisector|Divider)/
{center, foot} :=
makeAngDiv(method, args, cdata, aux, auxiliaries, commands)
unless foot return
auxiliaries.push aux + 'F'
commands.push
`${aux}F = ${foot}`
ends[0] = center
ends[1] = aux+'F'
'bichord'
// To match Joyce, we need to get the ordering here correct.
// we want the order so that start -> end sweeping past the
// center of the other circle is clockwise in the first
// circle
cr := args.circle
unless cr return
commands.push ...[1..2].map (n) =>
`${aux}${n} = Intersect(${cr[0]}, ${cr[1]}, ${n})`
inPlane := cdata.is3d ? `, Plane(${cr[0]})` : ''
ctr := cr.map (c) => `Center(${c})`
condition := (`Angle(${aux}1,${ctr[0]},${ctr[1]}${inPlane})`
+ `< Angle(${aux}2,${ctr[0]},${ctr[1]}${inPlane})`)
commands.push
`${aux}3 = If(${condition}, ${aux}1, ${aux}2)`
`${aux}4 = If(${condition}, ${aux}2, ${aux}1)`
ends[0] = aux + 3
ends[1] = aux + 4
auxiliaries.push aux+n for n of [1..4]
'chord'
// To match Joyce, we need to get the ordering here correct.
// The complicated condition about distances is modeled after
// Joyce's code, but it boils down to: take the endpoint of the
// chord closest to the first subpoint, unless that first
// subpoint is essentially at the midpoint of the chord, in
// which case start with the endpoint nearest the second subpoint.
unless args.subpoints and args.circle then return
// We intersect with the whole line, not just the segment:
line := `Line(${args.subpoints.join ','})`
pA := args.subpoints[0]
pB := args.subpoints[1]
commands.push ...[1..2].map (n) =>
`${aux}${n} = Intersect(${args.circle}, ${line}, ${n})`
s := `Distance(${aux}1,${aux}2)`
d := `Distance(${aux}1,${pA}) - Distance(${aux}2,${pA})`
condition := (`If(${s}/10^9 < abs(${d}), ${d} > 0,`
+ `Distance(${aux}2,${pB}) < Distance(${aux}1,${pB}))`)
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 aux+n for n of [1..4]
'connect'
unless args.subpoints and args.subpoints.length is 2 then return
ends[0] = args.subpoints[0]
ends[1] = args.subpoints[1]
/cutoff|extend/
pt := args.subpoints
unless pt and pt.length is 4 then return
[source, displacement] :=
cutoffExtend method, pt, args.point, args.line
ends[0] = source
commands.push `${aux} = Translate(${source}, ${displacement})`
auxiliaries.push aux
ends[1] = aux
'foot'
pt := args.subpoints
unless pt then return
destination := args.plane
? args.plane[0]
: `Line(${pt[1]},${pt[2]})`
ends[0] = pt[0]
commands.push
`${aux} = ClosestPoint(${destination}, ${pt[0]})`
auxiliaries.push aux
ends[1] = aux
'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)`
'perpendicular'
pt := args.subpoints
unless pt then return
inPlane .= ''
if cdata.is3d
unless args.plane?.length is 1 then return
inPlane = `,${args.plane[0]}`
switch pt.length
when 2
ends[0] = pt[0]
ends[1] = aux
auxiliaries.push aux
commands.push
`${aux} = Rotate(${pt[1]}, pi/2, ${pt[0]}${inPlane})`
when 3
radius := `Distance(${pt[1]}, ${pt[2]})`
commands.push
`${aux}1 = PerpendicularLine(${pt[1]}${inPlane})`
`${aux}2 = Intersect(${aux}1${inPlane})`
`${aux}3 = Circle(${aux}2, ${radius}${inPlane})`
`${aux}4 = PointIn(${aux}3)`
`${aux}5 = Plane(${aux}4, ${aux}1)`
`${aux}6 = Rotate(${aux}4, pi/2, ${aux}2, ${aux}5)`
ends[0] = aux + 2
ends[1] = aux + 6
auxiliaries.push aux+n for n of [1..6]
when 4
ends[0] = pt[0]
ends[1] = aux + 2
auxiliaries.push aux+1, aux+2
commands.push
`${aux}1 = Rotate(${pt[1]}, 3*pi/2, ${pt[0]}${inPlane})`
unitVec := `UnitVector(Vector(${pt[0]}, ${aux}1))`
dist := `Distance(${pt[2]}, ${pt[3]})`
commands.push
`${aux}2 = Translate(${pt[0]}, ${dist}*${unitVec})`
else return
/proportion|similar/
[source, displacement] :=
proportionSimilar method, args, cdata, aux, commands, auxiliaries
unless source then return
ends[0] = source
commands.push `${aux}1 = Translate(${source},${displacement})`
auxiliaries.push aux+1
ends[1] = aux+1
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, index, cdata) =>
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
aux := name + 'aUx'
parts[1].push name
circle .= ''
switch method
'circumcircle'
pt := args.subpoints
unless pt and pt.length is 3 then return
circle = `Circle(${pt.join ','})`
'intersection'
sph := args.sphere
unless sph and sph.length is 2 then return
circle = `IntersectConic(${sph[0]}, ${sph[1]})`
'radius'
pt := args.subpoints
unless pt and pt.length > 1 and pt.length < 4 then return
ends[0] = pt[0]
if pt.length is 2 and not cdata.is3d
circle = `Circle(${pt[0]}, ${pt[1]})`
else // 3d or three points
radIx := pt.length - 2
inPlane := (cdata.is3d
? args.plane ? `, ${args.plane[0]}` : ', xOyPlane'
: '')
radius := `Distance(${pt[radIx]}, ${pt[radIx+1]})`
circle = `Circle(${pt[0]}, ${radius}${inPlane})`
commands.push
`${aux} = ${circle}` // for the filling
`${name} = ${circle}` // for the perimeter
parts[2].push aux
makeLinesInvisible callbacks, aux
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
polygon: (name, method, args, index, cdata) =>
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name
aux := geoname name + 'aUx', cdata.elements, 'point'
switch method
/equilateralTriangle|square|regularPolygon/
pt := args.subpoints
unless pt then return
N .= 3
if method is 'square' then N = 4
else if method is 'regularPolygon' and args.scalar
N = args.scalar[0]
commands.push '' // hack, make sure there is a command
parts[0].push pt[0], pt[1]
inPlane := cdata.is3d
? args.plane ? `, ${args.plane[0]}` : ', xOyPlane'
: ''
callbacks.push (api: AppletObject, moreParts: DimParts) =>
command := `${name} = Polygon(${pt[0]},${pt[1]}, ${N}${inPlane})`
made:= api.evalCommandGetLabels command
if adapParams.config?.commands
console.log 'Finishing with', command, 'producing', made
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'
moreParts[1].push newObj
'point'
moreParts[0].push newObj
api.setVisible newObj, false
'face'
unless args.polyhedron?.length is 1 then return
unless args.scalar?.length is 1 then return
element := cdata.elements[args.polyhedron[0]]
commands.push `${name} = ${element.parts?[2][args.scalar[0]-1]}`
///triangle|similar|parallelogram|application|quadrilateral
|octagon|pentagon|hexagon///
unless args.subpoints then return
pt .= args.subpoints
if method is 'parallelogram'
unless pt.length is 3 then return
commands.push `${aux} = ${pt[0]} + ${pt[2]} - ${pt[1]}`
auxiliaries.push aux
pt = [...pt, aux]
else if method is 'application'
unless pt.length is 3 then return
unless args.polygon?.length is 1 then return
direction := `UnitVector(${pt[2]} - ${pt[0]})`
angle := `Angle(${pt[1]},${pt[0]},${pt[2]})`
length := `Area(${args.polygon})`
+ `/(Distance(${pt[0]},${pt[1]})*abs(sin(${angle})))`
commands.push ...[0..1].map (n) =>
`${aux}${n} = ${pt[n]} + ${length}*${direction}`
auxiliaries.push aux+0, aux+1
pt = [pt[0], pt[1], aux+1, aux+0]
else if method is 'similar'
unless pt.length is 5 then return
if cdata.is3d and not args.plane then return
inSourcePlane := cdata.is3d
? `, Plane(${pt[2]},${pt[3]},${pt[4]})`
: ''
inDestPlane := cdata.is3d ? (', ' + args.plane?[0]) : ''
factor :=
`Distance(${pt[2]},${pt[4]})/Distance(${pt[2]},${pt[3]})`
commands.push
`${aux}1 = Angle(${pt[3]},${pt[2]},${pt[4]}${inSourcePlane})`
`${aux}2 = Rotate(${pt[1]},${aux}1,${pt[0]}${inDestPlane})`
`${aux}3 = ${pt[0]} + (${aux}2 - ${pt[0]})*${factor}`
auxiliaries.push aux+n for n of [1..3]
pt = [pt[0], pt[1], aux+3]
else
commands.push ''
parts[0].push ...pt
callbacks.push (api: AppletObject, moreParts: DimParts) =>
command := `Polygon(${pt.join ','})`
made := api.evalCommandGetLabels `${name} = ${command}`
if adapParams.config?.commands
console.log 'Finished constructing', name, 'with', command,
'producing', made
if not made return
for each obj of made.split ','
if obj is name continue
newObj := 'GeoAux' + index + obj
api.renameObject obj, newObj
moreParts[1].push newObj
sector: (name, method, args, index, cdata) =>
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
aux := name + 'aUx'
parts[2].push name
defaultPlane := cdata.is3d ? ', xOyPlane' : ''
inPlane .= args.plane ? `, ${args.plane[0]}` : defaultPlane
switch method
/arc|sector/
unless args.subpoints?.length is 3 return
parts[0].push ...args.subpoints
[center, start, end] .= args.subpoints
parms .= center + ', ' + start + ', ' + end
prefix .= 'Circular'
if method is 'arc'
temp := start
start = center
center = temp
parms = start + ', ' + center + ', ' + end
prefix = 'Circumcircular'
inPlane = '' // not needed in 3-point case
ends[0] = start
ends[1] = end
commands.push
`${name} = ${prefix}Sector(${parms}${inPlane})`
`${aux}1 = ${prefix}Arc(${parms}${inPlane})`
parts[1].push aux + 1
auxiliaries.push aux + 1
makeLinesInvisible callbacks, name
plane: (name, method, args, index, cdata) =>
return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name
switch method
'3points'
unless args.subpoints?.length is 3 then return
commands.push `${name} = Plane(${args.subpoints.join ','})`
'parallel'
unless args.subpoints?.length is 1 then return
unless args.plane?.length is 1 then return
commands.push `${name} = Plane(${args.subpoints[0]}, ${args.plane[0]})`
'perpendicular'
unless args.subpoints?.length is 2 then return
[thru, perp] := args.subpoints
commands.push
`${name} = PerpendicularPlane(${thru}, Line(${thru}, ${perp}))`
sphere: (name, method, args) =>
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
parts[2].push name
pt := args.subpoints
if method is 'radius' and pt
switch pt.length
when 2
[center, point] := pt
ends[0] = center
commands.push `${name} = Sphere(${center}, ${point})`
when 3
center := pt[0]
ends[0] = center
radius := `Distance(${pt[1]}, ${pt[2]})`
commands.push `${name} = Sphere(${center}, ${radius})`
polyhedron: (name, method, args, index, cdata) =>
return := freshCommander()
return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
aux := geoname name + 'aUx', cdata.elements, 'point'
switch method
'parallelepiped'
pt .= args.subpoints
unless pt and pt.length is 4 then return
// create all of the vertices we will need:
commands.push
`${aux}4 = ${pt[2]} + ${pt[1]} - ${pt[0]}`
`${aux}5 = ${pt[3]} + ${pt[1]} - ${pt[0]}`
`${aux}6 = ${pt[3]} + ${pt[2]} - ${pt[0]}`
`${aux}7 = ${pt[3]} + ${pt[2]} + ${pt[1]} - 2*${pt[0]}`
auxiliaries.push aux+i for i of [4..7]
pt = [...pt, ...auxiliaries]
parts[0].push ...pt
generalRecipe :=
A: [0,1,4,2]
B: [0,1,5,3]
C: [0,2,6,3]
D: [3,5,7,6]
E: [2,4,7,6]
F: [1,4,7,5]
letters := ['A'..'F'] as const
recipe: Record<string, string[]> := {}
for ltr of letters
auxlet := aux + ltr
auxiliaries.push auxlet
parts[2].push auxlet
recipe[auxlet] = (pt[i] for each i of generalRecipe[ltr])
ends[0] = aux + 'A'
ends[1] = aux + 'D'
callbacks.push (api: AppletObject, moreParts: DimParts) =>
ix .= 0
for piece in recipe
madeIt := api.evalCommandGetLabels
`${piece} = Polygon(${recipe[piece].join ','})`
if not madeIt return
for each obj of madeIt.split ','
if obj is piece continue
newObj := 'GeoAux' + index + obj + ix
api.renameObject obj, newObj
moreParts[1].push newObj
ix += 1
'prism'
unless args.polygon?.length is 1 then return
base := args.polygon[0]
ends[0] = base
pt := args.subpoints
unless pt and pt.length is 2 then return
commands.push
`${aux}1 = Vertex(${base},1) + ${pt[1]} - ${pt[0]}`
auxiliaries.push aux+1
ends[1] = aux+1
parts[0].push aux+1
parts[2].push ends[0]
callbacks.push (api: AppletObject, moreParts: DimParts) =>
made := api.evalCommandGetLabels
`${name} = Prism(${base}, ${aux}1)`
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
'point'
moreParts[0].push newObj
'segment'
moreParts[1].push newObj
else
moreParts[2].splice 1,0,newObj
/pyramid|tetrahedron/
base .= args.polygon?[0]
ends[0] = base or aux + 1
pt := args.subpoints
unless pt and pt.length > 0 then return
// A tetrahedron is just a pyramid where we have to build the
// base from three points ourselves. But it has to be done in the
// callback, since we have to capture the edges.
if method is 'tetrahedron' and pt.length !== 4 then return
commands.push '' // hack, make sure there is a command
parts[0].push ...pt
parts[2].push ends[0]
ends[1] = pt.at(-1) or ''
callbacks.push (api: AppletObject, moreParts: DimParts) =>
if not base
madeBase := api.evalCommandGetLabels
`${ends[0]} = Polygon(${pt[0]},${pt[1]},${pt[2]})`
if not madeBase return
for each obj of madeBase.split ','
if obj is ends[0] continue
newObj := 'GeoAux' + index + obj
api.renameObject obj, newObj
moreParts[1].push newObj
base = ends[0]
else // need to grab the parts of the base, which become
// parts of the polyhedron
baseParts := cdata.elements[base].parts
if baseParts
parts[0].push ...baseParts[0]
parts[1].push ...baseParts[1]
made := api.evalCommandGetLabels
`${name} = Pyramid(${base}, ${ends[1]})`
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'
moreParts[1].push newObj
'triangle'
moreParts[2].push newObj
// Helper for dividing an angle
function makeAngDiv(
method:string,
args: JoyceArguments,
cdata: ConstructionData,
aux: string,
auxiliaries: string[],
commands: string[])
// Note we just ignore a possible plane argument; it's irrelevant
unless args.subpoints return center: '', foot: ''
[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 := method is 'angleBisector' ? 2 : args.scalar?[0]
inPlane := cdata.is3d ? `, Plane(${start}, ${center}, ${end})` : ''
commands.push
`${aux}2 = Angle(${start}, ${center}, ${end}${inPlane})`
`${aux}3 = If(${aux}2 > pi, ${aux}2 - 2*pi, ${aux}2)`
`${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center}${inPlane})`
auxiliaries.push aux+i for i of [2..4]
return {center, foot: `Intersect(${destination}, Ray(${center}, ${aux}4))`}
// helper for separating color of perimeter and interior:
function makeLinesInvisible(callbacks: GeogebraCallback[], name: string)
callbacks.push (api: AppletObject) =>
api.setLineThickness name, 1
// The rest of this function is a weird roundabout way to make
// the lines of the sector have zero opacity. I got it from
// https://www.reddit.com/r/geogebra/comments/12cbr85/setlineopacity_command/
// I don't really understand how/why it works, but it seems to
// So that's good enough for me
xml .= api.getXML name
xml = xml.replace /opacity="\d+"/, 'opacity="0"'
api.evalXML xml
// This last step is especially confusing... I think
// 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