From 277d9b0a8cc276306a081aa73810a2ea80ab0b21 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 18 Oct 2023 01:07:58 +0000 Subject: [PATCH] feat: Get all of Rostamian's old pages working (#44) Implements the pivot parameter to the Geometry applet and numerous new construction methods. Resolves #36. Reviewed-on: https://code.studioinfinity.org/glen/archematics/pulls/44 Co-authored-by: Glen Whitney Co-committed-by: Glen Whitney --- etc/deps/geotypes/api.ts | 2 +- etc/options.html | 6 +- src/adapptext.civet | 2 +- src/adapptlet.civet | 415 +++++++++++++++++++++++++++++---------- src/adapptypes.civet | 3 +- 5 files changed, 318 insertions(+), 110 deletions(-) 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/etc/options.html b/etc/options.html index e618cf5..b512c0e 100644 --- a/etc/options.html +++ b/etc/options.html @@ -18,8 +18,12 @@ Alter execution of the translated applet:

- + +
+
+ + diff --git a/src/adapptext.civet b/src/adapptext.civet index 8b0be59..5138235 100644 --- a/src/adapptext.civet +++ b/src/adapptext.civet @@ -64,7 +64,7 @@ document.addEventListener "DOMContentLoaded", async => use3d = true break codebase .= browser.runtime.getURL 'deps/GeoGebra/HTML5/5.0/' - codebase += use3d ? 'web3d' : 'webSimple' + codebase += (use3d or config.algebra) ? 'web3d' : 'webSimple' adapParams: AdapParams := {codebase, config, joyceApplets } apars := JSON.stringify(adapParams) addScriptTag(browser.runtime.getURL 'deps/GeoGebra/deployggb.js').then => diff --git a/src/adapptlet.civet b/src/adapptlet.civet index 010e0fd..7ae1262 100644 --- a/src/adapptlet.civet +++ b/src/adapptlet.civet @@ -35,6 +35,42 @@ type Description // 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 + +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 := { @@ -44,11 +80,9 @@ 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 - worked := api.enable3D true + is3d := contains3d jApp.params + if is3d + api.enable3D true api.setPerspective 'T' // Get rid of the xy-plane indicator xml .= api.getXML() @@ -56,10 +90,26 @@ function postApplets(jApplets: AppletDescription[], codebase = '') 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: [], 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 @@ -78,22 +128,6 @@ function postApplets(jApplets: AppletDescription[], codebase = '') if codebase then geoApp.setHTML5Codebase codebase geoApp.inject jApp.id -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 - -console.log 'In script', document.currentScript, adapptScript - -adapParams: AdapParams := - typeof GGBApplet is 'undefined' - ? {loader: 'https://www.geogebra.org/apps/deployggb.js', joyceApplets: []} - : JSON.parse(adapptScript.dataset.params ?? '') as AdapParams - // Always use the final joyceApplets if there are any: if joyceApplets.length adapParams.joyceApplets = joyceApplets @@ -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 @@ -225,7 +257,27 @@ function jToG( 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 if colors.length is 4 and colors.every (color) => invisible color cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false @@ -243,11 +295,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 +311,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 +344,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 +369,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 +389,33 @@ 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 + 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' @@ -397,8 +469,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,19 +478,39 @@ 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); return += '1' while return.value in elements +// Helper for similar 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] + // 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 := 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)/ @@ -433,30 +525,41 @@ 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)` `${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center}${inPlane})` `${name} = Intersect(${destination}, Ray(${center}, ${aux}4))` auxiliaries.push ...[2..4].map (i) => `${aux}${i}` - 'extend' - sp := args.subpoints - unless sp then return - direction .= `UnitVector(Vector(${sp[0]},${sp[1]}))` - if args.line and ( - not args.point or args.point[0] !== sp[0]) - direction = `UnitVector(${args.line[0]})` - displacement := `Distance(${sp[2]}, ${sp[3]})*${direction}` - commands.push `${name} = Translate(${sp[1]}, ${displacement})` + 'circleSlider' + unless args.circle then return + 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 + /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 commands.push `${name} = ${args.subpoints[0]}` /fixed|free/ coords := args.scalar unless coords then return - commands.push `${name} = (${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 @@ -476,7 +579,8 @@ classHandler: Record := commands.push `${name} = Intersect(${l1},${e2})` 'last' unless args.subpoints then return - commands.push `${name} = ${args.subpoints.at(-1)}` + commands.push + `${name} = ${args.subpoints.at(-1)}` 'lineSegmentSlider' segment .= args.line?[0] unless segment @@ -500,9 +604,26 @@ classHandler: Record := [center, direction] := args.subpoints // Note clockwise 90° rotation (3π/2) confirmed in Joyce source commands.push `${name} = Rotate(${direction}, 3*pi/2, ${center})` - 'proportion' - pt := args.subpoints + /proportion|similar/ + pt .= args.subpoints unless pt then return + // reduce the similar case to general proportion + if method is 'similar' + unless pt.length is 5 then return + sourcePlane .= '' + destPlane .= '' + if cdata.is3d + unless args.plane then return + destPlane = `, ${args.plane[0]}` + if args.plane.length > 1 + sourcePlane = `, ${args.plane[1]}` + else + sourcePlane = `, Plane(${pt[2]}, ${pt[3]}, ${pt[4]})` + angle := `Angle(${pt[3]}, ${pt[2]}, ${pt[4]}${sourcePlane})` + commands.push + `${aux} = Rotate(${pt[1]}, ${angle}, ${pt[0]}${destPlane})` + auxiliaries.push aux + pt = [pt[2], pt[3], pt[2], pt[4], pt[0], pt[1], pt[0], aux] len := `Distance(${pt[2]},${pt[3]})*Distance(${pt[4]},${pt[5]})` + `/ Distance(${pt[0]},${pt[1]})` direction := `UnitVector(Vector(${pt[6]}, ${pt[7]}))` @@ -511,7 +632,7 @@ classHandler: Record := commands.push `${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})` - line: (name, method, args) => + line: (name, method, args, index, cdata) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value @@ -519,10 +640,62 @@ classHandler: Record := parts[1].push name madeSegment .= false switch method + '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 counterclockwise 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}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 + '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 'parallel' unless args.subpoints then return [newStart, oldStart, oldEnd] := args.subpoints @@ -538,24 +711,41 @@ classHandler: Record := madeSegment = true else commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)` - 'chord' - // To match Joyce, we need to get the ordering here correct. - // ends[0] should be the one closer to args.subpoints[0] - unless args.subpoints and args.circle then return - line := `Line(${args.subpoints.join ','})` - pt := args.subpoints[0] - commands.push ...[1..2].map (n) => - `${aux}${n} = Intersect(${args.circle}, ${line}, ${n})` - condition := `Distance(${aux}2,${pt}) < Distance(${aux}1,${pt})` - // NOTE: Joyce's code has special case for when pt is almost - // at midpoint of chord; in that case, it starts at endpoint - // closer to the second subpoint... postponing that nicety - 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 + '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 + if args.line?.length is 1 + ln := args.line[0] + commands.push + `${name} = Rotate(${ln}, 3*pi/2, ${pt[0]}${inPlane})` + `${aux} = Vertex(${name}, 2)` + madeSegment = true + else + commands.push + `${aux} = Rotate(${pt[1]}, 3*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 + unless madeSegment commands.push `${name} = Segment(${ends[0]},${ends[1]})` callbacks.push (api: AppletObject) => api.setLabelVisible name, true @@ -563,14 +753,27 @@ 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 'radius' - unless args.subpoints then return - [center, point] := args.subpoints - commands.push `${name} = Circle(${center}, ${point})` + pt := args.subpoints + unless pt then return + inPlane := args.plane ? `, ${args.plane[0]}` : '' + 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})` callbacks.push (api: AppletObject) => api.setLabelVisible name, true polygon: (name, method, args, index) => diff --git a/src/adapptypes.civet b/src/adapptypes.civet index 5abf456..72d28ea 100644 --- a/src/adapptypes.civet +++ b/src/adapptypes.civet @@ -1,4 +1,5 @@ -export const flags = ['color', 'commands', 'showall', 'showaux'] as const +export const flags = [ + 'color', 'commands', 'showall', 'showaux', 'algebra'] as const export type FlagType = (typeof flags)[number] export type ConfigType = Partial>