diff --git a/etc/deps/geotypes/api.ts b/etc/deps/geotypes/api.ts index 859b5c7..a0d6917 100644 --- a/etc/deps/geotypes/api.ts +++ b/etc/deps/geotypes/api.ts @@ -101,7 +101,7 @@ export interface AppletObject { getXcoord(objName: string): number; getYcoord(objName: string): number; getZcoord(objName: string): number; - setCoords(objName: string, x: number, y: number, z: number): void; + setCoords(objName: string, x: number, y: number, z?: number): void; getValue(objName: string): number; getVersion(): string; getScreenshotBase64(callback: (data: string) => void, scale?: number): void; diff --git a/src/adapptlet.civet b/src/adapptlet.civet index a08a6a2..2542a4b 100644 --- a/src/adapptlet.civet +++ b/src/adapptlet.civet @@ -49,6 +49,28 @@ adapParams: AdapParams := ? {loader: 'https://www.geogebra.org/apps/deployggb.js', joyceApplets: []} : JSON.parse(adapptScript.dataset.params ?? '') as AdapParams +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[] + fixed: Record + +pivotData: Record := {} + function postApplets(jApplets: AppletDescription[], codebase = '') for each jApp of jApplets params := { @@ -58,10 +80,8 @@ function postApplets(jApplets: AppletDescription[], codebase = '') jApp.width, jApp.height, appletOnLoad: (api: AppletObject) => - elements: JoyceElements := {} - backgroundRGB := [255, 255, 255] as RGB - config3d := contains3d jApp.params - if config3d + is3d := contains3d jApp.params + if is3d api.enable3D true api.setPerspective 'T' // Get rid of the xy-plane indicator @@ -72,10 +92,24 @@ 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: [], 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, elements, backgroundRGB, config3d - if config3d + api, name, value, cdata + if is3d depth .= jApp.width if jApp.height > depth then depth = jApp.height api.setCoordSystem @@ -135,7 +169,7 @@ type ClassHandler = ( method: string, args: JoyceArguments, index: number, - is3d: boolean) => Commander + cdata: ConstructionData) => Commander type RGB = [number, number, number] type XYZ = RGB @@ -146,28 +180,26 @@ function dispatchJcommand( api: AppletObject, name: string, value: string, - elements: JoyceElements - backgroundRGB: RGB, - is3d: boolean): void + cdata: ConstructionData): void switch name 'background' - newback := joyce2rgb value, backgroundRGB + cdata.bg = joyce2rgb value, cdata.bg if adapParams.config?.commands console.log 'Setting background to', value, 'interpreted as', - newback - for i of [0..2] - backgroundRGB[i] = newback[i] - api.setGraphicsOptions 1, bgColor: colorsea(backgroundRGB).hex() + cdata.bg + api.setGraphicsOptions 1, bgColor: colorsea(cdata.bg).hex() 'title' if adapParams.config?.commands console.log 'Setting title to', value - corner := is3d ? 'Corner(-1,1)' : 'Corner(1,1)' + corner := cdata.is3d ? 'Corner(-1,1)' : 'Corner(1,1)' api.evalCommand `TitlePoint = ${corner} Text("${value}", TitlePoint + (2,5))` + 'pivot' + return // already handled in postApplets /e\[\d+\]/ num := parseInt(name.slice(2)) {commands, callbacks, parts} := - jToG value, elements, num, backgroundRGB, is3d + jToG value, num, cdata if commands.length lastTried .= 0 if commands.filter((&)).every (cmd) => @@ -180,16 +212,14 @@ function dispatchJcommand( (part of translation of '${value}') failed.` else console.warn `Could not parse command '${value}'` - else console.warn `Unkown param ${name} = ${value}` + else console.warn `Unknown param ${name} = ${value}` // Parses a Joyce element-creating command, extending the elements // by side effect: function jToG( jCom: string, - elements: JoyceElements, index: number, - backgroundRGB: RGB - is3d: boolean): Commander + cdata: ConstructionData): Commander [jname, klass, method, data, ...colors] := jCom.split ';' if adapParams.config?.commands console.log 'Defining', jname, 'as a', klass, 'constructed by', @@ -199,7 +229,9 @@ function jToG( console.warn `Unknown entity class ${klass}` return cmdr assertJoyceClass klass // shouldn't need to do that :-/ - name := if /^\p{L}\w*$/u.test jname then jname else geoname jname, elements + 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 ',' @@ -207,17 +239,17 @@ function jToG( if scalar is scalar // not NaN (args.scalar ?= []).push scalar continue - unless jdep in elements + 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} := elements[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, is3d + 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 @@ -226,6 +258,25 @@ function jToG( for each aux of cmdr.auxiliaries api.setAuxiliary aux, true 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 if colors.length is 4 and colors.every (color) => invisible color cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false @@ -243,11 +294,11 @@ function jToG( console.log 'Fading out interior of', face if trace // hide the interior by making it transparent api.setFilling face, 0 - else if face not in elements + else if face not in cdata.elements console.log 'Hiding face', face if trace api.setVisible face, false else - faceRGB := joyce2rgb(colors[3] or 'brighter', backgroundRGB) + 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] @@ -259,28 +310,30 @@ function jToG( // Lines default to black: if invisible colors[2] for each line of parts[1] - if line is name or line not in elements + if line is name or line not in cdata.elements console.log 'Hiding line', line if trace api.setVisible line, false else - lineRGB := joyce2rgb(colors[2] or 'black', backgroundRGB) + lineRGB := joyce2rgb(colors[2] or 'black', cdata.bg) for each line of parts[1] console.log 'Coloring line', line, 'to', colors[2] if trace api.setVisible line, true api.setColor line, ...lineRGB // Now color the points: - console.log 'Considering point colors for', name if trace + if trace + 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 elements + if point is name or point not in cdata.elements console.log 'Hiding point', point if trace api.setVisible point, false else if dimension is 0 or colors[1] // Need to color the points - ptRGB := colors[1] ? joyce2rgb colors[1], backgroundRGB - : pointDefaultRGB name, method + ptRGB := colors[1] ? joyce2rgb colors[1], cdata.bg + : pointDefaultRGB name, method, isPivot for each point of parts[0] console.log 'Coloring point', point, 'to', colors[1] if trace api.setVisible point, true @@ -290,12 +343,12 @@ function jToG( if invisible colors[0] console.log 'Hiding label', name if trace api.setLabelVisible name, false - else if colors[dimension] and colors[dimension] is not colors[0] + else 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 'point' then `1.02${name}` when 'line' (`Midpoint(${name}) + ` + `Rotate(Direction(${name})*Length(${name})*0.02, pi/2)`) @@ -315,8 +368,12 @@ function jToG( unless parts[0].includes ex1 then ex1 = `Centroid(${ex1})` unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})` `(4*${ex1}+${ex2})/5` - api.evalCommand `${textName} = Text("${jname}", ${locationExpr})` - api.setColor textName, ...joyce2rgb colors[0], backgroundRGB + textCmd := `${textName} = Text("${jname}", ${locationExpr})` + textCol := joyce2rgb colors[0], cdata.bg + if trace + 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 if colors[0] @@ -331,19 +388,35 @@ function jToG( console.log 'Setting label vis of', name, 'to', show if trace api.setLabelVisible name, show -// window[hideListener] = (arg) => -// api.setVisible name, false -// api.registerObjectUpdateListener name, hideListener - if cmdr.ends // line or sector - elements[jname] = - {otherName: name, usesCaptions, klass, cmdr.ends} - elements[name] = - {otherName: jname, usesCaptions, klass, cmdr.ends} - else // any other geometry - elements[jname] = {otherName: name, usesCaptions, klass} - elements[name] = {otherName: jname, usesCaptions, klass} + 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 + console.log 'Unfixing', anchor, relX[anchor], relY[anchor] + 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] + console.log 'Re-fixing', anchor, rX, rY + pd.api.setFixed anchor, true + function invisible(cname: string): boolean if adapParams.config?.showall then return false cname is '0' or cname is 'none' @@ -397,8 +470,8 @@ function joyce2rgb(cname: string, backgroundRGB?: RGB): RGB console.warn 'Could not parse color:', cname [128, 128, 128] -function pointDefaultRGB(name: string, method: string): RGB - // Need to short-circuit with green for pivot point, once that is implemented +function pointDefaultRGB(name: string, method: string, isPivot: boolean): RGB + if isPivot then return joyce2rgb 'green' switch method 'free' joyce2rgb 'red' @@ -406,7 +479,14 @@ function pointDefaultRGB(name: string, method: string): RGB joyce2rgb 'orange' else joyce2rgb 'black' -function geoname(jname: JoyceName, elements: JoyceElements): GeoName +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); @@ -426,11 +506,12 @@ function cutoffExtend( // All of the detailed semantics of each available command lies in this // function. classHandler: Record := - point: (name, method, args, index, is3d): Commander => + point: (name, method, args, index, cdata): Commander => return := freshCommander() {commands, callbacks, parts, auxiliaries} := return.value - zeroVector := is3d ? 'Vector((0,0,0))' : 'Vector((0,0))' + 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)/ @@ -445,7 +526,7 @@ classHandler: Record := commands.push `${destination} = Segment(${start}, ${end})` else destination = args.line[0] n := method is 'angleBisector' ? 2 : args.scalar?[0] - inPlane := is3d ? `, Plane(${start}, ${center}, ${end})` : '' + 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)` @@ -454,7 +535,12 @@ classHandler: Record := auxiliaries.push ...[2..4].map (i) => `${aux}${i}` 'circleSlider' unless args.circle then return - commands.push `${name} = Point(${args.circle[0]})` + circ := args.circle[0] + commands.push `${name} = Point(${circ})` + if (pivotable + and cdata.elements[circ].ends?[0] // center + is pivotData[cdata.pivot].pivot) + pivotData[cdata.pivot].rotatable.push name if args.scalar and args.scalar.length callbacks.push (api: AppletObject) => api.setCoords name, ...args.scalar as XYZ @@ -466,12 +552,15 @@ classHandler: Record := commands.push `${name} = Translate(${source}, ${displacement})` 'first' unless args.subpoints then return - commands.push `${name} = Point(${args.subpoints[0]},${zeroVector})` + commands.push `${name} = ${args.subpoints[0]}` /fixed|free/ coords := args.scalar unless coords then return - commands.push `${name} = Point({${coords.join ','}})` + 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 @@ -492,7 +581,7 @@ classHandler: Record := 'last' unless args.subpoints then return commands.push - `${name} = Point(${args.subpoints.at(-1)}, ${zeroVector})` + `${name} = ${args.subpoints.at(-1)}` 'lineSegmentSlider' segment .= args.line?[0] unless segment @@ -524,7 +613,7 @@ classHandler: Record := unless pt.length is 5 then return sourcePlane .= '' destPlane .= '' - if is3d + if cdata.is3d unless args.plane then return destPlane = `, ${args.plane[0]}` if args.plane.length > 1 @@ -544,7 +633,7 @@ classHandler: Record := commands.push `${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})` - line: (name, method, args, index, is3d) => + line: (name, method, args, index, cdata) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value @@ -561,7 +650,7 @@ classHandler: Record := unless cr return commands.push ...[1..2].map (n) => `${aux}${n} = Intersect(${cr[0]}, ${cr[1]}, ${n})` - inPlane := is3d ? `Plane(${cr[0]})` : '' + inPlane := cdata.is3d ? `Plane(${cr[0]})` : '' ctr := cr.map (c) => `Center(${c})` condition := (`Angle(${aux}1,${ctr[0]},${ctr[1]}${inPlane})` + `< Angle(${aux}2,${ctr[0]},${ctr[1]}${inPlane})`) @@ -630,7 +719,8 @@ classHandler: Record := circle: (name, method, args) => return := freshCommander() - {commands, callbacks, parts, auxiliaries} := return.value + return.value.ends = ['', ''] + {commands, callbacks, parts, auxiliaries, ends} := return.value parts[2].push name parts[1].push name switch method @@ -641,10 +731,12 @@ classHandler: Record := switch pt.length when 2 [center, point] := pt + ends[0] = center commands.push `${name} = Circle(${center}, ${point}${inPlane})` when 3 center := pt[0] + ends[0] = center radius := `Distance(${pt[1]}, ${pt[2]})` commands.push `${name} = Circle(${center}, ${radius}${inPlane})`