feat: Implement 3D Joyce Applets via JSXGraph
This is a stub/very preliminary implementation of calling JSXGraph for 3D Joyce applets. The only element/construction method implemented so far is a free point. NOTE: This implementation is so far extremely buggy. Loading a page with a 3D applet such as http://aleph0.clarku.edu/~djoyce/java/elements/bookXI/defXI9.html appears to enter a loop in which the div containing the JSXGraph Board progressively grows larger and larger, while using a tremendous amount of cpu.
This commit is contained in:
parent
e742ef3460
commit
4e2375b709
45 changed files with 23637 additions and 20477 deletions
|
@ -2,6 +2,12 @@ import ./deps/jquery.js
|
|||
import type {AppletObject} from ./deps/geotypes/api.ts
|
||||
import {AppletDescription, AdapParams, params, contains3d} from ./adapptypes.ts
|
||||
colorsea from ./deps/colorsea.js
|
||||
JXG from ./deps/jsxgraphcore.mjs
|
||||
|
||||
/* NOTE that the actual actions of the script are at the very bottom,
|
||||
* so that all of the lexical variables will have been initialized by
|
||||
* the time that those actions execute.
|
||||
*/
|
||||
|
||||
joyceApplets: AppletDescription[] := []
|
||||
$('applet[code="Geometry"]').before (i, html) ->
|
||||
|
@ -20,22 +26,25 @@ function assertJoyceClass(s: string): asserts s is JoyceClass
|
|||
unless classes.includes s then throw new Error `Oops ${s} slipped through`
|
||||
|
||||
type JoyceName = string // we use this to indicate where the names
|
||||
// from the Joyce commands (which are used as captions in the GeoGebra
|
||||
// from the Joyce commands (which are used as captions in the dynamic geometry
|
||||
// applet) go.
|
||||
type GeoName = string // and this to indicate where GeoGebra identifiers go
|
||||
type AnyName = GeoName | JoyceName // and this for slots that can be either
|
||||
type DyName = string // and this to indicate where dynamic geometry identifiers go
|
||||
type AnyName = DyName | JoyceName // and this for slots that can be either
|
||||
|
||||
type Description
|
||||
type Description // the semantic information needed about a Joyce geometry elt
|
||||
otherName: AnyName
|
||||
jsxElement?: JXG.GeometryElement // only JSX exposes JavaScript entities
|
||||
usesCaptions: JoyceName[]
|
||||
klass: JoyceClass
|
||||
ends?: [GeoName, GeoName]
|
||||
parts?: string[][]
|
||||
ends?: [DyName, DyName]
|
||||
parts?: string[][] // FIXME: Should be DyName[][], I think
|
||||
|
||||
// We put both JoyceNames and GeoNames in here, pointing to each other
|
||||
// We put both JoyceNames and DyNames in here, pointing to each other
|
||||
// with the otherName property:
|
||||
type JoyceElements = Record<AnyName, Description>
|
||||
|
||||
type DynApp = AppletObject | JXG.Board | JXG.GeometryElement
|
||||
|
||||
adapptScript := findAdappt() as HTMLScriptElement
|
||||
|
||||
function findAdappt()
|
||||
|
@ -57,19 +66,33 @@ function vertFlipped(coords: number[], cdata: ConstructionData): XYZ
|
|||
coords = coords.slice()
|
||||
if cdata.is3d
|
||||
if coords[Z] then coords[Z] = -coords[Z]
|
||||
while coords.length < 3
|
||||
coords.push 0
|
||||
coords[Y] = cdata.height - coords[Y]
|
||||
return coords as XYZ
|
||||
|
||||
type RGB = [number, number, number]
|
||||
type ConstructionData
|
||||
id: string
|
||||
bg: RGB
|
||||
is3d: boolean
|
||||
isJSX: boolean
|
||||
width: number
|
||||
height: number
|
||||
labelOffset?: [number, number]
|
||||
elements: JoyceElements
|
||||
pivot: string
|
||||
title?: string
|
||||
|
||||
// Helper function to deal with typing the various apis
|
||||
type DiscriminatedAPI
|
||||
jsxApi?: JXG.Board | JXG.View3D
|
||||
geoApi?: AppletObject
|
||||
|
||||
function getApis(api: DynApp, cdata: ConstructionData) : DiscriminatedAPI
|
||||
if cdata.isJSX then return {jsxApi: api as JXG.Board | JXG.View3D}
|
||||
else return {geoApi: api as AppletObject}
|
||||
|
||||
// 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):
|
||||
|
@ -86,6 +109,45 @@ pivotData: Record<string, PivotData> := {}
|
|||
|
||||
function postApplets(jApplets: AppletDescription[], codebase = '')
|
||||
for each jApp of jApplets
|
||||
is3d := contains3d jApp.params
|
||||
isJSX := is3d // For now; will eventually just be always true
|
||||
elements := {}
|
||||
pivot .= ''
|
||||
cdata: ConstructionData := {
|
||||
bg: ([255, 255, 255] as RGB),
|
||||
is3d, isJSX, jApp.id, jApp.width, jApp.height, elements, pivot}
|
||||
if 'pivot' in jApp.params
|
||||
if is3d
|
||||
console.warn('Geometry Applet "pivot" only supported for '
|
||||
+ '2D constructions. Ignoring.')
|
||||
// FIXME: JSXGraph might well support "pivot" for 3D, by moving
|
||||
// the implicit centerpoint of the "trackball navigation" mode of
|
||||
// dragging to the pivot point.
|
||||
else
|
||||
pivot = dyname(jApp.id, cdata, 'point') + 'Pivot'
|
||||
cdata.pivot = pivot
|
||||
if isJSX
|
||||
// for now, only use JSXGraph for 3D constructions
|
||||
board := JXG.JSXGraph.initBoard jApp.id, {
|
||||
boundingbox:
|
||||
[-jApp.width-10, jApp.height+10, jApp.width+10, -jApp.height-10],
|
||||
+zoom,
|
||||
pan: {+enabled, -needShift},
|
||||
drag: {+enabled},
|
||||
-grid }
|
||||
if is3d // redundant for now, but won't be when we use JSXGraph always
|
||||
depth .= jApp.width
|
||||
if jApp.height > depth then depth = jApp.height
|
||||
depth /= 8
|
||||
view := board.create 'view3d',
|
||||
[ [-jApp.width, -jApp.height],
|
||||
[2*jApp.width, 2*jApp.height],
|
||||
[ [-10 + jApp.width/6, 10 + 4*jApp.width/6],
|
||||
[-10 + jApp.height/6, 10 + 4*jApp.height/6],
|
||||
[-2*depth - 10, depth + 10]]]
|
||||
for name, value in jApp.params
|
||||
dispatchJcommand view, name, value, cdata
|
||||
continue
|
||||
params := {
|
||||
appName: 'classic',
|
||||
-enableRightClick,
|
||||
|
@ -93,7 +155,6 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
|
|||
jApp.width,
|
||||
jApp.height,
|
||||
appletOnLoad: (api: AppletObject) =>
|
||||
is3d := contains3d jApp.params
|
||||
if is3d
|
||||
api.enable3D true
|
||||
api.setPerspective 'T'
|
||||
|
@ -105,20 +166,10 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
|
|||
api.setPerspective 'G'
|
||||
if adapParams.config?.algebra
|
||||
api.setPerspective '+A'
|
||||
elements := {}
|
||||
pivot .= ''
|
||||
if 'pivot' in jApp.params
|
||||
if is3d
|
||||
console.warn('Geometry Applet "pivot" only supported for '
|
||||
+ '2D constrcutions. Ignoring.')
|
||||
else
|
||||
pivot = geoname(jApp.id, elements, 'point') + 'Pivot'
|
||||
pivotData[pivot] = {
|
||||
api, jApp.params.pivot,
|
||||
lastAngle: 0, rotatable: [], maybeRotatable: {}, fixed: {}}
|
||||
cdata: ConstructionData := {
|
||||
bg: ([255, 255, 255] as RGB),
|
||||
is3d, jApp.id, jApp.width, jApp.height, elements, pivot}
|
||||
if pivot
|
||||
pivotData[pivot] = {
|
||||
api, jApp.params.pivot,
|
||||
lastAngle: 0, rotatable: [], maybeRotatable: {}, fixed: {}}
|
||||
for name, value in jApp.params
|
||||
dispatchJcommand
|
||||
api, name, value, cdata
|
||||
|
@ -147,22 +198,19 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
|
|||
if codebase then geoApp.setHTML5Codebase codebase
|
||||
geoApp.inject jApp.id
|
||||
|
||||
// Always use the final joyceApplets if there are any:
|
||||
if joyceApplets.length
|
||||
adapParams.joyceApplets = joyceApplets
|
||||
|
||||
if adapParams.joyceApplets.length
|
||||
if adapParams.loader
|
||||
jQuery.getScript adapParams.loader, =>
|
||||
postApplets adapParams.joyceApplets
|
||||
else
|
||||
postApplets adapParams.joyceApplets, adapParams.codebase
|
||||
|
||||
/* That's all of the actions of this script. All of the remainder is
|
||||
* implementation.
|
||||
*/
|
||||
|
||||
type DimParts = [string[], string[], string[]] // Gives GeoNames
|
||||
type JoyceArguments =
|
||||
Partial<Record<JoyceClass|'subpoints', DyName[]> & {scalar: number[]}>
|
||||
type Element // the syntactic specfication of a Joyce construction element
|
||||
index: number
|
||||
name: DyName
|
||||
jname: JoyceName
|
||||
klass: JoyceClass
|
||||
method: string
|
||||
args: JoyceArguments
|
||||
usesCaptions: JoyceName[]
|
||||
colors: string[]
|
||||
|
||||
type DimParts = [string[], string[], string[]] // Gives DyNames
|
||||
// or expressions for 0-, 1-, and 2-dimensional parts for coloring
|
||||
|
||||
// need to pass the parts into the callbacks because sometimes the parts
|
||||
|
@ -172,8 +220,8 @@ type Commander
|
|||
commands: string[]
|
||||
callbacks: GeogebraCallback[]
|
||||
parts: DimParts
|
||||
auxiliaries: GeoName[] // extra entities needed in GeoGebra
|
||||
ends?: [GeoName, GeoName]
|
||||
auxiliaries: DyName[] // extra entities needed in JavaScript dynamic app
|
||||
ends?: [DyName, DyName]
|
||||
|
||||
function freshCommander(): Commander
|
||||
commands: []
|
||||
|
@ -181,265 +229,79 @@ function freshCommander(): Commander
|
|||
parts: [[], [], []]
|
||||
auxiliaries: []
|
||||
|
||||
type JoyceArguments =
|
||||
Partial<Record<JoyceClass|'subpoints', GeoName[]> & {scalar: number[]}>
|
||||
type ClassHandler = (
|
||||
name: GeoName,
|
||||
method: string,
|
||||
args: JoyceArguments,
|
||||
index: number,
|
||||
cdata: ConstructionData,
|
||||
colors: string[],
|
||||
jname: string) => Commander
|
||||
type RGB = [number, number, number]
|
||||
|
||||
type ClassHandler = (elt: Element, cdata: ConstructionData) => Commander
|
||||
|
||||
type XYZ = RGB
|
||||
|
||||
// For interpreting Joyce applet `align` parameter
|
||||
alignTranslation: Record<string, [number, number]|undefined> := {
|
||||
above: [0,10]
|
||||
right: [10, 0]
|
||||
below: [0,-10]
|
||||
left: [-10, 0]
|
||||
central: undefined }
|
||||
|
||||
// Executes the command corresponding to param against the GeoGebra applet
|
||||
// api, consulting and extending by side effect the elements that are
|
||||
// present in that applet
|
||||
function dispatchJcommand(
|
||||
api: AppletObject,
|
||||
api: DynApp,
|
||||
name: string,
|
||||
value: string,
|
||||
cdata: ConstructionData): void
|
||||
{geoApi, jsxApi} := getApis api, cdata
|
||||
switch name
|
||||
'background'
|
||||
cdata.bg = joyce2rgb value, cdata.bg
|
||||
if adapParams.config?.commands
|
||||
console.log 'Setting background to', value, 'interpreted as',
|
||||
cdata.bg
|
||||
api.setGraphicsOptions 1, bgColor: colorsea(cdata.bg).hex()
|
||||
col := colorsea(cdata.bg).hex()
|
||||
if cdata.isJSX // just take advantage of JSXGraph transparency
|
||||
domElt := document.getElementById(cdata.id)
|
||||
if domElt then domElt.style.background = col
|
||||
else if geoApi then geoApi.setGraphicsOptions 1, bgColor: col
|
||||
'title'
|
||||
if adapParams.config?.commands
|
||||
console.log 'Setting title to', value
|
||||
cdata.title = value
|
||||
api.evalCommand `TitlePoint = Corner(1,1)
|
||||
Text("${value}", TitlePoint + (2,5))`
|
||||
if cdata.isJSX // Currently no good way to make titles :-(
|
||||
console.log('Title of Joyce applet', cdata.id, 'is', value)
|
||||
else if geoApi
|
||||
geoApi.evalCommand `TitlePoint = Corner(1,1)
|
||||
Text("${value}", TitlePoint + (2,5))`
|
||||
'pivot'
|
||||
return // already handled in postApplets
|
||||
'align'
|
||||
console.warn
|
||||
if cdata.isJSX // need to change the default offset of labels
|
||||
cdata.labelOffset = alignTranslation[value.toLowerCase()]
|
||||
// FIXME: need to actually read this when labeling
|
||||
else console.warn
|
||||
'Label alignment is not available in GeoGebra'
|
||||
'translation, as there is no facility for automatically'
|
||||
'positioning labels. However, they can be dragged manually.'
|
||||
return
|
||||
/e\[\d+\]/
|
||||
num := parseInt(name.slice(2))
|
||||
{commands, callbacks, parts} :=
|
||||
jToG value, num, cdata
|
||||
if commands.length
|
||||
lastTried .= 0
|
||||
if commands.filter((&)).every (cmd) =>
|
||||
if adapParams.config?.commands
|
||||
console.log 'Translated to:', cmd
|
||||
api.evalCommand(cmd) and ++lastTried
|
||||
callbacks.forEach &(api, parts)
|
||||
else console.warn
|
||||
`Geogebra command '${commands[lastTried]}'
|
||||
(part of translation of '${value}')
|
||||
failed.`
|
||||
else console.warn `Could not parse command '${value}'`
|
||||
if jsxApi
|
||||
jAsJSX value, num, jsxApi, cdata
|
||||
else if geoApi
|
||||
{commands, callbacks, parts} := jToG value, num, cdata
|
||||
if commands.length
|
||||
lastTried .= 0
|
||||
if commands.filter(&).every (cmd) =>
|
||||
if adapParams.config?.commands
|
||||
console.log 'Translated to:', cmd
|
||||
geoApi.evalCommand(cmd) and ++lastTried
|
||||
callbacks.forEach &(geoApi, parts)
|
||||
else console.warn
|
||||
`Geogebra command '${commands[lastTried]}'
|
||||
(part of translation of '${value}')
|
||||
failed.`
|
||||
else console.warn `Could not parse command '${value}'`
|
||||
else console.warn `Unknown param ${name} = ${value}`
|
||||
|
||||
// Parses a Joyce element-creating command, extending the elements
|
||||
// by side effect:
|
||||
function jToG(
|
||||
jCom: string,
|
||||
index: number,
|
||||
cdata: ConstructionData): Commander
|
||||
[jname, klass, method, data, ...colors] := jCom.split ';'
|
||||
if adapParams.config?.commands
|
||||
console.log 'Defining', jname, 'as a', klass, 'constructed by',
|
||||
method, 'from', data, 'colored as', colors
|
||||
cmdr .= freshCommander()
|
||||
unless klass in classHandler
|
||||
console.warn `Unknown entity class ${klass}`
|
||||
return cmdr
|
||||
assertJoyceClass klass // shouldn't need to do that :-/
|
||||
isPivot := !!cdata.pivot and jname is pivotData[cdata.pivot].pivot
|
||||
name := geoname jname, cdata.elements, klass
|
||||
if isPivot then pivotData[cdata.pivot].pivot = name
|
||||
args: JoyceArguments := {}
|
||||
usesCaptions := []
|
||||
for each jdep of data.split ','
|
||||
scalar := Number jdep
|
||||
if scalar is scalar // not NaN
|
||||
(args.scalar ?= []).push scalar
|
||||
continue
|
||||
if jdep is 'screen'
|
||||
// special case for Joyce; assume xOyPlane
|
||||
(args.plane ?= []).push 'xOyPlane'
|
||||
else
|
||||
unless jdep in cdata.elements
|
||||
console.warn
|
||||
`Reference to unknown geometric entity ${jdep} in ${jCom}`
|
||||
return cmdr
|
||||
usesCaptions.push jdep
|
||||
{klass: depKlass, otherName: depGeo, ends} := cdata.elements[jdep]
|
||||
(args[depKlass] ?= []).push depGeo
|
||||
if depKlass is 'point'
|
||||
(args.subpoints ?= []).push depGeo
|
||||
else if depKlass is 'line'
|
||||
(args.subpoints ?= []).push ...ends ?? []
|
||||
cmdr = classHandler[klass] name, method, args, index, cdata, colors, jname
|
||||
unless name is jname then cmdr.callbacks.push (api: AppletObject) =>
|
||||
api.setCaption name, jname
|
||||
api.setLabelStyle name, 3 // style CAPTION = 3
|
||||
if cmdr.auxiliaries.length and not adapParams.config?.showaux
|
||||
cmdr.callbacks.push (api: AppletObject) =>
|
||||
for each aux of cmdr.auxiliaries
|
||||
api.setAuxiliary aux, true
|
||||
api.setLabelVisible aux, false
|
||||
api.setVisible aux,false
|
||||
|
||||
// set up the pivot if there is one
|
||||
if isPivot
|
||||
unless klass is 'point'
|
||||
console.warn(`Can only pivot around a point, not the ${klass}`
|
||||
+ `named ${jname}. Ignoring.`)
|
||||
cdata.pivot = ''
|
||||
cmdr.commands.push(`${cdata.pivot} = `
|
||||
+ `Slider(0°,360°,1°,1,${cdata.width/3},true,true,false,false)`)
|
||||
cmdr.callbacks.push (api: AppletObject) =>
|
||||
api.setCaption cdata.pivot, 'Rotate Display'
|
||||
api.setLabelStyle cdata.pivot, 3
|
||||
api.setCoords(cdata.pivot, 2*cdata.width/3, cdata.height-10)
|
||||
// Not sure how to let TypeScript deal with putting a new function
|
||||
// on the global window object, so punting at least for now:
|
||||
// @ts-ignore
|
||||
window.pivotListener = pivotListener
|
||||
api.registerObjectUpdateListener(cdata.pivot, 'pivotListener')
|
||||
|
||||
// Create callback to assign colors
|
||||
traceC := adapParams.config?.color
|
||||
console.log 'Considering coloring', name, 'with', colors if traceC
|
||||
if colors.length is 4 and colors.every (color) => invisible color
|
||||
cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false
|
||||
else // we have to decorate
|
||||
dimension .= cmdr.parts.findLastIndex .includes name
|
||||
cmdr.callbacks.push (api: AppletObject, parts: DimParts) =>
|
||||
// Operate in order faces, lines, point, caption so that
|
||||
// we can adjust components after setting overall color, etc.
|
||||
|
||||
// Color the "Faces"; they default to 'brighter':
|
||||
if invisible(colors[3]) and (klass !== 'sphere' or invisible colors[2])
|
||||
for each face of parts[2]
|
||||
if face is name
|
||||
console.log 'Fading out interior of', face if traceC
|
||||
// hide the interior by making it transparent
|
||||
api.setFilling face, 0
|
||||
else if face not in cdata.elements
|
||||
console.log 'Hiding face', face if traceC
|
||||
api.setVisible face, false
|
||||
else
|
||||
surface .= colors[3]
|
||||
if klass is 'sphere' and invisible surface
|
||||
surface = colors[2] // for Joyce, spheres had one circular "edge"
|
||||
faceRGB := joyce2rgb(surface or 'brighter', cdata.bg)
|
||||
deep := ['circle', 'polygon', 'sector']
|
||||
filling := deep.includes(klass) ? 0.7 : 0.2
|
||||
for each face of parts[2]
|
||||
if traceC
|
||||
console.log 'Coloring face', face, 'to',
|
||||
surface, '=', faceRGB
|
||||
api.setVisible face, true
|
||||
api.setFilling face, filling
|
||||
api.setColor face, ...faceRGB
|
||||
|
||||
// Lines default to black:
|
||||
if invisible colors[2]
|
||||
for each line of parts[1]
|
||||
if line is name or line not in cdata.elements
|
||||
console.log 'Hiding line', line if traceC
|
||||
api.setVisible line, false
|
||||
else
|
||||
lineRGB := joyce2rgb(colors[2] or 'black', cdata.bg)
|
||||
if traceC
|
||||
console.log 'Need to color lines', parts[1], 'with', lineRGB
|
||||
for each line of parts[1]
|
||||
console.log 'Coloring line', line, 'to', colors[2] if traceC
|
||||
api.setVisible line, true
|
||||
api.setColor line, ...lineRGB
|
||||
|
||||
// Now color the points:
|
||||
if traceC
|
||||
console.log
|
||||
'Considering point colors for', name, 'of dimension', dimension
|
||||
if invisible colors[1]
|
||||
// Hide all the dim-0 elements that are not distinct independent
|
||||
// items:
|
||||
for each point of parts[0]
|
||||
if point is name or point not in cdata.elements
|
||||
console.log 'Hiding point', point if traceC
|
||||
api.setVisible point, false
|
||||
else if dimension is 0 or colors[1] // Need to color the points
|
||||
if not colors[1]
|
||||
colors[1] = pointDefaultColorName name, method, isPivot
|
||||
ptRGB := joyce2rgb colors[1], cdata.bg
|
||||
for each point of parts[0]
|
||||
console.log 'Coloring point', point, 'to', colors[1] if traceC
|
||||
api.setVisible point, true
|
||||
api.setColor point, ...ptRGB
|
||||
|
||||
// Make the caption the correct color
|
||||
if invisible colors[0]
|
||||
console.log 'Hiding label', name if traceC
|
||||
api.setLabelVisible name, false
|
||||
else if colors[0]
|
||||
if colors[dimension+1] and colors[dimension+1] is not colors[0]
|
||||
// Have to make a text to provide the caption, since GeoGebra
|
||||
// doesn't allow caption different color from entity.
|
||||
textName := 'GeoText' + index
|
||||
locationExpr := switch klass
|
||||
when 'point' then `1.02${name}`
|
||||
when 'line'
|
||||
(`Midpoint(${name})`
|
||||
+ ` + Rotate(Direction(${name})`
|
||||
+ `*Length(${name})*0.025/Length(Direction(${name})),`
|
||||
+ `pi/2)`)
|
||||
when 'circle'
|
||||
(`Center(${name})`
|
||||
+ ` + Radius(${name})*Vector((12/13,5/13))*1.03`)
|
||||
when 'polygon' then `Centroid(${name})`
|
||||
when 'sector'
|
||||
(`(5*Center(${name})`
|
||||
+ ` - ${cmdr.ends?[0]} - ${cmdr.ends?[1]})/3`)
|
||||
when 'plane'
|
||||
`Intersect(${name}, PerpendicularLine((0, 0, 0), ${name}))`
|
||||
when 'sphere'
|
||||
(`Center(${name})`
|
||||
+ ` + Radius(${name})*Vector((12/13,0,5/13))*1.03`)
|
||||
when 'polyhedron'
|
||||
// The "ends" are faces or vertices roughly opposite
|
||||
// from each other
|
||||
[ex1, ex2] .= cmdr.ends ?? ['', '']
|
||||
unless parts[0].includes ex1 then ex1 = `Centroid(${ex1})`
|
||||
unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})`
|
||||
`(4*${ex1}+${ex2})/5`
|
||||
textCmd := `${textName} = Text("${jname}", ${locationExpr})`
|
||||
textCol := joyce2rgb colors[0], cdata.bg
|
||||
if traceC
|
||||
console.log `Making text '${textCmd}' colored`, textCol
|
||||
api.evalCommand textCmd
|
||||
api.setColor textName, ...textCol
|
||||
// and hide the underlying GeoGebra label
|
||||
api.setLabelVisible name, false
|
||||
else // specified label color matches the entity color
|
||||
// So label gets the correct color from element
|
||||
// but we had better make sure it is visible:
|
||||
console.log 'Showing label', name if traceC
|
||||
api.setLabelVisible name, true
|
||||
else // label color is defaulting
|
||||
// Make it same as the element for points, invisible otherwise:
|
||||
show := klass is 'point'
|
||||
console.log 'Setting label vis of', name, 'to', show if traceC
|
||||
api.setLabelVisible name, show
|
||||
|
||||
cdata.elements[jname] =
|
||||
{otherName: name, usesCaptions, klass, cmdr.ends, cmdr.parts}
|
||||
cdata.elements[name] =
|
||||
{otherName: jname, usesCaptions, klass, cmdr.ends, cmdr.parts}
|
||||
cmdr
|
||||
|
||||
function pivotListener(slider: string)
|
||||
pd := pivotData[slider]
|
||||
|
@ -527,8 +389,9 @@ function pointDefaultColorName(
|
|||
'orange'
|
||||
else 'black'
|
||||
|
||||
function geoname(
|
||||
jname: JoyceName, elements: JoyceElements, klass: JoyceClass): GeoName
|
||||
function dyname(
|
||||
jname: JoyceName, cd: ConstructionData, klass: JoyceClass): DyName
|
||||
if cd.isJSX then return jname
|
||||
unless jname is 'floor' or jname.substring(0,3) is 'Geo' // those might clash
|
||||
// Names with word characters starting with a capital are always good:
|
||||
if /^[A-Z]['\w]*$/.test jname then return jname
|
||||
|
@ -538,7 +401,7 @@ function geoname(
|
|||
numCode .= 0n
|
||||
numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname
|
||||
return .= 'Geo' + numCode.toString(36);
|
||||
return += '1' while return.value in elements
|
||||
return += '1' while return.value in cd.elements
|
||||
|
||||
// Helpers for some corresponding point/line functions:
|
||||
function cutoffExtend(
|
||||
|
@ -583,7 +446,8 @@ function proportionSimilar(
|
|||
// All of the detailed semantics of each available command lies in this
|
||||
// function.
|
||||
classHandler: Record<JoyceClass, ClassHandler> :=
|
||||
point: (name, method, args, index, cdata, colors, jname): Commander =>
|
||||
point: (elt, cdata): Commander =>
|
||||
{name, jname, method, args, colors} .= elt // Mutable for hack patches
|
||||
return := freshCommander()
|
||||
{commands, callbacks, parts, auxiliaries} := return.value
|
||||
zeroVector := cdata.is3d ? 'Vector((0,0,0))' : 'Vector((0,0))'
|
||||
|
@ -801,7 +665,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
|
||||
else console.warn 'Unknown point method:', method
|
||||
|
||||
line: (name, method, args, index, cdata) =>
|
||||
line: (elt, cdata) =>
|
||||
{name, method, args} := elt
|
||||
return := freshCommander()
|
||||
return.value.ends = ['', '']
|
||||
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
||||
|
@ -951,7 +816,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
|
||||
parts[0].push ...ends
|
||||
|
||||
circle: (name, method, args, index, cdata) =>
|
||||
circle: (elt, cdata) =>
|
||||
{name, method, args} := elt
|
||||
return := freshCommander()
|
||||
return.value.ends = ['', '']
|
||||
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
||||
|
@ -987,11 +853,12 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
makeLinesInvisible callbacks, aux
|
||||
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
|
||||
|
||||
polygon: (name, method, args, index, cdata) =>
|
||||
polygon: (elt, cdata) =>
|
||||
{name, method, args, index} := elt
|
||||
return := freshCommander()
|
||||
{commands, callbacks, parts, auxiliaries} := return.value
|
||||
parts[2].push name
|
||||
aux := geoname name + 'aUx', cdata.elements, 'point'
|
||||
aux := dyname name + 'aUx', cdata, 'point'
|
||||
switch method
|
||||
/equilateralTriangle|square|regularPolygon/
|
||||
pt := args.subpoints
|
||||
|
@ -1077,7 +944,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
api.renameObject obj, newObj
|
||||
moreParts[1].push newObj
|
||||
|
||||
sector: (name, method, args, index, cdata) =>
|
||||
sector: (elt, cdata) =>
|
||||
{name, method, args} := elt
|
||||
return := freshCommander()
|
||||
return.value.ends = ['', '']
|
||||
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
||||
|
@ -1108,7 +976,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
auxiliaries.push aux + 1
|
||||
makeLinesInvisible callbacks, name
|
||||
|
||||
plane: (name, method, args, index, cdata) =>
|
||||
plane: (elt, cdata) =>
|
||||
{name, method, args} := elt
|
||||
return := freshCommander()
|
||||
{commands, callbacks, parts, auxiliaries} := return.value
|
||||
parts[2].push name
|
||||
|
@ -1126,7 +995,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
commands.push
|
||||
`${name} = PerpendicularPlane(${thru}, Line(${thru}, ${perp}))`
|
||||
|
||||
sphere: (name, method, args) =>
|
||||
sphere: (elt, cdata) =>
|
||||
{name, method, args} := elt
|
||||
return := freshCommander()
|
||||
return.value.ends = ['', '']
|
||||
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
||||
|
@ -1144,11 +1014,12 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
radius := `Distance(${pt[1]}, ${pt[2]})`
|
||||
commands.push `${name} = Sphere(${center}, ${radius})`
|
||||
|
||||
polyhedron: (name, method, args, index, cdata) =>
|
||||
polyhedron: (elt, cdata) =>
|
||||
{name, method, args, index} := elt
|
||||
return := freshCommander()
|
||||
return.value.ends = ['', '']
|
||||
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
||||
aux := geoname name + 'aUx', cdata.elements, 'point'
|
||||
aux := dyname name + 'aUx', cdata, 'point'
|
||||
switch method
|
||||
'parallelepiped'
|
||||
pt .= args.subpoints
|
||||
|
@ -1260,6 +1131,217 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
'triangle'
|
||||
moreParts[2].push newObj
|
||||
|
||||
// Parses a Joyce element-creating command
|
||||
function parseElement(
|
||||
jCom: string,
|
||||
index: number,
|
||||
cdata: ConstructionData): Element | undefined
|
||||
[jname, klass, method, data, ...colors] := jCom.split ';'
|
||||
if adapParams.config?.commands
|
||||
console.log 'Defining', jname, 'as a', klass, 'constructed by',
|
||||
method, 'from', data, 'colored as', colors
|
||||
unless klass in classHandler
|
||||
console.warn `Unknown entity class ${klass}`
|
||||
return undefined
|
||||
assertJoyceClass klass // shouldn't need to do that :-/
|
||||
name := dyname jname, cdata, klass
|
||||
args: JoyceArguments := {}
|
||||
usesCaptions: JoyceName[] := []
|
||||
for each jdep of data.split ','
|
||||
scalar := Number jdep
|
||||
if scalar is scalar // not NaN
|
||||
(args.scalar ?= []).push scalar
|
||||
continue
|
||||
if jdep is 'screen'
|
||||
// special case for Joyce; assume xOyPlane
|
||||
(args.plane ?= []).push 'xOyPlane'
|
||||
else
|
||||
unless jdep in cdata.elements
|
||||
console.warn
|
||||
`Reference to unknown geometric entity ${jdep} in ${jCom}`
|
||||
return undefined
|
||||
usesCaptions.push jdep
|
||||
{klass: depKlass, otherName: depDyn, ends} := cdata.elements[jdep]
|
||||
(args[depKlass] ?= []).push depDyn
|
||||
if depKlass is 'point'
|
||||
(args.subpoints ?= []).push depDyn
|
||||
else if depKlass is 'line'
|
||||
(args.subpoints ?= []).push ...ends ?? []
|
||||
return {index, name, jname, klass, method, args, usesCaptions, colors}
|
||||
|
||||
// Parses a Joyce element-creating command, extending the elements
|
||||
// by side effect:
|
||||
function jToG(
|
||||
jCom: string,
|
||||
index: number,
|
||||
cdata: ConstructionData): Commander
|
||||
elt := parseElement jCom, index, cdata
|
||||
cmdr .= freshCommander()
|
||||
unless elt then return cmdr
|
||||
{name, jname, klass, method, usesCaptions, colors} := elt
|
||||
isPivot := !!cdata.pivot and elt.jname is pivotData[cdata.pivot].pivot
|
||||
if isPivot then pivotData[cdata.pivot].pivot = elt.name
|
||||
cmdr = classHandler[klass] elt, cdata
|
||||
unless name is jname then cmdr.callbacks.push (api: AppletObject) =>
|
||||
api.setCaption name, jname
|
||||
api.setLabelStyle name, 3 // style CAPTION = 3
|
||||
if cmdr.auxiliaries.length and not adapParams.config?.showaux
|
||||
cmdr.callbacks.push (api: AppletObject) =>
|
||||
for each aux of cmdr.auxiliaries
|
||||
api.setAuxiliary aux, true
|
||||
api.setLabelVisible aux, false
|
||||
api.setVisible aux,false
|
||||
|
||||
// set up the pivot if there is one
|
||||
if isPivot
|
||||
unless klass is 'point'
|
||||
console.warn(`Can only pivot around a point, not the ${klass}`
|
||||
+ `named ${jname}. Ignoring.`)
|
||||
cdata.pivot = ''
|
||||
cmdr.commands.push(`${cdata.pivot} = `
|
||||
+ `Slider(0°,360°,1°,1,${cdata.width/3},true,true,false,false)`)
|
||||
cmdr.callbacks.push (api: AppletObject) =>
|
||||
api.setCaption cdata.pivot, 'Rotate Display'
|
||||
api.setLabelStyle cdata.pivot, 3
|
||||
api.setCoords(cdata.pivot, 2*cdata.width/3, cdata.height-10)
|
||||
// Not sure how to let TypeScript deal with putting a new function
|
||||
// on the global window object, so punting at least for now:
|
||||
// @ts-ignore
|
||||
window.pivotListener = pivotListener
|
||||
api.registerObjectUpdateListener(cdata.pivot, 'pivotListener')
|
||||
|
||||
// Create callback to assign colors
|
||||
traceC := adapParams.config?.color
|
||||
console.log 'Considering coloring', name, 'with', colors if traceC
|
||||
if colors.length is 4 and colors.every (color) => invisible color
|
||||
cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false
|
||||
else // we have to decorate
|
||||
dimension .= cmdr.parts.findLastIndex .includes name
|
||||
cmdr.callbacks.push (api: AppletObject, parts: DimParts) =>
|
||||
// Operate in order faces, lines, point, caption so that
|
||||
// we can adjust components after setting overall color, etc.
|
||||
|
||||
// Color the "Faces"; they default to 'brighter':
|
||||
if invisible(colors[3]) and (klass !== 'sphere' or invisible colors[2])
|
||||
for each face of parts[2]
|
||||
if face is name
|
||||
console.log 'Fading out interior of', face if traceC
|
||||
// hide the interior by making it transparent
|
||||
api.setFilling face, 0
|
||||
else if face not in cdata.elements
|
||||
console.log 'Hiding face', face if traceC
|
||||
api.setVisible face, false
|
||||
else
|
||||
surface .= colors[3]
|
||||
if klass is 'sphere' and invisible surface
|
||||
surface = colors[2] // for Joyce, spheres had one circular "edge"
|
||||
faceRGB := joyce2rgb(surface or 'brighter', cdata.bg)
|
||||
deep := ['circle', 'polygon', 'sector']
|
||||
filling := deep.includes(klass) ? 0.7 : 0.2
|
||||
for each face of parts[2]
|
||||
if traceC
|
||||
console.log 'Coloring face', face, 'to',
|
||||
surface, '=', faceRGB
|
||||
api.setVisible face, true
|
||||
api.setFilling face, filling
|
||||
api.setColor face, ...faceRGB
|
||||
|
||||
// Lines default to black:
|
||||
if invisible colors[2]
|
||||
for each line of parts[1]
|
||||
if line is name or line not in cdata.elements
|
||||
console.log 'Hiding line', line if traceC
|
||||
api.setVisible line, false
|
||||
else
|
||||
lineRGB := joyce2rgb(colors[2] or 'black', cdata.bg)
|
||||
if traceC
|
||||
console.log 'Need to color lines', parts[1], 'with', lineRGB
|
||||
for each line of parts[1]
|
||||
console.log 'Coloring line', line, 'to', colors[2] if traceC
|
||||
api.setVisible line, true
|
||||
api.setColor line, ...lineRGB
|
||||
|
||||
// Now color the points:
|
||||
if traceC
|
||||
console.log
|
||||
'Considering point colors for', name, 'of dimension', dimension
|
||||
if invisible colors[1]
|
||||
// Hide all the dim-0 elements that are not distinct independent
|
||||
// items:
|
||||
for each point of parts[0]
|
||||
if point is name or point not in cdata.elements
|
||||
console.log 'Hiding point', point if traceC
|
||||
api.setVisible point, false
|
||||
else if dimension is 0 or colors[1] // Need to color the points
|
||||
if not colors[1]
|
||||
colors[1] = pointDefaultColorName name, method, isPivot
|
||||
ptRGB := joyce2rgb colors[1], cdata.bg
|
||||
for each point of parts[0]
|
||||
console.log 'Coloring point', point, 'to', colors[1] if traceC
|
||||
api.setVisible point, true
|
||||
api.setColor point, ...ptRGB
|
||||
|
||||
// Make the caption the correct color
|
||||
if invisible colors[0]
|
||||
console.log 'Hiding label', name if traceC
|
||||
api.setLabelVisible name, false
|
||||
else if colors[0]
|
||||
if colors[dimension+1] and colors[dimension+1] is not colors[0]
|
||||
// Have to make a text to provide the caption, since GeoGebra
|
||||
// doesn't allow caption different color from entity.
|
||||
textName := 'GeoText' + index
|
||||
locationExpr := switch klass
|
||||
when 'point' then `1.02${name}`
|
||||
when 'line'
|
||||
(`Midpoint(${name})`
|
||||
+ ` + Rotate(Direction(${name})`
|
||||
+ `*Length(${name})*0.025/Length(Direction(${name})),`
|
||||
+ `pi/2)`)
|
||||
when 'circle'
|
||||
(`Center(${name})`
|
||||
+ ` + Radius(${name})*Vector((12/13,5/13))*1.03`)
|
||||
when 'polygon' then `Centroid(${name})`
|
||||
when 'sector'
|
||||
(`(5*Center(${name})`
|
||||
+ ` - ${cmdr.ends?[0]} - ${cmdr.ends?[1]})/3`)
|
||||
when 'plane'
|
||||
`Intersect(${name}, PerpendicularLine((0, 0, 0), ${name}))`
|
||||
when 'sphere'
|
||||
(`Center(${name})`
|
||||
+ ` + Radius(${name})*Vector((12/13,0,5/13))*1.03`)
|
||||
when 'polyhedron'
|
||||
// The "ends" are faces or vertices roughly opposite
|
||||
// from each other
|
||||
[ex1, ex2] .= cmdr.ends ?? ['', '']
|
||||
unless parts[0].includes ex1 then ex1 = `Centroid(${ex1})`
|
||||
unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})`
|
||||
`(4*${ex1}+${ex2})/5`
|
||||
textCmd := `${textName} = Text("${jname}", ${locationExpr})`
|
||||
textCol := joyce2rgb colors[0], cdata.bg
|
||||
if traceC
|
||||
console.log `Making text '${textCmd}' colored`, textCol
|
||||
api.evalCommand textCmd
|
||||
api.setColor textName, ...textCol
|
||||
// and hide the underlying GeoGebra label
|
||||
api.setLabelVisible name, false
|
||||
else // specified label color matches the entity color
|
||||
// So label gets the correct color from element
|
||||
// but we had better make sure it is visible:
|
||||
console.log 'Showing label', name if traceC
|
||||
api.setLabelVisible name, true
|
||||
else // label color is defaulting
|
||||
// Make it same as the element for points, invisible otherwise:
|
||||
show := klass is 'point'
|
||||
console.log 'Setting label vis of', name, 'to', show if traceC
|
||||
api.setLabelVisible name, show
|
||||
|
||||
cdata.elements[jname] =
|
||||
{otherName: name, usesCaptions, klass, cmdr.ends, cmdr.parts}
|
||||
if name !== jname
|
||||
cdata.elements[name] =
|
||||
{otherName: jname, usesCaptions, klass, cmdr.ends, cmdr.parts}
|
||||
cmdr
|
||||
|
||||
// Helper for dividing an angle
|
||||
function makeAngDiv(
|
||||
method:string,
|
||||
|
@ -1303,3 +1385,93 @@ function makeLinesInvisible(callbacks: GeogebraCallback[], name: string)
|
|||
// 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
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// JSXGraph translation below this line, up to the actual actions of script
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
type AuxiliaryData
|
||||
jsxElement?: JXG.GeometryElement
|
||||
auxiliaries: JXG.GeometryElement[]
|
||||
ends?: [DyName, DyName]
|
||||
parts: [DyName[], DyName[], DyName[]]
|
||||
|
||||
function freshAuxEndsParts(): AuxiliaryData
|
||||
auxiliaries: []
|
||||
parts: [[], [], []]
|
||||
|
||||
// Execute the commands needed to create the element encoded by jCom
|
||||
// against the given api entity
|
||||
function jAsJSX(
|
||||
jCom: string,
|
||||
index: number,
|
||||
api: JXG.Board | JXG.View3D,
|
||||
cdata: ConstructionData)
|
||||
elt := parseElement jCom, index, cdata
|
||||
unless elt then return
|
||||
{jsxElement, auxiliaries, ends, parts} :=
|
||||
jsxHandler[elt.klass] elt, api, cdata
|
||||
unless jsxElement then return
|
||||
cdata.elements[elt.name] = {
|
||||
otherName: elt.jname,
|
||||
jsxElement, elt.usesCaptions, elt.klass, ends, parts}
|
||||
if elt.name !== elt.jname
|
||||
cdata.elements[elt.jname] =
|
||||
Object.assign {}, cdata.elements[elt.name], {otherName: elt.name}
|
||||
|
||||
type JSXHandler = (
|
||||
elt: Element,
|
||||
api: JXG.Board | JXG.View3D,
|
||||
cdata: ConstructionData) => AuxiliaryData
|
||||
|
||||
// Helper function to deal with TypeScript typing of JSX classes
|
||||
type JSX_APIS
|
||||
jsx2d?: JXG.Board
|
||||
jsx3d?: JXG.View3D
|
||||
|
||||
function getJSXapi(
|
||||
api: JXG.Board | JXG.View3D,
|
||||
cdata: ConstructionData): JSX_APIS
|
||||
if cdata.is3d then return {jsx3d: api as JXG.View3D}
|
||||
else return {jsx2d: api as JXG.Board}
|
||||
|
||||
jsxHandler: Record<JoyceClass, JSXHandler> :=
|
||||
point: (elt, api, cdata) =>
|
||||
return .= freshAuxEndsParts()
|
||||
{jsxElement, auxiliaries, ends, parts} .= return.value
|
||||
parts[0].push elt.name
|
||||
{jname, args} := elt
|
||||
{jsx2d, jsx3d} := getJSXapi api, cdata
|
||||
// FIXME: put Joyce hacks here
|
||||
switch elt.method
|
||||
'free'
|
||||
unless args.scalar then return
|
||||
coords := vertFlipped args.scalar, cdata
|
||||
// FIXME: Is something needed here for supporting pivot?
|
||||
if jsx3d
|
||||
jsx3d.create 'point3d', coords, {name: elt.jname}
|
||||
// FIXME: Handle 2d
|
||||
return.value = {jsxElement, auxiliaries, ends, parts}
|
||||
line: (elt, api, cdata) => freshAuxEndsParts()
|
||||
polygon: (elt, api, cdata) => freshAuxEndsParts()
|
||||
circle: (elt, api, cdata) => freshAuxEndsParts()
|
||||
sector: (elt, api, cdata) => freshAuxEndsParts()
|
||||
plane: (elt, api, cdata) => freshAuxEndsParts()
|
||||
polyhedron: (elt, api, cdata) => freshAuxEndsParts()
|
||||
sphere: (elt, api, cdata) => freshAuxEndsParts()
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// The actual actions of the script follow:
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// Always use the final joyceApplets if there are any:
|
||||
if joyceApplets.length
|
||||
adapParams.joyceApplets = joyceApplets
|
||||
|
||||
if adapParams.joyceApplets.length
|
||||
if adapParams.loader
|
||||
jQuery.getScript adapParams.loader, =>
|
||||
postApplets adapParams.joyceApplets
|
||||
else
|
||||
postApplets adapParams.joyceApplets, adapParams.codebase
|
||||
|
|
|
@ -27,9 +27,9 @@ export function generateVRML(n: number, d: number, chk: boolean[])
|
|||
unless 1 <= d < n
|
||||
return err: 'Denominator should be strictly between 1 and numerator.'
|
||||
if d > n/2 then d = n-d // equivalent
|
||||
which := chk.findIndex (&)
|
||||
which := chk.findIndex &
|
||||
chk[which] = false
|
||||
if chk.some (&) return err: 'Currently only one shape option may be checked.'
|
||||
if chk.some & return err: 'Currently only one shape option may be checked.'
|
||||
name .= d > 1 ? `${n}/${d}-gonal` :
|
||||
switch n
|
||||
when 3: 'triangular'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue