feat: Implement additional Geometry Applet commands toward Book I #45

Merged
glen merged 2 commits from finish_bookI into main 2023-10-22 17:33:02 +00:00
3 changed files with 230 additions and 132 deletions
Showing only changes of commit 791c6173fd - Show all commits

View File

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

View File

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

View File

@ -497,7 +497,7 @@ function geoname(
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: // Helpers for some corresponding point/line functions:
function cutoffExtend( function cutoffExtend(
method: string, pt: string[], point?: string[], line?: string[] method: string, pt: string[], point?: string[], line?: string[]
): [string, string] ): [string, string]
@ -508,6 +508,35 @@ function cutoffExtend(
source := method is 'cutoff' ? pt[0] : pt[1] source := method is 'cutoff' ? pt[0] : pt[1]
[source, displacement] [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 // All of the detailed semantics of each available command lies in this
// function. // function.
classHandler: Record<JoyceClass, ClassHandler> := classHandler: Record<JoyceClass, ClassHandler> :=
@ -520,24 +549,10 @@ classHandler: Record<JoyceClass, ClassHandler> :=
parts[0].push name parts[0].push name
switch method switch method
/angle(?:Bisector|Divider)/ /angle(?:Bisector|Divider)/
// Note we just ignore a possible plane argument; it's irrelevant {center, foot} :=
unless args.subpoints return makeAngDiv(method, args, cdata, aux, auxiliaries, commands)
[start, center, end] := args.subpoints unless foot return
// see if we need to make the destination segment from start to end commands.push `${name} = ${foot}`
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}`
'circleSlider' 'circleSlider'
unless args.circle then return unless args.circle then return
circ := args.circle[0] circ := args.circle[0]
@ -611,6 +626,10 @@ classHandler: Record<JoyceClass, ClassHandler> :=
else else
commands.push commands.push
`${name} = Midpoint(${args.point?[0]},${args.point?[1]})` `${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' 'perpendicular'
// Note only the two-point option implemented so far // Note only the two-point option implemented so far
unless args.subpoints return unless args.subpoints return
@ -618,29 +637,10 @@ classHandler: Record<JoyceClass, ClassHandler> :=
// 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|similar/ /proportion|similar/
pt .= args.subpoints [source, displacement] :=
unless pt then return proportionSimilar method, args, cdata, aux, commands, auxiliaries
// reduce the similar case to general proportion unless source then return
if method is 'similar' commands.push `${name} = Translate(${source}, ${displacement})`
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})`
'vertex' 'vertex'
commands.push commands.push
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})` `${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
@ -653,6 +653,15 @@ classHandler: Record<JoyceClass, ClassHandler> :=
parts[1].push name parts[1].push name
madeSegment .= false madeSegment .= false
switch method 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' 'bichord'
// To match Joyce, we need to get the ordering here correct. // To match Joyce, we need to get the ordering here correct.
// we want the order so that start -> end sweeping past the // we want the order so that start -> end sweeping past the
@ -662,7 +671,7 @@ classHandler: Record<JoyceClass, ClassHandler> :=
unless cr return unless cr return
commands.push ...[1..2].map (n) => commands.push ...[1..2].map (n) =>
`${aux}${n} = Intersect(${cr[0]}, ${cr[1]}, ${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})` ctr := cr.map (c) => `Center(${c})`
condition := (`Angle(${aux}1,${ctr[0]},${ctr[1]}${inPlane})` condition := (`Angle(${aux}1,${ctr[0]},${ctr[1]}${inPlane})`
+ `< Angle(${aux}2,${ctr[0]},${ctr[1]}${inPlane})`) + `< Angle(${aux}2,${ctr[0]},${ctr[1]}${inPlane})`)
@ -758,6 +767,14 @@ classHandler: Record<JoyceClass, ClassHandler> :=
commands.push commands.push
`${aux}2 = Translate(${pt[0]}, ${dist}*${unitVec})` `${aux}2 = Translate(${pt[0]}, ${dist}*${unitVec})`
else return 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 unless madeSegment
commands.push `${name} = Segment(${ends[0]},${ends[1]})` commands.push `${name} = Segment(${ends[0]},${ends[1]})`
@ -768,9 +785,14 @@ classHandler: Record<JoyceClass, ClassHandler> :=
return := freshCommander() return := freshCommander()
return.value.ends = ['', ''] return.value.ends = ['', '']
{commands, callbacks, parts, auxiliaries, ends} := return.value {commands, callbacks, parts, auxiliaries, ends} := return.value
parts[2].push name aux := name + 'aUx'
parts[1].push name parts[1].push name
circle .= ''
switch method switch method
'circumcircle'
pt := args.subpoints
unless pt and pt.length is 3 then return
circle = `Circle(${pt.join ','})`
'radius' 'radius'
pt := args.subpoints pt := args.subpoints
unless pt then return unless pt then return
@ -779,30 +801,37 @@ classHandler: Record<JoyceClass, ClassHandler> :=
when 2 when 2
[center, point] := pt [center, point] := pt
ends[0] = center ends[0] = center
commands.push circle = `Circle(${center}, ${point}${inPlane})`
`${name} = Circle(${center}, ${point}${inPlane})`
when 3 when 3
center := pt[0] center := pt[0]
ends[0] = center ends[0] = center
radius := `Distance(${pt[1]}, ${pt[2]})` radius := `Distance(${pt[1]}, ${pt[2]})`
circle = `Circle(${center}, ${radius}${inPlane})`
commands.push commands.push
`${name} = Circle(${center}, ${radius}${inPlane})` `${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 callbacks.push (api: AppletObject) => api.setLabelVisible name, true
polygon: (name, method, args, index) => polygon: (name, method, args, index, cdata) =>
return := freshCommander() return := freshCommander()
{commands, callbacks, parts, auxiliaries} := return.value {commands, callbacks, parts, auxiliaries} := return.value
parts[2].push name parts[2].push name
// what to push for edges? aux := name + 'aUx'
switch method switch method
'equilateralTriangle' /equilateralTriangle|square|regularPolygon/
pt := args.subpoints pt := args.subpoints
unless pt then return 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 commands.push '' // hack, make sure there is a command
parts[0].push pt[0], pt[1] parts[0].push pt[0], pt[1]
callbacks.push (api: AppletObject, moreParts: DimParts) => callbacks.push (api: AppletObject, moreParts: DimParts) =>
made:= api.evalCommandGetLabels made:= api.evalCommandGetLabels
`${name} = Polygon(${pt[1]},${pt[0]}, 3)` `${name} = Polygon(${pt[0]},${pt[1]}, ${N})`
if not made return if not made return
for each obj of made.split ',' for each obj of made.split ','
if obj is name continue if obj is name continue
@ -814,9 +843,41 @@ classHandler: Record<JoyceClass, ClassHandler> :=
'point' 'point'
moreParts[0].push newObj moreParts[0].push newObj
api.setVisible newObj, false api.setVisible newObj, false
/triangle|quadrilateral/ /triangle|similar|parallelogram|application|quadrilateral|octagon/
pt := args.subpoints unless args.subpoints then return
unless pt 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 '' commands.push ''
parts[0].push ...pt parts[0].push ...pt
callbacks.push (api: AppletObject, moreParts: DimParts) => callbacks.push (api: AppletObject, moreParts: DimParts) =>
@ -836,32 +897,25 @@ classHandler: Record<JoyceClass, ClassHandler> :=
aux := name + 'aUx' aux := name + 'aUx'
parts[2].push name parts[2].push name
switch method switch method
'sector' /arc|sector/
unless args.subpoints?.length is 3 return unless args.subpoints?.length is 3 return
parts[0].push ...args.subpoints 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[0] = start
ends[1] = end ends[1] = end
parms := center + ', ' + start + ', ' + end
commands.push commands.push
`${name} = CircularSector(${parms})` `${name} = ${prefix}Sector(${parms})`
`${aux}1 = CircularArc(${parms})` `${aux}1 = ${prefix}Arc(${parms})`
parts[1].push aux + 1 parts[1].push aux + 1
callbacks.push (api: AppletObject) => makeLinesInvisible callbacks, name
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
plane: (name, method, args) => plane: (name, method, args) =>
return := freshCommander() return := freshCommander()
@ -913,3 +967,47 @@ classHandler: Record<JoyceClass, ClassHandler> :=
moreParts[1].push newObj moreParts[1].push newObj
'triangle' 'triangle'
moreParts[2].push newObj 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