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] // 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] else 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 // 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 api.setCoordSystem -10, 10 + jApp.width, -10, 10 + jApp.height, -depth - 10, depth + 10, false 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) => 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 corner := cdata.is3d ? 'Corner(-1,1)' : 'Corner(1,1)' api.evalCommand `TitlePoint = ${corner} 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 := parseFloat jdep if scalar is scalar // not NaN (args.scalar ?= []).push scalar continue 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 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] 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 faceRGB := joyce2rgb(colors[3] or 'brighter', cdata.bg) deep := ['circle', 'polygon', 'sector'] filling := deep.includes(klass) ? 0.7 : 0.2 for each face of parts[2] console.log 'Coloring face', face, 'to', colors[3] if traceC 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) 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.02, 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} cdata.elements[name] = {otherName: jname, usesCaptions, klass, cmdr.ends} 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(40).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.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): Commander => return := freshCommander() {commands, callbacks, parts, auxiliaries} := return.value zeroVector := cdata.is3d ? 'Vector((0,0,0))' : 'Vector((0,0))' aux := name + 'aUx' pivotable := cdata.pivot and name !== pivotData[cdata.pivot].pivot parts[0].push name 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) /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 // HACK: Special-case correction for Joyce Elements Bk II, prop 14 index .= 0 if name === 'H' and args.line and args.line[0] === '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]})` 'intersection' // Checking Joyce source, means intersection of lines, not // intersection of line segments pt := args.subpoints unless pt then return l1 := `Line(${pt[0]},${pt[1]})` e2 := args.plane ? 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) 'midpoint' if args.line commands.push `${name} = Midpoint(${args.line[0]})` else commands.push `${name} = Midpoint(${args.point?[0]},${args.point?[1]})` 'parallelogram' pt := args.subpoints unless pt then return commands.push `${name} = ${pt[0]} + ${pt[2]} - ${pt[1]}` 'perpendicular' // Note only the two-point option implemented so far unless args.subpoints return [center, direction] := args.subpoints commands.push `${name} = Rotate(${direction}, pi/2, ${center})` /proportion|similar/ [source, displacement] := proportionSimilar method, args, cdata, aux, commands, auxiliaries unless source then return commands.push `${name} = Translate(${source}, ${displacement})` 'vertex' commands.push `${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})` 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 ...[1..4].map (n) => aux + n '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 ...[1..4].map (n) => aux + n '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 return // TODO: line perpendicular to plane 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) => 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 ','})` 'radius' pt := args.subpoints unless pt then return inPlane := args.plane ? `, ${args.plane[0]}` : '' switch pt.length when 2 [center, point] := pt ends[0] = center circle = `Circle(${center}, ${point}${inPlane})` when 3 center := pt[0] ends[0] = center radius := `Distance(${pt[1]}, ${pt[2]})` circle = `Circle(${center}, ${radius}${inPlane})` commands.push `${aux} = ${circle}` // for the filling `${name} = ${circle}` // for the perimeter parts[2].push aux makeLinesInvisible callbacks, aux callbacks.push (api: AppletObject) => api.setLabelVisible name, true polygon: (name, method, args, index, cdata) => return := freshCommander() {commands, callbacks, parts, auxiliaries} := return.value parts[2].push name aux := name + 'aUx' 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] callbacks.push (api: AppletObject, moreParts: DimParts) => made:= api.evalCommandGetLabels `${name} = Polygon(${pt[0]},${pt[1]}, ${N})` 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 /triangle|similar|parallelogram|application|quadrilateral|octagon/ unless args.subpoints then return pt .= args.subpoints if method is 'parallelogram' unless pt.length is 3 then return commands.push `${aux} = ${pt[0]} + ${pt[2]} - ${pt[1]}` auxiliaries.push aux pt = [...pt, aux] else if method is 'application' unless pt.length is 3 then return unless args.polygon?.length is 1 then return direction := `UnitVector(${pt[2]} - ${pt[0]})` angle := `Angle(${pt[1]},${pt[0]},${pt[2]})` length := `Area(${args.polygon})` + `/(Distance(${pt[0]},${pt[1]})*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 ...[1..3].map (n) => aux + n pt = [pt[0], pt[1], aux+3] else commands.push '' parts[0].push ...pt callbacks.push (api: AppletObject, moreParts: DimParts) => made := api.evalCommandGetLabels `${name} = Polygon(${pt.join ','})` 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) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value aux := name + 'aUx' parts[2].push name 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' ends[0] = start ends[1] = end commands.push `${name} = ${prefix}Sector(${parms})` `${aux}1 = ${prefix}Arc(${parms})` parts[1].push aux + 1 makeLinesInvisible callbacks, name plane: (name, method, args) => 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 ','})` '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) => freshCommander() polyhedron: (name, method, args, index) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value aux := name + 'aUx' switch method 'tetrahedron' pt := args.subpoints unless pt and pt.length is 4 then return commands.push '' // hack, make sure there is a command parts[0].push ...pt ends[0] = aux + 1 ends[1] = pt[3] callbacks.push (api: AppletObject, moreParts: DimParts) => 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 made := api.evalCommandGetLabels `${name} = Pyramid(${aux}1, ${pt[3]})` 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 ...[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