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 joyceApplets: AppletDescription[] := [] $('applet[code="Geometry"]').before (i, html) -> id := `joyceApplet${i}` joyceApplets.push { html, params(this.children), id, width: parseInt(this.getAttribute('width') ?? '200'), height: parseInt(this.getAttribute('height') ?? '200') } `
` type Split S extends `${infer W} ${infer R}` ? (W | Split) : S classes := 'point line circle polygon sector plane sphere polyhedron' type JoyceClass = Split 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 // 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 Description otherName: AnyName usesCaptions: JoyceName[] klass: JoyceClass ends?: [GeoName, GeoName] parts?: string[][] // We put both JoyceNames and GeoNames in here, pointing to each other // with the otherName property: type JoyceElements = Record adapptScript := findAdappt() as HTMLScriptElement function findAdappt() scripts := document.querySelectorAll 'script' for scrip of scripts src := scrip.getAttribute 'src' if src and src.includes 'adapptlet' return scrip adapParams: AdapParams := typeof GGBApplet is 'undefined' ? {loader: 'https://www.geogebra.org/apps/deployggb.js', joyceApplets: []} : JSON.parse(adapptScript.dataset.params ?? '') as AdapParams // For accessing/setting the y coordinate of a set of coordinates: Y := 1 Z := 2 function vertFlipped(coords: number[], cdata: ConstructionData): XYZ coords = coords.slice() if cdata.is3d if coords[Z] then coords[Z] = -coords[Z] coords[Y] = cdata.height - coords[Y] return coords as XYZ type ConstructionData id: string bg: RGB is3d: boolean width: number height: number elements: JoyceElements pivot: string title?: string // Global data setup for pivoting (ugh, but necessary because the api // is not passed to the callback, so we have to look up the slider in // a global list): type PivotData api: AppletObject pivot: string lastAngle: number rotatable: string[] maybeRotatable: Record fixed: Record pivotData: Record := {} function postApplets(jApplets: AppletDescription[], codebase = '') for each jApp of jApplets params := { appName: 'classic', -enableRightClick, // +showMenuBar, jApp.width, jApp.height, appletOnLoad: (api: AppletObject) => is3d := contains3d jApp.params if is3d api.enable3D true api.setPerspective 'T' // Get rid of the xy-plane indicator xml .= api.getXML() xml = xml.replace /plate.show="\w+"/, 'plate show="false"' api.setXML xml else if codebase.includes 'web3d' 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} for name, value in jApp.params dispatchJcommand api, name, value, cdata if pivot pd := pivotData[pivot] for entity, circ in pd.maybeRotatable if cdata.elements[circ].ends?[0] is pd.pivot pd.rotatable.push entity if is3d depth .= jApp.width if jApp.height > depth then depth = jApp.height depth /= 8 api.setCoordSystem -10 + jApp.width/6, 10 + 4*jApp.width/6, -10 + jApp.height/6, 10 + 4*jApp.height/6, -2*depth - 10, depth + 10, true api.setAxesVisible 3, false, false, false api.setGridVisible 3, false else api.setCoordSystem -10, 10 + jApp.width, -10, 10 + jApp.height api.setAxesVisible false, false api.setGridVisible false } as const geoApp := new GGBApplet params 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 // or expressions for 0-, 1-, and 2-dimensional parts for coloring // need to pass the parts into the callbacks because sometimes the parts // are not generated until callback time type GeogebraCallback = (api: AppletObject, parts: DimParts) => void type Commander commands: string[] callbacks: GeogebraCallback[] parts: DimParts auxiliaries: GeoName[] // extra entities needed in GeoGebra ends?: [GeoName, GeoName] function freshCommander(): Commander commands: [] callbacks: [] parts: [[], [], []] auxiliaries: [] type JoyceArguments = Partial & {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 XYZ = RGB // 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, name: string, value: string, cdata: ConstructionData): void 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() '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))` '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} := 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}'` 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] newval := pd.api.getValue slider if newval is pd.lastAngle then return rotation := newval - pd.lastAngle pd.lastAngle = newval pX := pd.api.getXcoord pd.pivot pY := pd.api.getYcoord pd.pivot relX: Record := {} relY: Record := {} for each anchor of pd.rotatable relX[anchor] = pd.api.getXcoord(anchor) - pX relY[anchor] = pd.api.getYcoord(anchor) - pY pd.api.setFixed anchor, false ct := Math.cos rotation st := Math.sin rotation for each anchor of pd.rotatable rX := relX[anchor]*ct - relY[anchor]*st rY := relY[anchor]*ct + relX[anchor]*st pd.api.setCoords(anchor, rX + pX, rY + pY) if pd.fixed[anchor] pd.api.setFixed anchor, true function invisible(cname: string): boolean if adapParams.config?.showall then return false cname is '0' or cname is 'none' function joyce2rgb(cname: string, backgroundRGB?: RGB): RGB whiteRGB: RGB := [255, 255, 255] bg: RGB := backgroundRGB or whiteRGB switch cname // RGB values from Duck Duck Go search on "[COLOR] rgb" /black/i [0,0,0] /blue/i [0,0,255] /cyan/i [0,255,255] /darkgray/i [128,128,128] /^gray/i [169,169,169] /green/i [0,255,0] /lightgray/i [211,211,211] /magenta/i [255,0,255] /orange/i [255,165,0] /pink/i [255,192,203] /red/i [255,0,0] /white/i [255,255,255] /yellow/i [255,255,0] /random/i colorsea.random().lighten(30).rgb() /background/i bg /brighter/i colorsea(bg).lighten(30).rgb() /darker/i colorsea(bg).darken(20).rgb() /^[0-9A-F]{6}$/i colorsea(`#${cname}`).rgb() /^\d+,\d+,\d+$/ // HSB specification [H,S,B] := cname.split(',').map (s) => parseInt s colorsea.hsv(H, S, B).rgb() else console.warn 'Could not parse color:', cname [128, 128, 128] function pointDefaultColorName( name: string, method: string, isPivot: boolean): string if isPivot then return 'green' switch method 'free' 'red' /.*[Ss]lider$/ 'orange' else 'black' function geoname( jname: JoyceName, elements: JoyceElements, klass: JoyceClass): GeoName 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 // If it's not a point, can start with any letter: if klass !== 'point' and /^\p{L}['\w]*$/u.test jname then return jname // GeoGebra won't deal with this name, so hash it: numCode .= 0n numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname return .= 'Geo' + numCode.toString(36); return += '1' while return.value in elements // Helpers for some corresponding point/line functions: function cutoffExtend( method: string, pt: string[], point?: string[], line?: string[] ): [string, string] direction .= `UnitVector(Vector(${pt[0]},${pt[1]}))` if line and (not point or point[0] !== pt[0]) direction = `UnitVector(${line[0]})` displacement := `Distance(${pt[2]}, ${pt[3]})*${direction}` source := method is 'cutoff' ? pt[0] : pt[1] [source, displacement] 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 := point: (name, method, args, index, cdata, colors, jname): Commander => return := freshCommander() {commands, callbacks, parts, auxiliaries} := return.value zeroVector := cdata.is3d ? 'Vector((0,0,0))' : 'Vector((0,0))' defaultPlane := cdata.is3d ? ', xOyPlane' : '' aux := name + 'aUx' pivotable := cdata.pivot and name !== pivotData[cdata.pivot].pivot parts[0].push name // HACK: Special-case corrections for Joyce Elements Bk XI -- XIII if cdata.title is 'XI.4' if name is 'Z' and method is 'fixed' and args.scalar?.length is 3 method = 'perpendicular' args.subpoints = ['E','P2','A'] args.plane = ['baseplane'] colors[0] = '0' colors[1] = '0' if name is 'F' and method is 'lineSlider' and args.subpoints?[0] is 'E' args.scalar = [160,40,60] if cdata.title is 'XI.5' if name is 'A' and method is 'free' method = 'perpendicular' args.subpoints = ['B','P1','P3'] args.plane = ['xOyPlane'] commands.push 'B=(80,140)' if cdata.title is 'XII.7' if (name is 'E' and method is 'lineSlider' and args.subpoints?[1] is cdata.elements['1Y'].otherName) args.subpoints[1] = cdata.elements['1Z'].otherName if cdata.title is 'XII.8' if (name is 'M' and method is 'lineSlider' and args.subpoints?[1] is cdata.elements['1Y'].otherName) args.subpoints[1] = cdata.elements['1Z'].otherName if cdata.title is 'XII.9' if (jname is 'a' and method is 'lineSlider' and args.subpoints?[1] is cdata.elements['1Y'].otherName) args.subpoints[1] = cdata.elements['1Z'].otherName if cdata.title is 'XIII.17' // Joyce's point perpendicular to a plane is ambiguous between // two possible positions, and Geogebra's choice does not // always agree with what Joyce's code did. So we just have to flip // one of his choices in the dodecahedron if name is 'U' and method is 'extend' and args.subpoints?[0] is "U'" method = 'midpoint' args.point = ["U'", "U'"] // hacky way to make U = U' if cdata.title is 'Dodecahedron and cube' // similar issues to XIII.17 if (name is 'VAB' or name is 'VBC') and method is 'perpendicular' args.scalar = [1] if cdata.title is 'XIII.18' // Joyce just mistook where N is if name is 'N' and method is 'intersection' method = 'golden' args.subpoints = ['B', 'F'] switch method /angle(?:Bisector|Divider)/ {center, foot} := makeAngDiv(method, args, cdata, aux, auxiliaries, commands) unless foot return commands.push `${name} = ${foot}` 'center' entity := args.circle ? args.circle[0] : args.sphere?[0] if entity commands.push `${name} = Center(${entity})` entityEnds := cdata.elements[entity].ends if entityEnds and not entityEnds[0] entityEnds[0] = name // labeled the center else console.warn 'Nothing to produce center point of in', name, method, args 'circleSlider' unless args.circle then return circ := args.circle[0] commands.push `${name} = Point(${circ})` if pivotable maybeCenter := cdata.elements[circ].ends?[0] if maybeCenter is pivotData[cdata.pivot].pivot pivotData[cdata.pivot].rotatable.push name if not maybeCenter pivotData[cdata.pivot].maybeRotatable[name] = circ if args.scalar and args.scalar.length callbacks.push (api: AppletObject) => api.setCoords name, ...vertFlipped(args.scalar or [], cdata) 'circumcenter' unless args.subpoints?.length is 3 then return commands.push `${aux} = Circle(${args.subpoints.join ','})` `${name} = Center(${aux})` auxiliaries.push aux /cutoff|extend/ pt := args.subpoints unless pt and pt.length is 4 then return [source, displacement] := cutoffExtend method, pt, args.point, args.line commands.push `${name} = Translate(${source}, ${displacement})` 'first' unless args.subpoints then return index .= 0 // HACK: Special-case correction for Joyce Elements Bk II, prop 14 if cdata.title is 'II.14' and name is 'H' and args.line?[0] is 'HH2' index = 1 commands.push `${name} = ${args.subpoints[index]}` /fixed|free/ unless args.scalar then return coords := vertFlipped(args.scalar, cdata) scoord := coords.join ',' if pivotable then pivotData[cdata.pivot].rotatable.push name commands.push `${name} = (${scoord})` if method is 'fixed' if pivotable then pivotData[cdata.pivot].fixed[name] = true callbacks.push (api: AppletObject) => api.setFixed name, true 'foot' pt := args.subpoints unless pt then return destination := args.plane ? args.plane[0] : `Line(${pt[1]},${pt[2]})` commands.push `${name} = ClosestPoint(${destination}, ${pt[0]})` 'golden' // Added in this implementation to ease fix of XIII.18 pt := args.subpoints unless pt and pt.length is 2 then return commands.push `${name} = ${pt[0]} + ((sqrt(5)-1)/2)*(${pt[1]}-${pt[0]})` 'intersection' // Checking Joyce source, means intersection of lines, not // intersection of line segments pt := args.subpoints unless pt and pt.length > 1 then return l1 := `Line(${pt[0]},${pt[1]})` e2 := pt.length < 3 ? args.plane?[0] : `Line(${pt[2]},${pt[3]})` commands.push `${name} = Intersect(${l1},${e2})` 'last' unless args.subpoints then return commands.push `${name} = ${args.subpoints.at(-1)}` 'lineSegmentSlider' segment .= args.line?[0] unless segment unless args.point then return commands.push `${aux} = Segment(${args.point.join ','})` auxiliaries.push aux segment = aux commands.push `${name} = Point(${segment})` if args.scalar and args.scalar.length callbacks.push (api: AppletObject) => api.setCoords name, ...vertFlipped(args.scalar or [], cdata) '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, ...vertFlipped(args.scalar or [], cdata) 'meanProportional' pt := args.subpoints unless pt and pt.length is 6 then return direction := `UnitVector(Vector(${pt[4]},${pt[5]}))` lensq := `Distance(${pt[0]},${pt[1]})*Distance(${pt[2]},${pt[3]})` disp := `sqrt(${lensq})*${direction}` commands.push `${name} = Translate(${pt[4]},${disp})` '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' pt := args.subpoints unless pt return inPlane := args.plane ? `,${args.plane[0]}` : defaultPlane center := pt[0] switch pt.length when 2 commands.push `${name} = Rotate(${pt[1]}, pi/2, ${center}${inPlane})` when 3 // perpendicular **to** the plane radius := `Distance(${pt[1]}, ${pt[2]})` which := args.scalar ? args.scalar[0] : 2 //HACK: 2 vs 1?? commands.push `${aux}1 = Sphere(${center}, ${radius})` `${aux}2 = PerpendicularLine(${center}${inPlane})` `${name} = Intersect(${aux}2,${aux}1,${which})` auxiliaries.push aux+1, aux+2 when 4 commands.push `${aux}1 = Ray(${center}, Rotate(${pt[1]}, pi/2, ${center}${inPlane}))` `${aux}2 = Circle(${center}, Distance(${pt[2]},${pt[3]})${inPlane})` `${name} = Intersect(${aux}1, ${aux}2)` auxiliaries.push aux+1, aux+2 'planeSlider' pln := args.plane?[0] unless pln then return commands.push `${name} = PointIn(${pln})` if args.scalar and args.scalar.length callbacks.push (api: AppletObject) => api.setCoords name, ...vertFlipped(args.scalar or [], cdata) /proportion|similar/ [source, displacement] := proportionSimilar method, args, cdata, aux, commands, auxiliaries unless source then return commands.push `${name} = Translate(${source}, ${displacement})` 'sphereSlider' sph := args.sphere?[0] unless sph then return commands.push `${name} = PointIn(${sph})` if args.scalar and args.scalar.length callbacks.push (api: AppletObject) => api.setCoords name, ...vertFlipped(args.scalar or [], cdata) 'vertex' commands.push `${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})` else console.warn 'Unknown point method:', method line: (name, method, args, index, cdata) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value aux := name + 'aUx' 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 // center of the other circle is clockwise in the first // circle cr := args.circle unless cr return commands.push ...[1..2].map (n) => `${aux}${n} = Intersect(${cr[0]}, ${cr[1]}, ${n})` inPlane := cdata.is3d ? `, Plane(${cr[0]})` : '' ctr := cr.map (c) => `Center(${c})` condition := (`Angle(${aux}1,${ctr[0]},${ctr[1]}${inPlane})` + `< Angle(${aux}2,${ctr[0]},${ctr[1]}${inPlane})`) commands.push `${aux}3 = If(${condition}, ${aux}1, ${aux}2)` `${aux}4 = If(${condition}, ${aux}2, ${aux}1)` ends[0] = aux + 3 ends[1] = aux + 4 auxiliaries.push aux+n for n of [1..4] 'chord' // To match Joyce, we need to get the ordering here correct. // The complicated condition about distances is modeled after // Joyce's code, but it boils down to: take the endpoint of the // chord closest to the first subpoint, unless that first // subpoint is essentially at the midpoint of the chord, in // which case start with the endpoint nearest the second subpoint. unless args.subpoints and args.circle then return // We intersect with the whole line, not just the segment: line := `Line(${args.subpoints.join ','})` pA := args.subpoints[0] pB := args.subpoints[1] commands.push ...[1..2].map (n) => `${aux}${n} = Intersect(${args.circle}, ${line}, ${n})` s := `Distance(${aux}1,${aux}2)` d := `Distance(${aux}1,${pA}) - Distance(${aux}2,${pA})` condition := (`If(${s}/10^9 < abs(${d}), ${d} > 0,` + `Distance(${aux}2,${pB}) < Distance(${aux}1,${pB}))`) commands.push `${aux}3 = If(${condition}, ${aux}2, ${aux}1)` `${aux}4 = If(${condition}, ${aux}1, ${aux}2)` ends[0] = aux + 3 ends[1] = aux + 4 auxiliaries.push aux+n for n of [1..4] 'connect' unless args.subpoints and args.subpoints.length is 2 then return ends[0] = args.subpoints[0] ends[1] = args.subpoints[1] /cutoff|extend/ pt := args.subpoints unless pt and pt.length is 4 then return [source, displacement] := cutoffExtend method, pt, args.point, args.line ends[0] = source commands.push `${aux} = Translate(${source}, ${displacement})` auxiliaries.push aux ends[1] = aux 'foot' pt := args.subpoints unless pt then return destination := args.plane ? args.plane[0] : `Line(${pt[1]},${pt[2]})` ends[0] = pt[0] commands.push `${aux} = ClosestPoint(${destination}, ${pt[0]})` auxiliaries.push aux ends[1] = aux 'parallel' unless args.subpoints then return [newStart, oldStart, oldEnd] := args.subpoints commands.push `${aux}1 = Vector(${oldStart}, ${newStart})` auxiliaries.push aux + 1, aux + 2 ends[0] = newStart ends[1] = aux + 2 if args.line?.length is 1 and args.point?[0] is args.subpoints[0] // In this case we are translating an existing segment commands.push `${name} = Translate(${args.line[0]}, ${aux}1)` `${aux}2 = Vertex(${name}, 2)` madeSegment = true else commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)` 'perpendicular' pt := args.subpoints unless pt then return inPlane .= '' if cdata.is3d unless args.plane?.length is 1 then return inPlane = `,${args.plane[0]}` switch pt.length when 2 ends[0] = pt[0] ends[1] = aux auxiliaries.push aux commands.push `${aux} = Rotate(${pt[1]}, pi/2, ${pt[0]}${inPlane})` when 3 radius := `Distance(${pt[1]}, ${pt[2]})` commands.push `${aux}1 = PerpendicularLine(${pt[1]}${inPlane})` `${aux}2 = Intersect(${aux}1${inPlane})` `${aux}3 = Circle(${aux}2, ${radius}${inPlane})` `${aux}4 = PointIn(${aux}3)` `${aux}5 = Plane(${aux}4, ${aux}1)` `${aux}6 = Rotate(${aux}4, pi/2, ${aux}2, ${aux}5)` ends[0] = aux + 2 ends[1] = aux + 6 auxiliaries.push aux+n for n of [1..6] when 4 ends[0] = pt[0] ends[1] = aux + 2 auxiliaries.push aux+1, aux+2 commands.push `${aux}1 = Rotate(${pt[1]}, 3*pi/2, ${pt[0]}${inPlane})` unitVec := `UnitVector(Vector(${pt[0]}, ${aux}1))` dist := `Distance(${pt[2]}, ${pt[3]})` commands.push `${aux}2 = Translate(${pt[0]}, ${dist}*${unitVec})` else return /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]})` callbacks.push (api: AppletObject) => api.setLabelVisible name, true parts[0].push ...ends circle: (name, method, args, index, cdata) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value 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 ','})` 'intersection' sph := args.sphere unless sph and sph.length is 2 then return circle = `IntersectConic(${sph[0]}, ${sph[1]})` 'radius' pt := args.subpoints unless pt and pt.length > 1 and pt.length < 4 then return ends[0] = pt[0] if pt.length is 2 and not cdata.is3d circle = `Circle(${pt[0]}, ${pt[1]})` else // 3d or three points radIx := pt.length - 2 inPlane := (cdata.is3d ? args.plane ? `, ${args.plane[0]}` : ', xOyPlane' : '') radius := `Distance(${pt[radIx]}, ${pt[radIx+1]})` circle = `Circle(${pt[0]}, ${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, cdata) => return := freshCommander() {commands, callbacks, parts, auxiliaries} := return.value parts[2].push name aux := geoname name + 'aUx', cdata.elements, 'point' switch method /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] inPlane := cdata.is3d ? args.plane ? `, ${args.plane[0]}` : ', xOyPlane' : '' callbacks.push (api: AppletObject, moreParts: DimParts) => command := `${name} = Polygon(${pt[0]},${pt[1]}, ${N}${inPlane})` made:= api.evalCommandGetLabels command if adapParams.config?.commands console.log 'Finishing with', command, 'producing', made if not made return for each obj of made.split ',' if obj is name continue newObj := 'GeoAux' + index + obj api.renameObject obj, newObj switch api.getObjectType newObj 'segment' moreParts[1].push newObj 'point' moreParts[0].push newObj api.setVisible newObj, false 'face' unless args.polyhedron?.length is 1 then return unless args.scalar?.length is 1 then return element := cdata.elements[args.polyhedron[0]] commands.push `${name} = ${element.parts?[2][args.scalar[0]-1]}` ///triangle|similar|parallelogram|application|quadrilateral |octagon|pentagon|hexagon/// 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]})*abs(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 aux+n for n of [1..3] pt = [pt[0], pt[1], aux+3] else commands.push '' parts[0].push ...pt callbacks.push (api: AppletObject, moreParts: DimParts) => command := `Polygon(${pt.join ','})` made := api.evalCommandGetLabels `${name} = ${command}` if adapParams.config?.commands console.log 'Finished constructing', name, 'with', command, 'producing', made if not made return for each obj of made.split ',' if obj is name continue newObj := 'GeoAux' + index + obj api.renameObject obj, newObj moreParts[1].push newObj sector: (name, method, args, index, cdata) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value aux := name + 'aUx' parts[2].push name defaultPlane := cdata.is3d ? ', xOyPlane' : '' inPlane .= args.plane ? `, ${args.plane[0]}` : defaultPlane switch method /arc|sector/ unless args.subpoints?.length is 3 return parts[0].push ...args.subpoints [center, start, end] .= args.subpoints parms .= center + ', ' + start + ', ' + end prefix .= 'Circular' if method is 'arc' temp := start start = center center = temp parms = start + ', ' + center + ', ' + end prefix = 'Circumcircular' inPlane = '' // not needed in 3-point case ends[0] = start ends[1] = end commands.push `${name} = ${prefix}Sector(${parms}${inPlane})` `${aux}1 = ${prefix}Arc(${parms}${inPlane})` parts[1].push aux + 1 auxiliaries.push aux + 1 makeLinesInvisible callbacks, name plane: (name, method, args, index, cdata) => return := freshCommander() {commands, callbacks, parts, auxiliaries} := return.value parts[2].push name switch method '3points' unless args.subpoints?.length is 3 then return commands.push `${name} = Plane(${args.subpoints.join ','})` 'parallel' unless args.subpoints?.length is 1 then return unless args.plane?.length is 1 then return commands.push `${name} = Plane(${args.subpoints[0]}, ${args.plane[0]})` 'perpendicular' unless args.subpoints?.length is 2 then return [thru, perp] := args.subpoints commands.push `${name} = PerpendicularPlane(${thru}, Line(${thru}, ${perp}))` sphere: (name, method, args) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value parts[2].push name pt := args.subpoints if method is 'radius' and pt switch pt.length when 2 [center, point] := pt ends[0] = center commands.push `${name} = Sphere(${center}, ${point})` when 3 center := pt[0] ends[0] = center radius := `Distance(${pt[1]}, ${pt[2]})` commands.push `${name} = Sphere(${center}, ${radius})` polyhedron: (name, method, args, index, cdata) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value aux := geoname name + 'aUx', cdata.elements, 'point' switch method 'parallelepiped' pt .= args.subpoints unless pt and pt.length is 4 then return // create all of the vertices we will need: commands.push `${aux}4 = ${pt[2]} + ${pt[1]} - ${pt[0]}` `${aux}5 = ${pt[3]} + ${pt[1]} - ${pt[0]}` `${aux}6 = ${pt[3]} + ${pt[2]} - ${pt[0]}` `${aux}7 = ${pt[3]} + ${pt[2]} + ${pt[1]} - 2*${pt[0]}` auxiliaries.push aux+i for i of [4..7] pt = [...pt, ...auxiliaries] parts[0].push ...pt generalRecipe := A: [0,1,4,2] B: [0,1,5,3] C: [0,2,6,3] D: [3,5,7,6] E: [2,4,7,6] F: [1,4,7,5] letters := ['A'..'F'] as const recipe: Record := {} for ltr of letters auxlet := aux + ltr auxiliaries.push auxlet parts[2].push auxlet recipe[auxlet] = (pt[i] for each i of generalRecipe[ltr]) ends[0] = aux + 'A' ends[1] = aux + 'D' callbacks.push (api: AppletObject, moreParts: DimParts) => ix .= 0 for piece in recipe madeIt := api.evalCommandGetLabels `${piece} = Polygon(${recipe[piece].join ','})` if not madeIt return for each obj of madeIt.split ',' if obj is piece continue newObj := 'GeoAux' + index + obj + ix api.renameObject obj, newObj moreParts[1].push newObj ix += 1 'prism' unless args.polygon?.length is 1 then return base := args.polygon[0] ends[0] = base pt := args.subpoints unless pt and pt.length is 2 then return commands.push `${aux}1 = Vertex(${base},1) + ${pt[1]} - ${pt[0]}` auxiliaries.push aux+1 ends[1] = aux+1 parts[0].push aux+1 parts[2].push ends[0] callbacks.push (api: AppletObject, moreParts: DimParts) => made := api.evalCommandGetLabels `${name} = Prism(${base}, ${aux}1)` if not made return for each obj of made.split ',' if obj is name continue newObj := 'GeoAux' + index + obj api.renameObject obj, newObj switch api.getObjectType newObj 'point' moreParts[0].push newObj 'segment' moreParts[1].push newObj else moreParts[2].splice 1,0,newObj /pyramid|tetrahedron/ base .= args.polygon?[0] ends[0] = base or aux + 1 pt := args.subpoints unless pt and pt.length > 0 then return // A tetrahedron is just a pyramid where we have to build the // base from three points ourselves. But it has to be done in the // callback, since we have to capture the edges. if method is 'tetrahedron' and pt.length !== 4 then return commands.push '' // hack, make sure there is a command parts[0].push ...pt parts[2].push ends[0] ends[1] = pt.at(-1) or '' callbacks.push (api: AppletObject, moreParts: DimParts) => if not base madeBase := api.evalCommandGetLabels `${ends[0]} = Polygon(${pt[0]},${pt[1]},${pt[2]})` if not madeBase return for each obj of madeBase.split ',' if obj is ends[0] continue newObj := 'GeoAux' + index + obj api.renameObject obj, newObj moreParts[1].push newObj base = ends[0] else // need to grab the parts of the base, which become // parts of the polyhedron baseParts := cdata.elements[base].parts if baseParts parts[0].push ...baseParts[0] parts[1].push ...baseParts[1] made := api.evalCommandGetLabels `${name} = Pyramid(${base}, ${ends[1]})` if not made return for each obj of made.split ',' if obj is name continue newObj := 'GeoAux' + index + obj api.renameObject obj, newObj switch api.getObjectType newObj 'segment' 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 aux+i for i of [2..4] 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