From a4f3a96d6e961a74824b49a03d7f38a930b5dd4a Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 8 Feb 2024 17:45:26 -0800 Subject: [PATCH] feat: Improve 3D operation Working toward successfully rendering Book Eleven. Specific changes in this commit include: * Improvement to coordinate system/initial view for 3D constructions * More attention to ambient plane for some 2D commands * Add sphereSlider construction method for point * Add intersection construction method for circle (intersection of two spheres) * Fix display/non-display of perimeter of sectors * Add sphere construction by radius * Add polyhedron construction methods parallelepiped and pyramid --- src/adapptext.civet | 3 +- src/adapptlet.civet | 145 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 116 insertions(+), 32 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..bef45b1 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 @@ -128,11 +128,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 @@ -207,8 +208,7 @@ 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} + api.evalCommand `TitlePoint = Corner(1,1) Text("${value}", TitlePoint + (2,5))` 'pivot' return // already handled in postApplets @@ -485,7 +485,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 @@ -572,6 +572,7 @@ classHandler: Record := 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 @@ -684,13 +685,22 @@ classHandler: Record := 'perpendicular' // Note only the two-point option implemented so far unless args.subpoints return + inPlane := args.plane ? `,${args.plane[0]}` : defaultPlane [center, direction] := args.subpoints - commands.push `${name} = Rotate(${direction}, pi/2, ${center})` + commands.push + `${name} = Rotate(${direction}, pi/2, ${center}${inPlane})` /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]})` @@ -836,22 +846,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 @@ -949,12 +964,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 @@ -971,9 +988,10 @@ classHandler: Record := 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) => @@ -990,32 +1008,97 @@ classHandler: Record := 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 + // 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]}` + for i of [4..7] + auxiliaries.push aux+i + pt = [...pt, ...auxiliaries] + parts[0].push ...pt + generalRecipe := + A: [0,1,4,2] + B: [0,1,5,3] + C: [0,2,6,3] + D: [3,5,7,6] + E: [2,4,7,6] + F: [1,4,7,5] + letters := ['A'..'F'] as const + recipe: Record := {} + for ltr of letters + auxlet := aux + ltr + auxiliaries.push auxlet + parts[2].push auxlet + recipe[auxlet] = (pt[i] for each i of generalRecipe[ltr]) + ends[0] = aux + 'A' + ends[1] = aux + 'D' + callbacks.push (api: AppletObject, moreParts: DimParts) => + ix .= 0 + for piece in recipe + madeIt := api.evalCommandGetLabels + `${piece} = Polygon(${recipe[piece].join ','})` + if not madeIt return + for each obj of madeIt.split ',' + if obj is piece continue + newObj := 'GeoAux' + index + obj + ix + api.renameObject obj, newObj + moreParts[1].push newObj + ix += 1 + /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 - ends[0] = aux + 1 - ends[1] = pt[3] + parts[2].push ends[0] + ends[1] = pt.at(-1) or '' 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 + 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