From 0b241e010ef31d3333bd602a35ead0e70d645eab Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 10 Feb 2024 07:01:20 +0000 Subject: [PATCH] feat: joyce commands needed for BookEleven (#57) Reviewed-on: https://code.studioinfinity.org/glen/archematics/pulls/57 Co-authored-by: Glen Whitney Co-committed-by: Glen Whitney --- src/adapptext.civet | 3 +- src/adapptlet.civet | 293 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 237 insertions(+), 59 deletions(-) diff --git a/src/adapptext.civet b/src/adapptext.civet index 5138235..661a250 100644 --- a/src/adapptext.civet +++ b/src/adapptext.civet @@ -13,7 +13,8 @@ obs := new MutationObserver (mutationList) => newNode := (newGenericNode as HTMLElement) newParent := (change.target as HTMLElement) unless newNode.tagName is 'APPLET' then continue - unless newNode.getAttribute('code') is 'Geometry' then continue + code := newNode.getAttribute('code') + unless code is 'Geometry' or code is 'Geometry.class' then continue id .= newParent.getAttribute 'id' unless id id = 'joyceApplet' + joyceApplets.length diff --git a/src/adapptlet.civet b/src/adapptlet.civet index 70b8e15..edec023 100644 --- a/src/adapptlet.civet +++ b/src/adapptlet.civet @@ -56,7 +56,7 @@ 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] + coords[Y] = cdata.height - coords[Y] return coords as XYZ type ConstructionData @@ -67,6 +67,7 @@ type ConstructionData 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 @@ -128,11 +129,12 @@ function postApplets(jApplets: AppletDescription[], codebase = '') if is3d depth .= jApp.width if jApp.height > depth then depth = jApp.height + depth /= 8 api.setCoordSystem - -10, 10 + jApp.width, - -10, 10 + jApp.height, - -depth - 10, depth + 10, - false + -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 @@ -185,7 +187,8 @@ type ClassHandler = ( method: string, args: JoyceArguments, index: number, - cdata: ConstructionData) => Commander + cdata: ConstructionData, + colors: string[]) => Commander type RGB = [number, number, number] type XYZ = RGB @@ -207,8 +210,8 @@ function dispatchJcommand( 'title' if adapParams.config?.commands console.log 'Setting title to', value - corner := cdata.is3d ? 'Corner(-1,1)' : 'Corner(1,1)' - api.evalCommand `TitlePoint = ${corner} + cdata.title = value + api.evalCommand `TitlePoint = Corner(1,1) Text("${value}", TitlePoint + (2,5))` 'pivot' return // already handled in postApplets @@ -257,7 +260,7 @@ function jToG( args: JoyceArguments := {} usesCaptions := [] for each jdep of data.split ',' - scalar := parseFloat jdep + scalar := Number jdep if scalar is scalar // not NaN (args.scalar ?= []).push scalar continue @@ -271,7 +274,7 @@ function jToG( (args.subpoints ?= []).push depGeo else if depKlass is 'line' (args.subpoints ?= []).push ...ends ?? [] - cmdr = classHandler[klass] name, method, args, index, cdata + cmdr = classHandler[klass] name, method, args, index, cdata, colors unless name is jname then cmdr.callbacks.push (api: AppletObject) => api.setCaption name, jname api.setLabelStyle name, 3 // style CAPTION = 3 @@ -312,23 +315,26 @@ function jToG( // 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 + 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 - faceRGB := joyce2rgb(colors[3] or 'brighter', cdata.bg) + 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', - colors[3], '=', faceRGB + surface, '=', faceRGB api.setVisible face, true api.setFilling face, filling api.setColor face, ...faceRGB @@ -485,7 +491,7 @@ function joyce2rgb(cname: string, backgroundRGB?: RGB): RGB /yellow/i [255,255,0] /random/i - colorsea.random().lighten(40).rgb() + colorsea.random().lighten(30).rgb() /background/i bg /brighter/i @@ -514,7 +520,7 @@ function pointDefaultColorName( function geoname( jname: JoyceName, elements: JoyceElements, klass: JoyceClass): GeoName - unless jname.substring(0,3) is 'Geo' // those might clash + 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: @@ -568,13 +574,30 @@ function proportionSimilar( // All of the detailed semantics of each available command lies in this // function. classHandler: Record := - point: (name, method, args, index, cdata): Commander => + point: (name, method, args, index, cdata, colors): 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 + 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)' switch method /angle(?:Bisector|Divider)/ {center, foot} := @@ -604,6 +627,12 @@ classHandler: Record := 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 @@ -612,9 +641,9 @@ classHandler: Record := 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' + // 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/ @@ -638,9 +667,9 @@ classHandler: Record := // Checking Joyce source, means intersection of lines, not // intersection of line segments pt := args.subpoints - unless pt then return + unless pt and pt.length > 1 then return l1 := `Line(${pt[0]},${pt[1]})` - e2 := args.plane ? args.plane[0] : `Line(${pt[2]},${pt[3]})` + e2 := pt.length < 3 ? args.plane?[0] : `Line(${pt[2]},${pt[3]})` commands.push `${name} = Intersect(${l1},${e2})` 'last' unless args.subpoints then return @@ -682,15 +711,49 @@ classHandler: Record := 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})` + 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 + // Uses lots of auxiliaries + radius := `Distance(${pt[1]}, ${pt[2]})` + commands.push + `${aux}1 = Circle(${center}, ${radius}${inPlane})` + `${aux}2 = PointIn(${aux}1)` + `${aux}3 = PerpendicularLine(${center}${inPlane})` + `${aux}4 = Plane(${aux}2, ${aux}3)` + `${name} = Rotate(${aux}2, pi/2, ${center}, ${aux}4)` + auxiliaries.push aux+n for n of [1..4] + 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]})` @@ -731,7 +794,7 @@ classHandler: Record := `${aux}4 = If(${condition}, ${aux}2, ${aux}1)` ends[0] = aux + 3 ends[1] = aux + 4 - auxiliaries.push ...[1..4].map (n) => aux + n + 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 @@ -755,7 +818,7 @@ classHandler: Record := `${aux}4 = If(${condition}, ${aux}1, ${aux}2)` ends[0] = aux + 3 ends[1] = aux + 4 - auxiliaries.push ...[1..4].map (n) => aux + n + 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] @@ -810,7 +873,17 @@ classHandler: Record := commands.push `${aux} = Rotate(${pt[1]}, pi/2, ${pt[0]}${inPlane})` when 3 - return // TODO: line perpendicular to plane + 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 @@ -836,22 +909,27 @@ classHandler: Record := callbacks.push (api: AppletObject) => api.setLabelVisible name, true parts[0].push ...ends - circle: (name, method, args) => + 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 .= '' + defaultPlane := cdata.is3d ? ', xOyPlane' : '' 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 then return - inPlane := args.plane ? `, ${args.plane[0]}` : '' + inPlane := args.plane ? `, ${args.plane[0]}` : defaultPlane switch pt.length when 2 [center, point] := pt @@ -931,7 +1009,7 @@ classHandler: Record := `${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 + auxiliaries.push aux+n for n of [1..3] pt = [pt[0], pt[1], aux+3] else commands.push '' @@ -949,12 +1027,14 @@ classHandler: Record := api.renameObject obj, newObj moreParts[1].push newObj - sector: (name, method, args, index) => + 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 @@ -968,12 +1048,14 @@ classHandler: Record := 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})` - `${aux}1 = ${prefix}Arc(${parms})` + `${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) => @@ -984,38 +1066,133 @@ classHandler: Record := '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) => freshCommander() - - polyhedron: (name, method, args, index) => + sphere: (name, method, args) => return := freshCommander() return.value.ends = ['', ''] {commands, callbacks, parts, auxiliaries, ends} := return.value - aux := name + 'aUx' + 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 - 'tetrahedron' - pt := args.subpoints + 'parallelepiped' + pt .= args.subpoints unless pt and pt.length is 4 then return - commands.push '' // hack, make sure there is a command + // 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 - ends[0] = aux + 1 - ends[1] = pt[3] + 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) => - 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 + 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 - moreParts[1].push newObj + switch api.getObjectType newObj + 'point' + moreParts[0].push newObj + 'segment' + moreParts[1].push newObj + else + moreParts[2].push 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] made := api.evalCommandGetLabels - `${name} = Pyramid(${aux}1, ${pt[3]})` + `${name} = Pyramid(${base}, ${ends[1]})` if not made return for each obj of made.split ',' if obj is name continue @@ -1051,7 +1228,7 @@ function makeAngDiv( `${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}` + 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: