feat: Get all of Rostamian's old pages working (#44)

Implements the pivot parameter to the Geometry applet and numerous new construction methods.
Resolves #36.

Reviewed-on: #44
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
Glen Whitney 2023-10-18 01:07:58 +00:00 committed by Glen Whitney
parent a236e15990
commit 277d9b0a8c
5 changed files with 318 additions and 110 deletions

View File

@ -101,7 +101,7 @@ export interface AppletObject {
getXcoord(objName: string): number; getXcoord(objName: string): number;
getYcoord(objName: string): number; getYcoord(objName: string): number;
getZcoord(objName: string): number; getZcoord(objName: string): number;
setCoords(objName: string, x: number, y: number, z: number): void; setCoords(objName: string, x: number, y: number, z?: number): void;
getValue(objName: string): number; getValue(objName: string): number;
getVersion(): string; getVersion(): string;
getScreenshotBase64(callback: (data: string) => void, scale?: number): void; getScreenshotBase64(callback: (data: string) => void, scale?: number): void;

View File

@ -18,8 +18,12 @@
Alter execution of the translated applet: <br/> Alter execution of the translated applet: <br/>
<label for="showall">Show all applet entities, even hidden ones</label> <label for="showall">Show all applet entities, even hidden ones</label>
<input type="checkbox" id="showall"><br/> <input type="checkbox" id="showall"><br/>
<label for="showaux">Show geogebra auxiliaries (not in applet)</label> <label for="showaux">Show GeoGebra auxiliaries (not in applet)</label>
<input type="checkbox" id="showaux"> <input type="checkbox" id="showaux">
<br/>
<br/>
<label for="algebra">Show the GeoGebra algebra pane</label>
<input type="checkbox" id="algebra">
<script src="options.js" type="module"></script> <script src="options.js" type="module"></script>
</body> </body>

View File

@ -64,7 +64,7 @@ document.addEventListener "DOMContentLoaded", async =>
use3d = true use3d = true
break break
codebase .= browser.runtime.getURL 'deps/GeoGebra/HTML5/5.0/' codebase .= browser.runtime.getURL 'deps/GeoGebra/HTML5/5.0/'
codebase += use3d ? 'web3d' : 'webSimple' codebase += (use3d or config.algebra) ? 'web3d' : 'webSimple'
adapParams: AdapParams := {codebase, config, joyceApplets } adapParams: AdapParams := {codebase, config, joyceApplets }
apars := JSON.stringify(adapParams) apars := JSON.stringify(adapParams)
addScriptTag(browser.runtime.getURL 'deps/GeoGebra/deployggb.js').then => addScriptTag(browser.runtime.getURL 'deps/GeoGebra/deployggb.js').then =>

View File

@ -35,6 +35,42 @@ type Description
// with the otherName property: // with the otherName property:
type JoyceElements = Record<AnyName, Description> 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
type ConstructionData
id: string
bg: RGB
is3d: boolean
width: number
height: number
elements: JoyceElements
pivot: 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[]
fixed: Record<string, boolean>
pivotData: Record<string, PivotData> := {}
function postApplets(jApplets: AppletDescription[], codebase = '') function postApplets(jApplets: AppletDescription[], codebase = '')
for each jApp of jApplets for each jApp of jApplets
params := { params := {
@ -44,11 +80,9 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
jApp.width, jApp.width,
jApp.height, jApp.height,
appletOnLoad: (api: AppletObject) => appletOnLoad: (api: AppletObject) =>
elements: JoyceElements := {} is3d := contains3d jApp.params
backgroundRGB := [255, 255, 255] as RGB if is3d
config3d := contains3d jApp.params api.enable3D true
if config3d
worked := api.enable3D true
api.setPerspective 'T' api.setPerspective 'T'
// Get rid of the xy-plane indicator // Get rid of the xy-plane indicator
xml .= api.getXML() xml .= api.getXML()
@ -56,10 +90,26 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
api.setXML xml api.setXML xml
else if codebase.includes 'web3d' else if codebase.includes 'web3d'
api.setPerspective 'G' 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: [], fixed: {}}
cdata: ConstructionData := {
bg: ([255, 255, 255] as RGB),
is3d, jApp.id, jApp.width, jApp.height, elements, pivot}
for name, value in jApp.params for name, value in jApp.params
dispatchJcommand dispatchJcommand
api, name, value, elements, backgroundRGB, config3d api, name, value, cdata
if config3d if is3d
depth .= jApp.width depth .= jApp.width
if jApp.height > depth then depth = jApp.height if jApp.height > depth then depth = jApp.height
api.setCoordSystem api.setCoordSystem
@ -78,22 +128,6 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
if codebase then geoApp.setHTML5Codebase codebase if codebase then geoApp.setHTML5Codebase codebase
geoApp.inject jApp.id geoApp.inject jApp.id
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
console.log 'In script', document.currentScript, adapptScript
adapParams: AdapParams :=
typeof GGBApplet is 'undefined'
? {loader: 'https://www.geogebra.org/apps/deployggb.js', joyceApplets: []}
: JSON.parse(adapptScript.dataset.params ?? '') as AdapParams
// Always use the final joyceApplets if there are any: // Always use the final joyceApplets if there are any:
if joyceApplets.length if joyceApplets.length
adapParams.joyceApplets = joyceApplets adapParams.joyceApplets = joyceApplets
@ -135,7 +169,7 @@ type ClassHandler = (
method: string, method: string,
args: JoyceArguments, args: JoyceArguments,
index: number, index: number,
is3d: boolean) => Commander cdata: ConstructionData) => Commander
type RGB = [number, number, number] type RGB = [number, number, number]
type XYZ = RGB type XYZ = RGB
@ -146,28 +180,26 @@ function dispatchJcommand(
api: AppletObject, api: AppletObject,
name: string, name: string,
value: string, value: string,
elements: JoyceElements cdata: ConstructionData): void
backgroundRGB: RGB,
is3d: boolean): void
switch name switch name
'background' 'background'
newback := joyce2rgb value, backgroundRGB cdata.bg = joyce2rgb value, cdata.bg
if adapParams.config?.commands if adapParams.config?.commands
console.log 'Setting background to', value, 'interpreted as', console.log 'Setting background to', value, 'interpreted as',
newback cdata.bg
for i of [0..2] api.setGraphicsOptions 1, bgColor: colorsea(cdata.bg).hex()
backgroundRGB[i] = newback[i]
api.setGraphicsOptions 1, bgColor: colorsea(backgroundRGB).hex()
'title' 'title'
if adapParams.config?.commands if adapParams.config?.commands
console.log 'Setting title to', value console.log 'Setting title to', value
corner := is3d ? 'Corner(-1,1)' : 'Corner(1,1)' corner := cdata.is3d ? 'Corner(-1,1)' : 'Corner(1,1)'
api.evalCommand `TitlePoint = ${corner} api.evalCommand `TitlePoint = ${corner}
Text("${value}", TitlePoint + (2,5))` Text("${value}", TitlePoint + (2,5))`
'pivot'
return // already handled in postApplets
/e\[\d+\]/ /e\[\d+\]/
num := parseInt(name.slice(2)) num := parseInt(name.slice(2))
{commands, callbacks, parts} := {commands, callbacks, parts} :=
jToG value, elements, num, backgroundRGB, is3d jToG value, num, cdata
if commands.length if commands.length
lastTried .= 0 lastTried .= 0
if commands.filter((&)).every (cmd) => if commands.filter((&)).every (cmd) =>
@ -180,16 +212,14 @@ function dispatchJcommand(
(part of translation of '${value}') (part of translation of '${value}')
failed.` failed.`
else console.warn `Could not parse command '${value}'` else console.warn `Could not parse command '${value}'`
else console.warn `Unkown param ${name} = ${value}` else console.warn `Unknown param ${name} = ${value}`
// Parses a Joyce element-creating command, extending the elements // Parses a Joyce element-creating command, extending the elements
// by side effect: // by side effect:
function jToG( function jToG(
jCom: string, jCom: string,
elements: JoyceElements,
index: number, index: number,
backgroundRGB: RGB cdata: ConstructionData): Commander
is3d: boolean): Commander
[jname, klass, method, data, ...colors] := jCom.split ';' [jname, klass, method, data, ...colors] := jCom.split ';'
if adapParams.config?.commands if adapParams.config?.commands
console.log 'Defining', jname, 'as a', klass, 'constructed by', console.log 'Defining', jname, 'as a', klass, 'constructed by',
@ -199,7 +229,9 @@ function jToG(
console.warn `Unknown entity class ${klass}` console.warn `Unknown entity class ${klass}`
return cmdr return cmdr
assertJoyceClass klass // shouldn't need to do that :-/ assertJoyceClass klass // shouldn't need to do that :-/
name := if /^\p{L}\w*$/u.test jname then jname else geoname jname, elements 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 := {} args: JoyceArguments := {}
usesCaptions := [] usesCaptions := []
for each jdep of data.split ',' for each jdep of data.split ','
@ -207,17 +239,17 @@ function jToG(
if scalar is scalar // not NaN if scalar is scalar // not NaN
(args.scalar ?= []).push scalar (args.scalar ?= []).push scalar
continue continue
unless jdep in elements unless jdep in cdata.elements
console.warn `Reference to unknown geometric entity ${jdep} in $jCom}` console.warn `Reference to unknown geometric entity ${jdep} in $jCom}`
return cmdr return cmdr
usesCaptions.push jdep usesCaptions.push jdep
{klass: depKlass, otherName: depGeo, ends} := elements[jdep] {klass: depKlass, otherName: depGeo, ends} := cdata.elements[jdep]
(args[depKlass] ?= []).push depGeo (args[depKlass] ?= []).push depGeo
if depKlass is 'point' if depKlass is 'point'
(args.subpoints ?= []).push depGeo (args.subpoints ?= []).push depGeo
else if depKlass is 'line' else if depKlass is 'line'
(args.subpoints ?= []).push ...ends ?? [] (args.subpoints ?= []).push ...ends ?? []
cmdr = classHandler[klass] name, method, args, index, is3d cmdr = classHandler[klass] name, method, args, index, cdata
unless name is jname then cmdr.callbacks.push (api: AppletObject) => unless name is jname then cmdr.callbacks.push (api: AppletObject) =>
api.setCaption name, jname api.setCaption name, jname
api.setLabelStyle name, 3 // style CAPTION = 3 api.setLabelStyle name, 3 // style CAPTION = 3
@ -225,7 +257,27 @@ function jToG(
cmdr.callbacks.push (api: AppletObject) => cmdr.callbacks.push (api: AppletObject) =>
for each aux of cmdr.auxiliaries for each aux of cmdr.auxiliaries
api.setAuxiliary aux, true api.setAuxiliary aux, true
api.setLabelVisible aux, false
api.setVisible 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 // Create callback to assign colors
if colors.length is 4 and colors.every (color) => invisible color if colors.length is 4 and colors.every (color) => invisible color
cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false
@ -243,11 +295,11 @@ function jToG(
console.log 'Fading out interior of', face if trace console.log 'Fading out interior of', face if trace
// hide the interior by making it transparent // hide the interior by making it transparent
api.setFilling face, 0 api.setFilling face, 0
else if face not in elements else if face not in cdata.elements
console.log 'Hiding face', face if trace console.log 'Hiding face', face if trace
api.setVisible face, false api.setVisible face, false
else else
faceRGB := joyce2rgb(colors[3] or 'brighter', backgroundRGB) faceRGB := joyce2rgb(colors[3] or 'brighter', cdata.bg)
deep := ['circle', 'polygon', 'sector'] deep := ['circle', 'polygon', 'sector']
filling := deep.includes(klass) ? 0.7 : 0.2 filling := deep.includes(klass) ? 0.7 : 0.2
for each face of parts[2] for each face of parts[2]
@ -259,28 +311,30 @@ function jToG(
// Lines default to black: // Lines default to black:
if invisible colors[2] if invisible colors[2]
for each line of parts[1] for each line of parts[1]
if line is name or line not in elements if line is name or line not in cdata.elements
console.log 'Hiding line', line if trace console.log 'Hiding line', line if trace
api.setVisible line, false api.setVisible line, false
else else
lineRGB := joyce2rgb(colors[2] or 'black', backgroundRGB) lineRGB := joyce2rgb(colors[2] or 'black', cdata.bg)
for each line of parts[1] for each line of parts[1]
console.log 'Coloring line', line, 'to', colors[2] if trace console.log 'Coloring line', line, 'to', colors[2] if trace
api.setVisible line, true api.setVisible line, true
api.setColor line, ...lineRGB api.setColor line, ...lineRGB
// Now color the points: // Now color the points:
console.log 'Considering point colors for', name if trace if trace
console.log
'Considering point colors for', name, 'of dimension', dimension
if invisible colors[1] if invisible colors[1]
// Hide all the dim-0 elements that are not distinct independent // Hide all the dim-0 elements that are not distinct independent
// items: // items:
for each point of parts[0] for each point of parts[0]
if point is name or point not in elements if point is name or point not in cdata.elements
console.log 'Hiding point', point if trace console.log 'Hiding point', point if trace
api.setVisible point, false api.setVisible point, false
else if dimension is 0 or colors[1] // Need to color the points else if dimension is 0 or colors[1] // Need to color the points
ptRGB := colors[1] ? joyce2rgb colors[1], backgroundRGB ptRGB := colors[1] ? joyce2rgb colors[1], cdata.bg
: pointDefaultRGB name, method : pointDefaultRGB name, method, isPivot
for each point of parts[0] for each point of parts[0]
console.log 'Coloring point', point, 'to', colors[1] if trace console.log 'Coloring point', point, 'to', colors[1] if trace
api.setVisible point, true api.setVisible point, true
@ -290,12 +344,12 @@ function jToG(
if invisible colors[0] if invisible colors[0]
console.log 'Hiding label', name if trace console.log 'Hiding label', name if trace
api.setLabelVisible name, false api.setLabelVisible name, false
else if colors[dimension] and colors[dimension] is not colors[0] else if colors[dimension+1] and colors[dimension+1] is not colors[0]
// Have to make a text to provide the caption, since GeoGebra // Have to make a text to provide the caption, since GeoGebra
// doesn't allow caption different color from entity. // doesn't allow caption different color from entity.
textName := 'GeoText' + index textName := 'GeoText' + index
locationExpr := switch klass locationExpr := switch klass
when 'point' then `1.02${name})` when 'point' then `1.02${name}`
when 'line' when 'line'
(`Midpoint(${name}) + ` (`Midpoint(${name}) + `
+ `Rotate(Direction(${name})*Length(${name})*0.02, pi/2)`) + `Rotate(Direction(${name})*Length(${name})*0.02, pi/2)`)
@ -315,8 +369,12 @@ function jToG(
unless parts[0].includes ex1 then ex1 = `Centroid(${ex1})` unless parts[0].includes ex1 then ex1 = `Centroid(${ex1})`
unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})` unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})`
`(4*${ex1}+${ex2})/5` `(4*${ex1}+${ex2})/5`
api.evalCommand `${textName} = Text("${jname}", ${locationExpr})` textCmd := `${textName} = Text("${jname}", ${locationExpr})`
api.setColor textName, ...joyce2rgb colors[0], backgroundRGB textCol := joyce2rgb colors[0], cdata.bg
if trace
console.log `Making text '${textCmd}' colored`, textCol
api.evalCommand textCmd
api.setColor textName, ...textCol
// and hide the underlying GeoGebra label // and hide the underlying GeoGebra label
api.setLabelVisible name, false api.setLabelVisible name, false
else if colors[0] else if colors[0]
@ -331,19 +389,33 @@ function jToG(
console.log 'Setting label vis of', name, 'to', show if trace console.log 'Setting label vis of', name, 'to', show if trace
api.setLabelVisible name, show api.setLabelVisible name, show
// window[hideListener] = (arg) => cdata.elements[jname] = {otherName: name, usesCaptions, klass, cmdr.ends}
// api.setVisible name, false cdata.elements[name] = {otherName: jname, usesCaptions, klass, cmdr.ends}
// 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 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 function invisible(cname: string): boolean
if adapParams.config?.showall then return false if adapParams.config?.showall then return false
cname is '0' or cname is 'none' cname is '0' or cname is 'none'
@ -397,8 +469,8 @@ function joyce2rgb(cname: string, backgroundRGB?: RGB): RGB
console.warn 'Could not parse color:', cname console.warn 'Could not parse color:', cname
[128, 128, 128] [128, 128, 128]
function pointDefaultRGB(name: string, method: string): RGB function pointDefaultRGB(name: string, method: string, isPivot: boolean): RGB
// Need to short-circuit with green for pivot point, once that is implemented if isPivot then return joyce2rgb 'green'
switch method switch method
'free' 'free'
joyce2rgb 'red' joyce2rgb 'red'
@ -406,19 +478,39 @@ function pointDefaultRGB(name: string, method: string): RGB
joyce2rgb 'orange' joyce2rgb 'orange'
else joyce2rgb 'black' else joyce2rgb 'black'
function geoname(jname: JoyceName, elements: JoyceElements): GeoName function geoname(
jname: JoyceName, elements: JoyceElements, klass: JoyceClass): GeoName
unless 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 .= 0n
numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname
return .= 'Geo' + numCode.toString(36); return .= 'Geo' + numCode.toString(36);
return += '1' while return.value in elements return += '1' while return.value in elements
// Helper for similar 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]
// All of the detailed semantics of each available command lies in this // All of the detailed semantics of each available command lies in this
// function. // function.
classHandler: Record<JoyceClass, ClassHandler> := classHandler: Record<JoyceClass, ClassHandler> :=
point: (name, method, args, index, is3d): Commander => point: (name, method, args, index, cdata): Commander =>
return := freshCommander() return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value {commands, callbacks, parts, auxiliaries} := return.value
zeroVector := cdata.is3d ? 'Vector((0,0,0))' : 'Vector((0,0))'
aux := name + 'aUx' aux := name + 'aUx'
pivotable := cdata.pivot and name !== pivotData[cdata.pivot].pivot
parts[0].push name parts[0].push name
switch method switch method
/angle(?:Bisector|Divider)/ /angle(?:Bisector|Divider)/
@ -433,30 +525,41 @@ classHandler: Record<JoyceClass, ClassHandler> :=
commands.push `${destination} = Segment(${start}, ${end})` commands.push `${destination} = Segment(${start}, ${end})`
else destination = args.line[0] else destination = args.line[0]
n := method is 'angleBisector' ? 2 : args.scalar?[0] n := method is 'angleBisector' ? 2 : args.scalar?[0]
inPlane := is3d ? `, Plane(${start}, ${center}, ${end})` : '' inPlane := cdata.is3d ? `, Plane(${start}, ${center}, ${end})` : ''
commands.push commands.push
`${aux}2 = Angle(${start}, ${center}, ${end}${inPlane})` `${aux}2 = Angle(${start}, ${center}, ${end}${inPlane})`
`${aux}3 = If(${aux}2 > pi, ${aux}2 - 2*pi, ${aux}2)` `${aux}3 = If(${aux}2 > pi, ${aux}2 - 2*pi, ${aux}2)`
`${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center}${inPlane})` `${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center}${inPlane})`
`${name} = Intersect(${destination}, Ray(${center}, ${aux}4))` `${name} = Intersect(${destination}, Ray(${center}, ${aux}4))`
auxiliaries.push ...[2..4].map (i) => `${aux}${i}` auxiliaries.push ...[2..4].map (i) => `${aux}${i}`
'extend' 'circleSlider'
sp := args.subpoints unless args.circle then return
unless sp then return circ := args.circle[0]
direction .= `UnitVector(Vector(${sp[0]},${sp[1]}))` commands.push `${name} = Point(${circ})`
if args.line and ( if (pivotable
not args.point or args.point[0] !== sp[0]) and cdata.elements[circ].ends?[0] // center
direction = `UnitVector(${args.line[0]})` is pivotData[cdata.pivot].pivot)
displacement := `Distance(${sp[2]}, ${sp[3]})*${direction}` pivotData[cdata.pivot].rotatable.push name
commands.push `${name} = Translate(${sp[1]}, ${displacement})` if args.scalar and args.scalar.length
callbacks.push (api: AppletObject) =>
api.setCoords name, ...args.scalar as XYZ
/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' 'first'
unless args.subpoints then return unless args.subpoints then return
commands.push `${name} = ${args.subpoints[0]}` commands.push `${name} = ${args.subpoints[0]}`
/fixed|free/ /fixed|free/
coords := args.scalar coords := args.scalar
unless coords then return unless coords then return
commands.push `${name} = (${coords.join ','})` scoord := coords.join ','
if pivotable then pivotData[cdata.pivot].rotatable.push name
commands.push `${name} = (${scoord})`
if method is 'fixed' if method is 'fixed'
if pivotable then pivotData[cdata.pivot].fixed[name] = true
callbacks.push (api: AppletObject) => api.setFixed name, true callbacks.push (api: AppletObject) => api.setFixed name, true
'foot' 'foot'
pt := args.subpoints pt := args.subpoints
@ -476,7 +579,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
commands.push `${name} = Intersect(${l1},${e2})` commands.push `${name} = Intersect(${l1},${e2})`
'last' 'last'
unless args.subpoints then return unless args.subpoints then return
commands.push `${name} = ${args.subpoints.at(-1)}` commands.push
`${name} = ${args.subpoints.at(-1)}`
'lineSegmentSlider' 'lineSegmentSlider'
segment .= args.line?[0] segment .= args.line?[0]
unless segment unless segment
@ -500,9 +604,26 @@ classHandler: Record<JoyceClass, ClassHandler> :=
[center, direction] := args.subpoints [center, direction] := args.subpoints
// Note clockwise 90° rotation (3π/2) confirmed in Joyce source // Note clockwise 90° rotation (3π/2) confirmed in Joyce source
commands.push `${name} = Rotate(${direction}, 3*pi/2, ${center})` commands.push `${name} = Rotate(${direction}, 3*pi/2, ${center})`
'proportion' /proportion|similar/
pt := args.subpoints pt .= args.subpoints
unless pt then return unless pt then return
// reduce the similar case to general proportion
if method is 'similar'
unless pt.length is 5 then return
sourcePlane .= ''
destPlane .= ''
if cdata.is3d
unless args.plane then return
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]})` len := `Distance(${pt[2]},${pt[3]})*Distance(${pt[4]},${pt[5]})`
+ `/ Distance(${pt[0]},${pt[1]})` + `/ Distance(${pt[0]},${pt[1]})`
direction := `UnitVector(Vector(${pt[6]}, ${pt[7]}))` direction := `UnitVector(Vector(${pt[6]}, ${pt[7]}))`
@ -511,7 +632,7 @@ classHandler: Record<JoyceClass, ClassHandler> :=
commands.push commands.push
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})` `${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
line: (name, method, args) => line: (name, method, args, index, cdata) =>
return := freshCommander() return := freshCommander()
return.value.ends = ['', ''] return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value {commands, callbacks, parts, auxiliaries, ends} := return.value
@ -519,10 +640,62 @@ classHandler: Record<JoyceClass, ClassHandler> :=
parts[1].push name parts[1].push name
madeSegment .= false madeSegment .= false
switch method switch method
'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 counterclockwise 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}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
'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 ...[1..4].map (n) => aux + n
'connect' 'connect'
unless args.subpoints and args.subpoints.length is 2 then return unless args.subpoints and args.subpoints.length is 2 then return
ends[0] = args.subpoints[0] ends[0] = args.subpoints[0]
ends[1] = args.subpoints[1] 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
'parallel' 'parallel'
unless args.subpoints then return unless args.subpoints then return
[newStart, oldStart, oldEnd] := args.subpoints [newStart, oldStart, oldEnd] := args.subpoints
@ -538,24 +711,41 @@ classHandler: Record<JoyceClass, ClassHandler> :=
madeSegment = true madeSegment = true
else else
commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)` commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)`
'chord' 'perpendicular'
// To match Joyce, we need to get the ordering here correct. pt := args.subpoints
// ends[0] should be the one closer to args.subpoints[0] unless pt then return
unless args.subpoints and args.circle then return inPlane .= ''
line := `Line(${args.subpoints.join ','})` if cdata.is3d
pt := args.subpoints[0] unless args.plane?.length is 1 then return
commands.push ...[1..2].map (n) => inPlane = `,${args.plane[0]}`
`${aux}${n} = Intersect(${args.circle}, ${line}, ${n})` switch pt.length
condition := `Distance(${aux}2,${pt}) < Distance(${aux}1,${pt})` when 2
// NOTE: Joyce's code has special case for when pt is almost ends[0] = pt[0]
// at midpoint of chord; in that case, it starts at endpoint ends[1] = aux
// closer to the second subpoint... postponing that nicety auxiliaries.push aux
if args.line?.length is 1
ln := args.line[0]
commands.push commands.push
`${aux}3 = If(${condition}, ${aux}2, ${aux}1)` `${name} = Rotate(${ln}, 3*pi/2, ${pt[0]}${inPlane})`
`${aux}4 = If(${condition}, ${aux}1, ${aux}2)` `${aux} = Vertex(${name}, 2)`
ends[0] = aux + 3 madeSegment = true
ends[1] = aux + 4 else
auxiliaries.push ...[1..4].map (n) => aux + n commands.push
`${aux} = Rotate(${pt[1]}, 3*pi/2, ${pt[0]}${inPlane})`
when 3
return // TODO: line perpendicular to plane
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
unless madeSegment unless madeSegment
commands.push `${name} = Segment(${ends[0]},${ends[1]})` commands.push `${name} = Segment(${ends[0]},${ends[1]})`
callbacks.push (api: AppletObject) => api.setLabelVisible name, true callbacks.push (api: AppletObject) => api.setLabelVisible name, true
@ -563,14 +753,27 @@ classHandler: Record<JoyceClass, ClassHandler> :=
circle: (name, method, args) => circle: (name, method, args) =>
return := freshCommander() return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value
parts[2].push name parts[2].push name
parts[1].push name parts[1].push name
switch method switch method
'radius' 'radius'
unless args.subpoints then return pt := args.subpoints
[center, point] := args.subpoints unless pt then return
commands.push `${name} = Circle(${center}, ${point})` inPlane := args.plane ? `, ${args.plane[0]}` : ''
switch pt.length
when 2
[center, point] := pt
ends[0] = center
commands.push
`${name} = Circle(${center}, ${point}${inPlane})`
when 3
center := pt[0]
ends[0] = center
radius := `Distance(${pt[1]}, ${pt[2]})`
commands.push
`${name} = Circle(${center}, ${radius}${inPlane})`
callbacks.push (api: AppletObject) => api.setLabelVisible name, true callbacks.push (api: AppletObject) => api.setLabelVisible name, true
polygon: (name, method, args, index) => polygon: (name, method, args, index) =>

View File

@ -1,4 +1,5 @@
export const flags = ['color', 'commands', 'showall', 'showaux'] as const export const flags = [
'color', 'commands', 'showall', 'showaux', 'algebra'] as const
export type FlagType = (typeof flags)[number] export type FlagType = (typeof flags)[number]
export type ConfigType = Partial<Record<FlagType, boolean>> export type ConfigType = Partial<Record<FlagType, boolean>>