feat: Produce an archematics plugin that works in Firefox (#38)
With this loaded in under the Firefox debugger, one can see linked WRL files and Java Geometry Applets on arbitrary web pages. This represents significant progress on #28, but getting more controls and getting it to work in other browsers is still on deck. Reviewed-on: #38 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
b31c0671d2
commit
e7361f94a7
43 changed files with 164418 additions and 102 deletions
51
src/adapptext.civet
Normal file
51
src/adapptext.civet
Normal file
|
@ -0,0 +1,51 @@
|
|||
{AppletDescription, AdapParams, params, flags, ConfigType} from ./adapptypes.ts
|
||||
|
||||
// (At least some of) Joyce's pages have inline scripts that remove their
|
||||
// own applet nodes, so we have to watch for them as they are added. Hence,
|
||||
// this script runs at document_start, and watches for applets being added,
|
||||
// setting up the joyceApplets structure as it goes
|
||||
|
||||
joyceApplets: AppletDescription[] := []
|
||||
obs := new MutationObserver (mutationList) =>
|
||||
for each change of mutationList
|
||||
for each newGenericNode of change.addedNodes
|
||||
newNode := (newGenericNode as HTMLElement)
|
||||
newParent := (change.target as HTMLElement)
|
||||
unless newNode.tagName is 'APPLET' then continue
|
||||
unless newNode.getAttribute('code') is 'Geometry' then continue
|
||||
id .= newParent.getAttribute 'id'
|
||||
unless id
|
||||
id = 'joyceApplet' + joyceApplets.length
|
||||
newParent.setAttribute 'id', id
|
||||
joyceApplets.push {
|
||||
html: newNode.outerHTML,
|
||||
params(newNode.children),
|
||||
id,
|
||||
width: parseInt(newNode.getAttribute('width') ?? '200'),
|
||||
height: parseInt(newNode.getAttribute('height') ?? '200') }
|
||||
|
||||
config := childList: true, subtree: true
|
||||
|
||||
obs.observe document.documentElement, config
|
||||
|
||||
function addScriptTag(url: string, module = false)
|
||||
return new Promise (resolve, reject) =>
|
||||
script := document.createElement 'script'
|
||||
script.addEventListener 'load', resolve
|
||||
script.addEventListener 'error', reject
|
||||
script.src = url
|
||||
if module then script.type = 'module'
|
||||
document.head.appendChild script
|
||||
|
||||
document.addEventListener "DOMContentLoaded", async =>
|
||||
finalJoyceApplet := document.querySelector "applet[code='Geometry']"
|
||||
if joyceApplets.length or finalJoyceApplet
|
||||
config := await browser.storage.local.get(flags) as ConfigType
|
||||
adapParams: AdapParams := {
|
||||
codebase: browser.runtime.getURL('deps/GeoGebra/HTML5/5.0/webSimple'),
|
||||
config,
|
||||
joyceApplets }
|
||||
// @ts-ignore
|
||||
window.wrappedJSObject.adapParams = cloneInto(adapParams, window)
|
||||
addScriptTag(browser.runtime.getURL 'deps/GeoGebra/deployggb.js').then =>
|
||||
addScriptTag browser.runtime.getURL('adapptlet.js'), true
|
|
@ -1,18 +1,12 @@
|
|||
import https://code.jquery.com/jquery-3.7.1.js
|
||||
import type {AppletObject} from ./deps/geogebra/api.ts
|
||||
import ./deps/jquery.js
|
||||
import type {AppletObject} from ./deps/geotypes/api.ts
|
||||
import {AppletDescription, AdapParams, params} from ./adapptypes.ts
|
||||
colorsea from ./deps/colorsea.js
|
||||
|
||||
type AppletDescription
|
||||
html: string
|
||||
children: HTMLCollection
|
||||
id: string
|
||||
width: number
|
||||
height: number
|
||||
|
||||
joyceApplets: AppletDescription[] := []
|
||||
$('applet[code="Geometry"]').before (i, html) ->
|
||||
id := `joyceApplet${i}`
|
||||
joyceApplets.push { html, this.children, id,
|
||||
joyceApplets.push { html, params(this.children), id,
|
||||
width: parseInt(this.getAttribute('width') ?? '200'),
|
||||
height: parseInt(this.getAttribute('height') ?? '200') }
|
||||
`<div id="${id}"></div>`
|
||||
|
@ -41,8 +35,8 @@ type Description
|
|||
// with the otherName property:
|
||||
type JoyceElements = Record<AnyName, Description>
|
||||
|
||||
jQuery.getScript 'https://www.geogebra.org/apps/deployggb.js', =>
|
||||
for each jApp of joyceApplets
|
||||
function postApplets(jApplets: AppletDescription[], codebase = '')
|
||||
for each jApp of jApplets
|
||||
params := {
|
||||
appName: 'classic',
|
||||
-enableRightClick,
|
||||
|
@ -52,15 +46,36 @@ jQuery.getScript 'https://www.geogebra.org/apps/deployggb.js', =>
|
|||
appletOnLoad: (api: AppletObject) =>
|
||||
elements: JoyceElements := {}
|
||||
backgroundRGB := [255, 255, 255] as RGB
|
||||
for child of jApp.children
|
||||
dispatchJcommand api, child, elements, backgroundRGB
|
||||
for name, value in jApp.params
|
||||
dispatchJcommand api, name, value, elements, backgroundRGB
|
||||
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
|
||||
|
||||
adapParams: AdapParams :=
|
||||
typeof GGBApplet is 'undefined'
|
||||
? {loader: 'https://www.geogebra.org/apps/deployggb.js', joyceApplets: []}
|
||||
: ((window as any).adapParams as AdapParams)
|
||||
|
||||
// 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
|
||||
|
||||
|
@ -92,42 +107,39 @@ type XYZ = RGB
|
|||
// present in that applet
|
||||
function dispatchJcommand(
|
||||
api: AppletObject,
|
||||
param: Element,
|
||||
name: string,
|
||||
value: string,
|
||||
elements: JoyceElements
|
||||
backgroundRGB: RGB): void
|
||||
val := param.getAttribute 'value'
|
||||
unless val return
|
||||
attr := param.getAttribute 'name'
|
||||
switch attr
|
||||
switch name
|
||||
'background'
|
||||
backgroundHex := `#${val}`
|
||||
api.setGraphicsOptions 1, bgColor: backgroundHex
|
||||
newback := colorsea(backgroundHex).rgb()
|
||||
newback := joyce2rgb value, backgroundRGB
|
||||
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()
|
||||
'title'
|
||||
if adapParams.config?.commands
|
||||
console.log 'Setting title to', value
|
||||
api.evalCommand `TitlePoint = Corner(1,1)
|
||||
Text("${val}", TitlePoint + (2,5))`
|
||||
Text("${value}", TitlePoint + (2,5))`
|
||||
/e\[\d+\]/
|
||||
num := parseInt(attr.slice(2))
|
||||
{commands, callbacks, parts} := jToG val, elements, num, backgroundRGB
|
||||
num := parseInt(name.slice(2))
|
||||
{commands, callbacks, parts} :=
|
||||
jToG value, elements, num, backgroundRGB
|
||||
if commands.length
|
||||
lastTried .= 0
|
||||
if commands.filter((&)).every (cmd) =>
|
||||
api.evalCommand(cmd) and ++lastTried
|
||||
callbacks.forEach &(api, parts)
|
||||
else console.log
|
||||
else console.warn
|
||||
`Geogebra command '${commands[lastTried]}'
|
||||
(part of translation of '${val}')
|
||||
(part of translation of '${value}')
|
||||
failed.`
|
||||
else console.log `Could not parse command '${val}'`
|
||||
else console.log `Unkown param ${param}`
|
||||
|
||||
// function myListener(...args: unknown[]) {
|
||||
// console.log 'In my listener with', args
|
||||
// }
|
||||
|
||||
// window.myListener = myListener
|
||||
else console.warn `Could not parse command '${value}'`
|
||||
else console.warn `Unkown param ${name} = ${value}`
|
||||
|
||||
// Parses a Joyce element-creating command, extending the elements
|
||||
// by side effect:
|
||||
|
@ -137,9 +149,12 @@ function jToG(
|
|||
index: number,
|
||||
backgroundRGB: RGB): 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.log `Unknown entity class ${klass}`
|
||||
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
|
||||
|
@ -151,7 +166,7 @@ function jToG(
|
|||
(args.scalar ?= []).push scalar
|
||||
continue
|
||||
unless jdep in elements
|
||||
console.log `Reference to unknown geometric entity ${jdep} in $jCom}`
|
||||
console.warn `Reference to unknown geometric entity ${jdep} in $jCom}`
|
||||
return cmdr
|
||||
usesCaptions.push jdep
|
||||
{klass: depKlass, otherName: depGeo, ends} := elements[jdep]
|
||||
|
@ -175,7 +190,7 @@ function jToG(
|
|||
else // we have to decorate
|
||||
dimension .= cmdr.parts.findLastIndex .includes name
|
||||
cmdr.callbacks.push (api: AppletObject, parts: DimParts) =>
|
||||
trace := false // e.g., klass is 'polygon'
|
||||
trace := adapParams.config?.color
|
||||
// Operate in order faces, lines, point, caption so that
|
||||
// we can adjust components after setting overall color, etc.
|
||||
|
||||
|
@ -202,7 +217,7 @@ function jToG(
|
|||
// Lines default to black:
|
||||
if invisible colors[2]
|
||||
for each line of parts[1]
|
||||
unless line in elements
|
||||
if line is name or line not in elements
|
||||
console.log 'Hiding line', line if trace
|
||||
api.setVisible line, false
|
||||
else
|
||||
|
@ -213,11 +228,12 @@ function jToG(
|
|||
api.setColor line, ...lineRGB
|
||||
|
||||
// Now color the points:
|
||||
console.log 'Considering point colors for', name if trace
|
||||
if invisible colors[1]
|
||||
// Hide all the dim-0 elements that are not their own independent
|
||||
// Hide all the dim-0 elements that are not distinct independent
|
||||
// items:
|
||||
for each point of parts[0]
|
||||
unless point in elements
|
||||
if point is name or point not in 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
|
||||
|
@ -335,7 +351,7 @@ function joyce2rgb(cname: string, backgroundRGB?: RGB): RGB
|
|||
[H,S,B] := cname.split(',').map (s) => parseInt s
|
||||
colorsea.hsv(H, S, B).rgb()
|
||||
else
|
||||
console.log 'Could not parse color:', cname
|
||||
console.warn 'Could not parse color:', cname
|
||||
[128, 128, 128]
|
||||
|
||||
function pointDefaultRGB(name: string, method: string): RGB
|
||||
|
@ -485,8 +501,7 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
auxiliaries.push ...[1..4].map (n) => aux + n
|
||||
unless madeSegment
|
||||
commands.push `${name} = Segment(${ends[0]},${ends[1]})`
|
||||
callbacks.push (api: AppletObject) =>
|
||||
api.setLabelVisible name, true
|
||||
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
|
||||
parts[0].push ...ends
|
||||
|
||||
circle: (name, method, args) =>
|
||||
|
@ -522,9 +537,9 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
api.renameObject obj, newObj
|
||||
switch api.getObjectType newObj
|
||||
'segment'
|
||||
parts[1].push newObj
|
||||
moreParts[1].push newObj
|
||||
'point'
|
||||
parts[0].push newObj
|
||||
moreParts[0].push newObj
|
||||
api.setVisible newObj, false
|
||||
/triangle|quadrilateral/
|
||||
pt := args.subpoints
|
||||
|
@ -539,9 +554,42 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||
if obj is name continue
|
||||
newObj := 'GeoAux' + index + obj
|
||||
api.renameObject obj, newObj
|
||||
parts[1].push 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
|
||||
'sector'
|
||||
unless args.subpoints?.length is 3 return
|
||||
parts[0].push ...args.subpoints
|
||||
[center, end, start] := args.subpoints
|
||||
ends[0] = start
|
||||
ends[1] = end
|
||||
parms := center + ', ' + start + ', ' + end
|
||||
commands.push
|
||||
`${name} = CircularSector(${parms})`
|
||||
`${aux}1 = CircularArc(${parms})`
|
||||
parts[1].push aux + 1
|
||||
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.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
|
||||
|
||||
sector: (name, method, args) => freshCommander()
|
||||
plane: (name, method, args) => freshCommander()
|
||||
sphere: (name, method, args) => freshCommander()
|
||||
polyhedron: (name, method, args) => freshCommander()
|
||||
|
|
25
src/adapptypes.civet
Normal file
25
src/adapptypes.civet
Normal file
|
@ -0,0 +1,25 @@
|
|||
export const flags = ['commands', 'color'] as const
|
||||
export type FlagType = (typeof flags)[number]
|
||||
export type ConfigType = Partial<Record<FlagType, boolean>>
|
||||
|
||||
export type AppletDescription
|
||||
html: string
|
||||
params: Record<string, string>
|
||||
id: string
|
||||
width: number
|
||||
height: number
|
||||
|
||||
export type AdapParams
|
||||
codebase?: string
|
||||
loader?: string
|
||||
config?: ConfigType
|
||||
joyceApplets: AppletDescription[]
|
||||
|
||||
export function params(kids: HTMLCollection): Record<string, string>
|
||||
return.value: Record<string, string> := {}
|
||||
for each kid of kids
|
||||
unless kid.tagName is 'PARAM' then continue
|
||||
name := kid.getAttribute 'name'
|
||||
value := kid.getAttribute 'value'
|
||||
unless name and value then continue
|
||||
return.value[name] = value
|
|
@ -1,20 +1,21 @@
|
|||
import https://code.jquery.com/jquery-3.7.1.js
|
||||
X3D from https://create3000.github.io/code/x_ite/latest/x_ite.mjs
|
||||
import ./deps/jquery.js
|
||||
{convert} from ./deps/vrml1to97/index.js
|
||||
|
||||
knownExtensions := /[.](?:wrl|x3d|gltf|glb|obj|stl|ply)$/
|
||||
certainlyHandled :=
|
||||
knownExtensions.source.slice(0, -2).split('wrl|')[1].split '|'
|
||||
|
||||
function makeBrowser(url: string)
|
||||
function makeBrowser(url: string, width: string, height: string)
|
||||
x_ite_rel := 'deps/x_ite/x_ite.mjs'
|
||||
x_ite_url .= './' + x_ite_rel
|
||||
unless typeof browser is 'undefined'
|
||||
if browser?.runtime?.getURL
|
||||
x_ite_url = browser.runtime.getURL x_ite_rel
|
||||
const { default: X3D } = await import x_ite_url
|
||||
canvas := X3D.createBrowser()
|
||||
|
||||
// Fix up css: smaller browser, but we can see the world info
|
||||
$(canvas).css
|
||||
width: '150px'
|
||||
height: '150px'
|
||||
overflow: 'visible'
|
||||
'vertical-align': 'top'
|
||||
$(canvas).css {width, height, overflow: 'visible', 'vertical-align': 'top'}
|
||||
x_ite_shadow := canvas.shadowRoot
|
||||
if x_ite_shadow
|
||||
x_ite_style := x_ite_shadow.querySelector 'link'
|
||||
|
@ -30,8 +31,8 @@ function makeBrowser(url: string)
|
|||
$(x_ite_browser).css overflow: 'visible'
|
||||
|
||||
// Now get the browser and load the requested VRML, converting if need be
|
||||
browser := X3D.getBrowser canvas
|
||||
browser.setBrowserOption 'StraightenHorizon', false
|
||||
browser3D := X3D.getBrowser canvas
|
||||
browser3D.setBrowserOption 'StraightenHorizon', false
|
||||
|
||||
if certainlyHandled.some((ext) => url.includes ext)
|
||||
canvas.setAttribute 'src', url
|
||||
|
@ -41,31 +42,62 @@ function makeBrowser(url: string)
|
|||
text .= await response.text()
|
||||
if /#\s*VRML\s*V?1[.]/i.test text
|
||||
text = convert text
|
||||
browser.baseURL = url
|
||||
scene := await browser.createX3DFromString text
|
||||
browser.replaceWorld scene
|
||||
browser3D.baseURL = url
|
||||
scene := await browser3D.createX3DFromString text
|
||||
browser3D.replaceWorld scene
|
||||
canvas
|
||||
|
||||
// Put eye icons after all of the eligible links
|
||||
links := $('a').filter -> knownExtensions.test @.getAttribute('href') ?? ''
|
||||
links.after ->
|
||||
newSpan := $('<span>👁</span>')
|
||||
newSpan.css 'overflow', 'visible'
|
||||
floatLike := this.lastElementChild as HTMLElement
|
||||
float .= ''
|
||||
if floatLike
|
||||
float = (floatLike.getAttribute('align') ?? '')
|
||||
or floatLike.style.getPropertyValue 'float'
|
||||
or window.getComputedStyle(floatLike).getPropertyValue 'float'
|
||||
switch float
|
||||
/left/i
|
||||
float = 'left'
|
||||
newSpan.css {float, position: 'relative'}
|
||||
/right/i
|
||||
float = 'right'
|
||||
newSpan.css {float, position: 'relative'}
|
||||
else float = ''
|
||||
newSpan.hover
|
||||
(-> $(@).css 'background-color', 'lightblue'),
|
||||
(-> $(@).css 'background-color', 'inherit')
|
||||
newSpan.on 'click', @,
|
||||
(e) =>
|
||||
eye := e.target
|
||||
state .= eye.getAttribute 'data'
|
||||
unless state
|
||||
state = 'off'
|
||||
url := e.data.getAttribute('href') ?? ''
|
||||
$(eye).after await makeBrowser url
|
||||
if state is 'off'
|
||||
eye.setAttribute 'data', 'on'
|
||||
$(eye).css 'text-decoration', 'line-through 3px'
|
||||
$(eye.nextSibling as Element).show()
|
||||
else
|
||||
eye.setAttribute 'data', 'off'
|
||||
$(eye).css 'text-decoration', 'none'
|
||||
$(eye.nextSibling as Element).hide()
|
||||
(e) =>
|
||||
eye := e.target
|
||||
state .= eye.getAttribute 'data'
|
||||
unless state
|
||||
state = 'off'
|
||||
url := e.data.getAttribute('href') ?? ''
|
||||
overImg := floatLike and floatLike.tagName is 'IMG'
|
||||
width := overImg ? ($(floatLike).width() + 'px') : '150px'
|
||||
height := overImg ? ($(floatLike).height() + 'px') : '150px'
|
||||
canvas := await makeBrowser url, width, height
|
||||
if float
|
||||
canvas.style.float = float
|
||||
if overImg
|
||||
canvas.style.position = 'absolute'
|
||||
imgSty := window.getComputedStyle floatLike
|
||||
canvas.style.marginTop = imgSty.getPropertyValue 'margin-top'
|
||||
canvas.style.marginLeft = imgSty.getPropertyValue 'margin-left'
|
||||
canvas.style.marginRight = imgSty.getPropertyValue 'margin-right'
|
||||
if float is 'right'
|
||||
canvas.style.left = $(eye).width() + 'px'
|
||||
else
|
||||
canvas.style.right = $(eye).width() + 'px'
|
||||
$(eye).append canvas
|
||||
if state is 'off'
|
||||
eye.setAttribute 'data', 'on'
|
||||
$(eye).css 'text-decoration', 'line-through 3px'
|
||||
$(eye.lastElementChild as Element).show()
|
||||
else
|
||||
eye.setAttribute 'data', 'off'
|
||||
$(eye).css 'text-decoration', 'none'
|
||||
$(eye.lastElementChild as Element).hide()
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
console.log """
|
||||
Hello, world --
|
||||
This is the beginning of something!
|
||||
"""
|
16
src/options.civet
Normal file
16
src/options.civet
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {flags} from ./adapptypes.ts
|
||||
|
||||
console.log('arrived')
|
||||
boxes := ['commands', 'color']
|
||||
cache := await browser.storage.local.get boxes
|
||||
console.log('Found', cache)
|
||||
|
||||
for each box of boxes
|
||||
checkbox := document.getElementById(box) as HTMLInputElement
|
||||
unless checkbox then continue
|
||||
checkbox.checked = cache[box] ?? false
|
||||
|
||||
document.body.addEventListener 'click', (event) ->
|
||||
elt := event.target as HTMLInputElement
|
||||
unless elt.tagName is 'INPUT' and elt.type is 'checkbox' then return
|
||||
browser.storage.local.set [elt.id]: elt.checked
|
Loading…
Add table
Add a link
Reference in a new issue