feat: Implement additional Geometry Applet commands toward Book I (#45)
Reviewed-on: #45 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
277d9b0a8c
commit
5b1ca40b26
3 changed files with 243 additions and 132 deletions
|
@ -196,6 +196,12 @@ function dispatchJcommand(
|
|||
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} :=
|
||||
|
@ -491,7 +497,7 @@ function geoname(
|
|||
return .= 'Geo' + numCode.toString(36);
|
||||
return += '1' while return.value in elements
|
||||
|
||||
// Helper for similar point/line functions:
|
||||
// Helpers for some corresponding point/line functions:
|
||||
function cutoffExtend(
|
||||
method: string, pt: string[], point?: string[], line?: string[]
|
||||
): [string, string]
|
||||
|
@ -502,6 +508,35 @@ function cutoffExtend(
|
|||
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> :=
|
||||
|
@ -514,24 +549,10 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
parts[0].push name
|
||||
switch method
|
||||
/angle(?:Bisector|Divider)/
|
||||
// Note we just ignore a possible plane argument; it's irrelevant
|
||||
unless args.subpoints return
|
||||
[start, center, end] := args.subpoints
|
||||
// see if we need to make the destination segment from start to end
|
||||
destination .= ''
|
||||
unless args.line?.length is 1 and args.point?[0] is center
|
||||
destination = aux + '1'
|
||||
auxiliaries.push destination
|
||||
commands.push `${destination} = Segment(${start}, ${end})`
|
||||
else destination = args.line[0]
|
||||
n := 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})`
|
||||
`${name} = Intersect(${destination}, Ray(${center}, ${aux}4))`
|
||||
auxiliaries.push ...[2..4].map (i) => `${aux}${i}`
|
||||
{center, foot} :=
|
||||
makeAngDiv(method, args, cdata, aux, auxiliaries, commands)
|
||||
unless foot return
|
||||
commands.push `${name} = ${foot}`
|
||||
'circleSlider'
|
||||
unless args.circle then return
|
||||
circ := args.circle[0]
|
||||
|
@ -592,12 +613,23 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
if args.scalar and args.scalar.length
|
||||
callbacks.push (api: AppletObject) =>
|
||||
api.setCoords name, ...args.scalar as XYZ
|
||||
'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, ...args.scalar as XYZ
|
||||
'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'
|
||||
// Note only the two-point option implemented so far
|
||||
unless args.subpoints return
|
||||
|
@ -605,29 +637,10 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
// Note clockwise 90° rotation (3π/2) confirmed in Joyce source
|
||||
commands.push `${name} = Rotate(${direction}, 3*pi/2, ${center})`
|
||||
/proportion|similar/
|
||||
pt .= args.subpoints
|
||||
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]})`
|
||||
+ `/ Distance(${pt[0]},${pt[1]})`
|
||||
direction := `UnitVector(Vector(${pt[6]}, ${pt[7]}))`
|
||||
commands.push `${name} = Translate(${pt[6]}, ${len}*${direction})`
|
||||
[source, displacement] :=
|
||||
proportionSimilar method, args, cdata, aux, commands, auxiliaries
|
||||
unless source then return
|
||||
commands.push `${name} = Translate(${source}, ${displacement})`
|
||||
'vertex'
|
||||
commands.push
|
||||
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
|
||||
|
@ -640,6 +653,15 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
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
|
||||
|
@ -649,7 +671,7 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
unless cr return
|
||||
commands.push ...[1..2].map (n) =>
|
||||
`${aux}${n} = Intersect(${cr[0]}, ${cr[1]}, ${n})`
|
||||
inPlane := cdata.is3d ? `Plane(${cr[0]})` : ''
|
||||
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})`)
|
||||
|
@ -745,6 +767,14 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
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]})`
|
||||
|
@ -755,9 +785,14 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
return := freshCommander()
|
||||
return.value.ends = ['', '']
|
||||
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
||||
parts[2].push name
|
||||
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 ','})`
|
||||
'radius'
|
||||
pt := args.subpoints
|
||||
unless pt then return
|
||||
|
@ -766,30 +801,37 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
when 2
|
||||
[center, point] := pt
|
||||
ends[0] = center
|
||||
commands.push
|
||||
`${name} = Circle(${center}, ${point}${inPlane})`
|
||||
circle = `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})`
|
||||
circle = `Circle(${center}, ${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) =>
|
||||
polygon: (name, method, args, index, cdata) =>
|
||||
return := freshCommander()
|
||||
{commands, callbacks, parts, auxiliaries} := return.value
|
||||
parts[2].push name
|
||||
// what to push for edges?
|
||||
aux := name + 'aUx'
|
||||
switch method
|
||||
'equilateralTriangle'
|
||||
/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]
|
||||
callbacks.push (api: AppletObject, moreParts: DimParts) =>
|
||||
made:= api.evalCommandGetLabels
|
||||
`${name} = Polygon(${pt[1]},${pt[0]}, 3)`
|
||||
`${name} = Polygon(${pt[0]},${pt[1]}, ${N})`
|
||||
if not made return
|
||||
for each obj of made.split ','
|
||||
if obj is name continue
|
||||
|
@ -801,10 +843,42 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
'point'
|
||||
moreParts[0].push newObj
|
||||
api.setVisible newObj, false
|
||||
/triangle|quadrilateral/
|
||||
pt := args.subpoints
|
||||
unless pt then return
|
||||
commands.push ''
|
||||
/triangle|similar|parallelogram|application|quadrilateral|octagon/
|
||||
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]})*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 ...[1..3].map (n) => aux + n
|
||||
pt = [pt[0], pt[1], aux+3]
|
||||
else
|
||||
commands.push ''
|
||||
parts[0].push ...pt
|
||||
callbacks.push (api: AppletObject, moreParts: DimParts) =>
|
||||
made := api.evalCommandGetLabels
|
||||
|
@ -823,32 +897,25 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
aux := name + 'aUx'
|
||||
parts[2].push name
|
||||
switch method
|
||||
'sector'
|
||||
/arc|sector/
|
||||
unless args.subpoints?.length is 3 return
|
||||
parts[0].push ...args.subpoints
|
||||
[center, end, start] := args.subpoints
|
||||
[center, end, start] .= args.subpoints
|
||||
parms .= center + ', ' + start + ', ' + end
|
||||
prefix .= 'Circular'
|
||||
if method is 'arc'
|
||||
temp := end
|
||||
end = center
|
||||
center = temp
|
||||
parms = start + ', ' + center + ', ' + end
|
||||
prefix = 'Circumcircular'
|
||||
ends[0] = start
|
||||
ends[1] = end
|
||||
parms := center + ', ' + start + ', ' + end
|
||||
commands.push
|
||||
`${name} = CircularSector(${parms})`
|
||||
`${aux}1 = CircularArc(${parms})`
|
||||
`${name} = ${prefix}Sector(${parms})`
|
||||
`${aux}1 = ${prefix}Arc(${parms})`
|
||||
parts[1].push aux + 1
|
||||
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
|
||||
makeLinesInvisible callbacks, name
|
||||
|
||||
plane: (name, method, args) =>
|
||||
return := freshCommander()
|
||||
|
@ -900,3 +967,47 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
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 ...[2..4].map (i) => `${aux}${i}`
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue