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:
Glen Whitney 2023-10-22 17:33:01 +00:00 committed by Glen Whitney
parent 277d9b0a8c
commit 5b1ca40b26
3 changed files with 243 additions and 132 deletions

View File

@ -34,12 +34,12 @@
url: 'https://code.studioinfinity.org/glen/archematics.git',
},
devDependencies: {
'@danielx/civet': '^0.6.43',
'@types/firefox-webext-browser': '^111.0.2',
'@types/jquery': '^3.5.22',
'@danielx/civet': '^0.6.46',
'@types/firefox-webext-browser': '^111.0.3',
'@types/jquery': '^3.5.24',
'@webcomponents/custom-elements': '^1.6.0',
'http-server': '^14.1.1',
rollup: '^4.0.2',
rollup: '^4.1.4',
typescript: '^5.2.2',
'webextension-polyfill': '^0.10.0',
},

View File

@ -14,14 +14,14 @@ dependencies:
devDependencies:
'@danielx/civet':
specifier: ^0.6.43
version: 0.6.43(typescript@5.2.2)
specifier: ^0.6.46
version: 0.6.46(typescript@5.2.2)
'@types/firefox-webext-browser':
specifier: ^111.0.2
version: 111.0.2
specifier: ^111.0.3
version: 111.0.3
'@types/jquery':
specifier: ^3.5.22
version: 3.5.22
specifier: ^3.5.24
version: 3.5.24
'@webcomponents/custom-elements':
specifier: ^1.6.0
version: 1.6.0
@ -29,8 +29,8 @@ devDependencies:
specifier: ^14.1.1
version: 14.1.1
rollup:
specifier: ^4.0.2
version: 4.0.2
specifier: ^4.1.4
version: 4.1.4
typescript:
specifier: ^5.2.2
version: 5.2.2
@ -47,8 +47,8 @@ packages:
'@jridgewell/trace-mapping': 0.3.9
dev: true
/@danielx/civet@0.6.43(typescript@5.2.2):
resolution: {integrity: sha512-K4G9Rq4J6TY1H+XJOXCxY7IZqlLK+roL1ufvJQaOIrzx+KJ7F8zdn/4GKM7F7NKD4BgJl3avBqHAMzuUWRkzRg==}
/@danielx/civet@0.6.46(typescript@5.2.2):
resolution: {integrity: sha512-8WjtFrnK9TbBg5nVIQWFkFeWvDFBv/jFYYaasjiaT4+Vwr6TpZfbXnB/A4uj1sETagPtdNJPgasVpE1LHdrm6A==}
engines: {node: '>=19 || ^18.6.0 || ^16.17.0'}
hasBin: true
peerDependencies:
@ -78,108 +78,108 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@rollup/rollup-android-arm-eabi@4.0.2:
resolution: {integrity: sha512-xDvk1pT4vaPU2BOLy0MqHMdYZyntqpaBf8RhBiezlqG9OjY8F50TyctHo8znigYKd+QCFhCmlmXHOL/LoaOl3w==}
/@rollup/rollup-android-arm-eabi@4.1.4:
resolution: {integrity: sha512-WlzkuFvpKl6CLFdc3V6ESPt7gq5Vrimd2Yv9IzKXdOpgbH4cdDSS1JLiACX8toygihtH5OlxyQzhXOph7Ovlpw==}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-android-arm64@4.0.2:
resolution: {integrity: sha512-lqCglytY3E6raze27DD9VQJWohbwCxzqs9aSHcj5X/8hJpzZfNdbsr4Ja9Hqp6iPyF53+5PtPx0pKRlkSvlHZg==}
/@rollup/rollup-android-arm64@4.1.4:
resolution: {integrity: sha512-D1e+ABe56T9Pq2fD+R3ybe1ylCDzu3tY4Qm2Mj24R9wXNCq35+JbFbOpc2yrroO2/tGhTobmEl2Bm5xfE/n8RA==}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-darwin-arm64@4.0.2:
resolution: {integrity: sha512-nkBKItS6E6CCzvRwgiKad+j+1ibmL7SIInj7oqMWmdkCjiSX6VeVZw2mLlRKIUL+JjsBgpATTfo7BiAXc1v0jA==}
/@rollup/rollup-darwin-arm64@4.1.4:
resolution: {integrity: sha512-7vTYrgEiOrjxnjsgdPB+4i7EMxbVp7XXtS+50GJYj695xYTTEMn3HZVEvgtwjOUkAP/Q4HDejm4fIAjLeAfhtg==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-darwin-x64@4.0.2:
resolution: {integrity: sha512-vX2C8xvWPIbpEgQht95+dY6BReKAvtDgPDGi0XN0kWJKkm4WdNmq5dnwscv/zxvi+n6jUTBhs6GtpkkWT4q8Gg==}
/@rollup/rollup-darwin-x64@4.1.4:
resolution: {integrity: sha512-eGJVZScKSLZkYjhTAESCtbyTBq9SXeW9+TX36ki5gVhDqJtnQ5k0f9F44jNK5RhAMgIj0Ht9+n6HAgH0gUUyWQ==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.0.2:
resolution: {integrity: sha512-DVFIfcHOjgmeHOAqji4xNz2wczt1Bmzy9MwBZKBa83SjBVO/i38VHDR+9ixo8QpBOiEagmNw12DucG+v55tCrg==}
/@rollup/rollup-linux-arm-gnueabihf@4.1.4:
resolution: {integrity: sha512-HnigYSEg2hOdX1meROecbk++z1nVJDpEofw9V2oWKqOWzTJlJf1UXVbDE6Hg30CapJxZu5ga4fdAQc/gODDkKg==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm64-gnu@4.0.2:
resolution: {integrity: sha512-GCK/a9ItUxPI0V5hQEJjH4JtOJO90GF2Hja7TO+EZ8rmkGvEi8/ZDMhXmcuDpQT7/PWrTT9RvnG8snMd5SrhBQ==}
/@rollup/rollup-linux-arm64-gnu@4.1.4:
resolution: {integrity: sha512-TzJ+N2EoTLWkaClV2CUhBlj6ljXofaYzF/R9HXqQ3JCMnCHQZmQnbnZllw7yTDp0OG5whP4gIPozR4QiX+00MQ==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm64-musl@4.0.2:
resolution: {integrity: sha512-cLuBp7rOjIB1R2j/VazjCmHC7liWUur2e9mFflLJBAWCkrZ+X0+QwHLvOQakIwDymungzAKv6W9kHZnTp/Mqrg==}
/@rollup/rollup-linux-arm64-musl@4.1.4:
resolution: {integrity: sha512-aVPmNMdp6Dlo2tWkAduAD/5TL/NT5uor290YvjvFvCv0Q3L7tVdlD8MOGDL+oRSw5XKXKAsDzHhUOPUNPRHVTQ==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-x64-gnu@4.0.2:
resolution: {integrity: sha512-Zqw4iVnJr2naoyQus0yLy7sLtisCQcpdMKUCeXPBjkJtpiflRime/TMojbnl8O3oxUAj92mxr+t7im/RbgA20w==}
/@rollup/rollup-linux-x64-gnu@4.1.4:
resolution: {integrity: sha512-77Fb79ayiDad0grvVsz4/OB55wJRyw9Ao+GdOBA9XywtHpuq5iRbVyHToGxWquYWlEf6WHFQQnFEttsAzboyKg==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-x64-musl@4.0.2:
resolution: {integrity: sha512-jJRU9TyUD/iMqjf8aLAp7XiN3pIj5v6Qcu+cdzBfVTKDD0Fvua4oUoK8eVJ9ZuKBEQKt3WdlcwJXFkpmMLk6kg==}
/@rollup/rollup-linux-x64-musl@4.1.4:
resolution: {integrity: sha512-/t6C6niEQTqmQTVTD9TDwUzxG91Mlk69/v0qodIPUnjjB3wR4UA3klg+orR2SU3Ux2Cgf2pWPL9utK80/1ek8g==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-arm64-msvc@4.0.2:
resolution: {integrity: sha512-ZkS2NixCxHKC4zbOnw64ztEGGDVIYP6nKkGBfOAxEPW71Sji9v8z3yaHNuae/JHPwXA+14oDefnOuVfxl59SmQ==}
/@rollup/rollup-win32-arm64-msvc@4.1.4:
resolution: {integrity: sha512-ZY5BHHrOPkMbCuGWFNpJH0t18D2LU6GMYKGaqaWTQ3CQOL57Fem4zE941/Ek5pIsVt70HyDXssVEFQXlITI5Gg==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-ia32-msvc@4.0.2:
resolution: {integrity: sha512-3SKjj+tvnZ0oZq2BKB+fI+DqYI83VrRzk7eed8tJkxeZ4zxJZcLSE8YDQLYGq1tZAnAX+H076RHHB4gTZXsQzw==}
/@rollup/rollup-win32-ia32-msvc@4.1.4:
resolution: {integrity: sha512-XG2mcRfFrJvYyYaQmvCIvgfkaGinfXrpkBuIbJrTl9SaIQ8HumheWTIwkNz2mktCKwZfXHQNpO7RgXLIGQ7HXA==}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-x64-msvc@4.0.2:
resolution: {integrity: sha512-MBdJIOxRauKkry7t2q+rTHa3aWjVez2eioWg+etRVS3dE4tChhmt5oqZYr48R6bPmcwEhxQr96gVRfeQrLbqng==}
/@rollup/rollup-win32-x64-msvc@4.1.4:
resolution: {integrity: sha512-ANFqWYPwkhIqPmXw8vm0GpBEHiPpqcm99jiiAp71DbCSqLDhrtr019C5vhD0Bw4My+LmMvciZq6IsWHqQpl2ZQ==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@types/firefox-webext-browser@111.0.2:
resolution: {integrity: sha512-NS7izfYOnQI/Opf3YdZSKkI5Ox89SqEffJHK2zfGY2BYEVuWuM6pSwDRglGl4W0SM84oUQfvLyYH4X6EQZAJ2w==}
/@types/firefox-webext-browser@111.0.3:
resolution: {integrity: sha512-yboaUEJfY6AGB1hJIWzRGNa/qQUCC/2CAgBSWkdJ8rml4k0bXIyxlWYLSRTr6jHT29gdHkdHsPVuF5McSNERfw==}
dev: true
/@types/jquery@3.5.22:
resolution: {integrity: sha512-ISQFeUK5GwRftLK4PVvKTWEVCxZ2BpaqBz0TWkIq5w4vGojxZP9+XkqgcPjxoqmPeew+HLyWthCBvK7GdF5NYA==}
/@types/jquery@3.5.24:
resolution: {integrity: sha512-V/TG69ge5amcr8Ap7vY3SObqKfZlV7ttqcYnNcYnndI77ySIRi05+3GjvfwRtE2qalAC2ySLIL1ker512sI20g==}
dependencies:
'@types/sizzle': 2.3.3
dev: true
@ -544,23 +544,23 @@ packages:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: true
/rollup@4.0.2:
resolution: {integrity: sha512-MCScu4usMPCeVFaiLcgMDaBQeYi1z6vpWxz0r0hq0Hv77Y2YuOTZldkuNJ54BdYBH3e+nkrk6j0Rre/NLDBYzg==}
/rollup@4.1.4:
resolution: {integrity: sha512-U8Yk1lQRKqCkDBip/pMYT+IKaN7b7UesK3fLSTuHBoBJacCE+oBqo/dfG/gkUdQNNB2OBmRP98cn2C2bkYZkyw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.0.2
'@rollup/rollup-android-arm64': 4.0.2
'@rollup/rollup-darwin-arm64': 4.0.2
'@rollup/rollup-darwin-x64': 4.0.2
'@rollup/rollup-linux-arm-gnueabihf': 4.0.2
'@rollup/rollup-linux-arm64-gnu': 4.0.2
'@rollup/rollup-linux-arm64-musl': 4.0.2
'@rollup/rollup-linux-x64-gnu': 4.0.2
'@rollup/rollup-linux-x64-musl': 4.0.2
'@rollup/rollup-win32-arm64-msvc': 4.0.2
'@rollup/rollup-win32-ia32-msvc': 4.0.2
'@rollup/rollup-win32-x64-msvc': 4.0.2
'@rollup/rollup-android-arm-eabi': 4.1.4
'@rollup/rollup-android-arm64': 4.1.4
'@rollup/rollup-darwin-arm64': 4.1.4
'@rollup/rollup-darwin-x64': 4.1.4
'@rollup/rollup-linux-arm-gnueabihf': 4.1.4
'@rollup/rollup-linux-arm64-gnu': 4.1.4
'@rollup/rollup-linux-arm64-musl': 4.1.4
'@rollup/rollup-linux-x64-gnu': 4.1.4
'@rollup/rollup-linux-x64-musl': 4.1.4
'@rollup/rollup-win32-arm64-msvc': 4.1.4
'@rollup/rollup-win32-ia32-msvc': 4.1.4
'@rollup/rollup-win32-x64-msvc': 4.1.4
fsevents: 2.3.3
dev: true

View File

@ -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