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: #44 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
a236e15990
commit
277d9b0a8c
@ -101,7 +101,7 @@ export interface AppletObject {
|
|||||||
getXcoord(objName: string): number;
|
getXcoord(objName: string): number;
|
||||||
getYcoord(objName: string): number;
|
getYcoord(objName: string): number;
|
||||||
getZcoord(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;
|
getValue(objName: string): number;
|
||||||
getVersion(): string;
|
getVersion(): string;
|
||||||
getScreenshotBase64(callback: (data: string) => void, scale?: number): void;
|
getScreenshotBase64(callback: (data: string) => void, scale?: number): void;
|
||||||
|
@ -18,8 +18,12 @@
|
|||||||
Alter execution of the translated applet: <br/>
|
Alter execution of the translated applet: <br/>
|
||||||
<label for="showall">Show all applet entities, even hidden ones</label>
|
<label for="showall">Show all applet entities, even hidden ones</label>
|
||||||
<input type="checkbox" id="showall"><br/>
|
<input type="checkbox" id="showall"><br/>
|
||||||
<label for="showaux">Show geogebra auxiliaries (not in applet)</label>
|
<label for="showaux">Show GeoGebra auxiliaries (not in applet)</label>
|
||||||
<input type="checkbox" id="showaux">
|
<input type="checkbox" id="showaux">
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<label for="algebra">Show the GeoGebra algebra pane</label>
|
||||||
|
<input type="checkbox" id="algebra">
|
||||||
<script src="options.js" type="module"></script>
|
<script src="options.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ document.addEventListener "DOMContentLoaded", async =>
|
|||||||
use3d = true
|
use3d = true
|
||||||
break
|
break
|
||||||
codebase .= browser.runtime.getURL 'deps/GeoGebra/HTML5/5.0/'
|
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 }
|
adapParams: AdapParams := {codebase, config, joyceApplets }
|
||||||
apars := JSON.stringify(adapParams)
|
apars := JSON.stringify(adapParams)
|
||||||
addScriptTag(browser.runtime.getURL 'deps/GeoGebra/deployggb.js').then =>
|
addScriptTag(browser.runtime.getURL 'deps/GeoGebra/deployggb.js').then =>
|
||||||
|
@ -35,6 +35,42 @@ type Description
|
|||||||
// with the otherName property:
|
// with the otherName property:
|
||||||
type JoyceElements = Record<AnyName, Description>
|
type JoyceElements = Record<AnyName, Description>
|
||||||
|
|
||||||
|
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<string, boolean>
|
||||||
|
|
||||||
|
pivotData: Record<string, PivotData> := {}
|
||||||
|
|
||||||
function postApplets(jApplets: AppletDescription[], codebase = '')
|
function postApplets(jApplets: AppletDescription[], codebase = '')
|
||||||
for each jApp of jApplets
|
for each jApp of jApplets
|
||||||
params := {
|
params := {
|
||||||
@ -44,11 +80,9 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
|
|||||||
jApp.width,
|
jApp.width,
|
||||||
jApp.height,
|
jApp.height,
|
||||||
appletOnLoad: (api: AppletObject) =>
|
appletOnLoad: (api: AppletObject) =>
|
||||||
elements: JoyceElements := {}
|
is3d := contains3d jApp.params
|
||||||
backgroundRGB := [255, 255, 255] as RGB
|
if is3d
|
||||||
config3d := contains3d jApp.params
|
api.enable3D true
|
||||||
if config3d
|
|
||||||
worked := api.enable3D true
|
|
||||||
api.setPerspective 'T'
|
api.setPerspective 'T'
|
||||||
// Get rid of the xy-plane indicator
|
// Get rid of the xy-plane indicator
|
||||||
xml .= api.getXML()
|
xml .= api.getXML()
|
||||||
@ -56,10 +90,26 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
|
|||||||
api.setXML xml
|
api.setXML xml
|
||||||
else if codebase.includes 'web3d'
|
else if codebase.includes 'web3d'
|
||||||
api.setPerspective 'G'
|
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
|
for name, value in jApp.params
|
||||||
dispatchJcommand
|
dispatchJcommand
|
||||||
api, name, value, elements, backgroundRGB, config3d
|
api, name, value, cdata
|
||||||
if config3d
|
if is3d
|
||||||
depth .= jApp.width
|
depth .= jApp.width
|
||||||
if jApp.height > depth then depth = jApp.height
|
if jApp.height > depth then depth = jApp.height
|
||||||
api.setCoordSystem
|
api.setCoordSystem
|
||||||
@ -78,22 +128,6 @@ function postApplets(jApplets: AppletDescription[], codebase = '')
|
|||||||
if codebase then geoApp.setHTML5Codebase codebase
|
if codebase then geoApp.setHTML5Codebase codebase
|
||||||
geoApp.inject jApp.id
|
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:
|
// Always use the final joyceApplets if there are any:
|
||||||
if joyceApplets.length
|
if joyceApplets.length
|
||||||
adapParams.joyceApplets = joyceApplets
|
adapParams.joyceApplets = joyceApplets
|
||||||
@ -135,7 +169,7 @@ type ClassHandler = (
|
|||||||
method: string,
|
method: string,
|
||||||
args: JoyceArguments,
|
args: JoyceArguments,
|
||||||
index: number,
|
index: number,
|
||||||
is3d: boolean) => Commander
|
cdata: ConstructionData) => Commander
|
||||||
type RGB = [number, number, number]
|
type RGB = [number, number, number]
|
||||||
type XYZ = RGB
|
type XYZ = RGB
|
||||||
|
|
||||||
@ -146,28 +180,26 @@ function dispatchJcommand(
|
|||||||
api: AppletObject,
|
api: AppletObject,
|
||||||
name: string,
|
name: string,
|
||||||
value: string,
|
value: string,
|
||||||
elements: JoyceElements
|
cdata: ConstructionData): void
|
||||||
backgroundRGB: RGB,
|
|
||||||
is3d: boolean): void
|
|
||||||
switch name
|
switch name
|
||||||
'background'
|
'background'
|
||||||
newback := joyce2rgb value, backgroundRGB
|
cdata.bg = joyce2rgb value, cdata.bg
|
||||||
if adapParams.config?.commands
|
if adapParams.config?.commands
|
||||||
console.log 'Setting background to', value, 'interpreted as',
|
console.log 'Setting background to', value, 'interpreted as',
|
||||||
newback
|
cdata.bg
|
||||||
for i of [0..2]
|
api.setGraphicsOptions 1, bgColor: colorsea(cdata.bg).hex()
|
||||||
backgroundRGB[i] = newback[i]
|
|
||||||
api.setGraphicsOptions 1, bgColor: colorsea(backgroundRGB).hex()
|
|
||||||
'title'
|
'title'
|
||||||
if adapParams.config?.commands
|
if adapParams.config?.commands
|
||||||
console.log 'Setting title to', value
|
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}
|
api.evalCommand `TitlePoint = ${corner}
|
||||||
Text("${value}", TitlePoint + (2,5))`
|
Text("${value}", TitlePoint + (2,5))`
|
||||||
|
'pivot'
|
||||||
|
return // already handled in postApplets
|
||||||
/e\[\d+\]/
|
/e\[\d+\]/
|
||||||
num := parseInt(name.slice(2))
|
num := parseInt(name.slice(2))
|
||||||
{commands, callbacks, parts} :=
|
{commands, callbacks, parts} :=
|
||||||
jToG value, elements, num, backgroundRGB, is3d
|
jToG value, num, cdata
|
||||||
if commands.length
|
if commands.length
|
||||||
lastTried .= 0
|
lastTried .= 0
|
||||||
if commands.filter((&)).every (cmd) =>
|
if commands.filter((&)).every (cmd) =>
|
||||||
@ -180,16 +212,14 @@ function dispatchJcommand(
|
|||||||
(part of translation of '${value}')
|
(part of translation of '${value}')
|
||||||
failed.`
|
failed.`
|
||||||
else console.warn `Could not parse command '${value}'`
|
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
|
// Parses a Joyce element-creating command, extending the elements
|
||||||
// by side effect:
|
// by side effect:
|
||||||
function jToG(
|
function jToG(
|
||||||
jCom: string,
|
jCom: string,
|
||||||
elements: JoyceElements,
|
|
||||||
index: number,
|
index: number,
|
||||||
backgroundRGB: RGB
|
cdata: ConstructionData): Commander
|
||||||
is3d: boolean): Commander
|
|
||||||
[jname, klass, method, data, ...colors] := jCom.split ';'
|
[jname, klass, method, data, ...colors] := jCom.split ';'
|
||||||
if adapParams.config?.commands
|
if adapParams.config?.commands
|
||||||
console.log 'Defining', jname, 'as a', klass, 'constructed by',
|
console.log 'Defining', jname, 'as a', klass, 'constructed by',
|
||||||
@ -199,7 +229,9 @@ function jToG(
|
|||||||
console.warn `Unknown entity class ${klass}`
|
console.warn `Unknown entity class ${klass}`
|
||||||
return cmdr
|
return cmdr
|
||||||
assertJoyceClass klass // shouldn't need to do that :-/
|
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 := {}
|
args: JoyceArguments := {}
|
||||||
usesCaptions := []
|
usesCaptions := []
|
||||||
for each jdep of data.split ','
|
for each jdep of data.split ','
|
||||||
@ -207,17 +239,17 @@ function jToG(
|
|||||||
if scalar is scalar // not NaN
|
if scalar is scalar // not NaN
|
||||||
(args.scalar ?= []).push scalar
|
(args.scalar ?= []).push scalar
|
||||||
continue
|
continue
|
||||||
unless jdep in elements
|
unless jdep in cdata.elements
|
||||||
console.warn `Reference to unknown geometric entity ${jdep} in $jCom}`
|
console.warn `Reference to unknown geometric entity ${jdep} in $jCom}`
|
||||||
return cmdr
|
return cmdr
|
||||||
usesCaptions.push jdep
|
usesCaptions.push jdep
|
||||||
{klass: depKlass, otherName: depGeo, ends} := elements[jdep]
|
{klass: depKlass, otherName: depGeo, ends} := cdata.elements[jdep]
|
||||||
(args[depKlass] ?= []).push depGeo
|
(args[depKlass] ?= []).push depGeo
|
||||||
if depKlass is 'point'
|
if depKlass is 'point'
|
||||||
(args.subpoints ?= []).push depGeo
|
(args.subpoints ?= []).push depGeo
|
||||||
else if depKlass is 'line'
|
else if depKlass is 'line'
|
||||||
(args.subpoints ?= []).push ...ends ?? []
|
(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) =>
|
unless name is jname then cmdr.callbacks.push (api: AppletObject) =>
|
||||||
api.setCaption name, jname
|
api.setCaption name, jname
|
||||||
api.setLabelStyle name, 3 // style CAPTION = 3
|
api.setLabelStyle name, 3 // style CAPTION = 3
|
||||||
@ -225,7 +257,27 @@ function jToG(
|
|||||||
cmdr.callbacks.push (api: AppletObject) =>
|
cmdr.callbacks.push (api: AppletObject) =>
|
||||||
for each aux of cmdr.auxiliaries
|
for each aux of cmdr.auxiliaries
|
||||||
api.setAuxiliary aux, true
|
api.setAuxiliary aux, true
|
||||||
|
api.setLabelVisible aux, false
|
||||||
api.setVisible 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
|
// Create callback to assign colors
|
||||||
if colors.length is 4 and colors.every (color) => invisible color
|
if colors.length is 4 and colors.every (color) => invisible color
|
||||||
cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false
|
cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false
|
||||||
@ -243,11 +295,11 @@ function jToG(
|
|||||||
console.log 'Fading out interior of', face if trace
|
console.log 'Fading out interior of', face if trace
|
||||||
// hide the interior by making it transparent
|
// hide the interior by making it transparent
|
||||||
api.setFilling face, 0
|
api.setFilling face, 0
|
||||||
else if face not in elements
|
else if face not in cdata.elements
|
||||||
console.log 'Hiding face', face if trace
|
console.log 'Hiding face', face if trace
|
||||||
api.setVisible face, false
|
api.setVisible face, false
|
||||||
else
|
else
|
||||||
faceRGB := joyce2rgb(colors[3] or 'brighter', backgroundRGB)
|
faceRGB := joyce2rgb(colors[3] or 'brighter', cdata.bg)
|
||||||
deep := ['circle', 'polygon', 'sector']
|
deep := ['circle', 'polygon', 'sector']
|
||||||
filling := deep.includes(klass) ? 0.7 : 0.2
|
filling := deep.includes(klass) ? 0.7 : 0.2
|
||||||
for each face of parts[2]
|
for each face of parts[2]
|
||||||
@ -259,28 +311,30 @@ function jToG(
|
|||||||
// Lines default to black:
|
// Lines default to black:
|
||||||
if invisible colors[2]
|
if invisible colors[2]
|
||||||
for each line of parts[1]
|
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
|
console.log 'Hiding line', line if trace
|
||||||
api.setVisible line, false
|
api.setVisible line, false
|
||||||
else
|
else
|
||||||
lineRGB := joyce2rgb(colors[2] or 'black', backgroundRGB)
|
lineRGB := joyce2rgb(colors[2] or 'black', cdata.bg)
|
||||||
for each line of parts[1]
|
for each line of parts[1]
|
||||||
console.log 'Coloring line', line, 'to', colors[2] if trace
|
console.log 'Coloring line', line, 'to', colors[2] if trace
|
||||||
api.setVisible line, true
|
api.setVisible line, true
|
||||||
api.setColor line, ...lineRGB
|
api.setColor line, ...lineRGB
|
||||||
|
|
||||||
// Now color the points:
|
// 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]
|
if invisible colors[1]
|
||||||
// Hide all the dim-0 elements that are not distinct independent
|
// Hide all the dim-0 elements that are not distinct independent
|
||||||
// items:
|
// items:
|
||||||
for each point of parts[0]
|
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
|
console.log 'Hiding point', point if trace
|
||||||
api.setVisible point, false
|
api.setVisible point, false
|
||||||
else if dimension is 0 or colors[1] // Need to color the points
|
else if dimension is 0 or colors[1] // Need to color the points
|
||||||
ptRGB := colors[1] ? joyce2rgb colors[1], backgroundRGB
|
ptRGB := colors[1] ? joyce2rgb colors[1], cdata.bg
|
||||||
: pointDefaultRGB name, method
|
: pointDefaultRGB name, method, isPivot
|
||||||
for each point of parts[0]
|
for each point of parts[0]
|
||||||
console.log 'Coloring point', point, 'to', colors[1] if trace
|
console.log 'Coloring point', point, 'to', colors[1] if trace
|
||||||
api.setVisible point, true
|
api.setVisible point, true
|
||||||
@ -290,12 +344,12 @@ function jToG(
|
|||||||
if invisible colors[0]
|
if invisible colors[0]
|
||||||
console.log 'Hiding label', name if trace
|
console.log 'Hiding label', name if trace
|
||||||
api.setLabelVisible name, false
|
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
|
// Have to make a text to provide the caption, since GeoGebra
|
||||||
// doesn't allow caption different color from entity.
|
// doesn't allow caption different color from entity.
|
||||||
textName := 'GeoText' + index
|
textName := 'GeoText' + index
|
||||||
locationExpr := switch klass
|
locationExpr := switch klass
|
||||||
when 'point' then `1.02${name})`
|
when 'point' then `1.02${name}`
|
||||||
when 'line'
|
when 'line'
|
||||||
(`Midpoint(${name}) + `
|
(`Midpoint(${name}) + `
|
||||||
+ `Rotate(Direction(${name})*Length(${name})*0.02, pi/2)`)
|
+ `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 ex1 then ex1 = `Centroid(${ex1})`
|
||||||
unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})`
|
unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})`
|
||||||
`(4*${ex1}+${ex2})/5`
|
`(4*${ex1}+${ex2})/5`
|
||||||
api.evalCommand `${textName} = Text("${jname}", ${locationExpr})`
|
textCmd := `${textName} = Text("${jname}", ${locationExpr})`
|
||||||
api.setColor textName, ...joyce2rgb colors[0], backgroundRGB
|
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
|
// and hide the underlying GeoGebra label
|
||||||
api.setLabelVisible name, false
|
api.setLabelVisible name, false
|
||||||
else if colors[0]
|
else if colors[0]
|
||||||
@ -331,19 +389,33 @@ function jToG(
|
|||||||
console.log 'Setting label vis of', name, 'to', show if trace
|
console.log 'Setting label vis of', name, 'to', show if trace
|
||||||
api.setLabelVisible name, show
|
api.setLabelVisible name, show
|
||||||
|
|
||||||
// window[hideListener] = (arg) =>
|
cdata.elements[jname] = {otherName: name, usesCaptions, klass, cmdr.ends}
|
||||||
// api.setVisible name, false
|
cdata.elements[name] = {otherName: jname, usesCaptions, klass, cmdr.ends}
|
||||||
// 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}
|
|
||||||
cmdr
|
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<string, number> := {}
|
||||||
|
relY: Record<string, number> := {}
|
||||||
|
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
|
function invisible(cname: string): boolean
|
||||||
if adapParams.config?.showall then return false
|
if adapParams.config?.showall then return false
|
||||||
cname is '0' or cname is 'none'
|
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
|
console.warn 'Could not parse color:', cname
|
||||||
[128, 128, 128]
|
[128, 128, 128]
|
||||||
|
|
||||||
function pointDefaultRGB(name: string, method: string): RGB
|
function pointDefaultRGB(name: string, method: string, isPivot: boolean): RGB
|
||||||
// Need to short-circuit with green for pivot point, once that is implemented
|
if isPivot then return joyce2rgb 'green'
|
||||||
switch method
|
switch method
|
||||||
'free'
|
'free'
|
||||||
joyce2rgb 'red'
|
joyce2rgb 'red'
|
||||||
@ -406,19 +478,39 @@ function pointDefaultRGB(name: string, method: string): RGB
|
|||||||
joyce2rgb 'orange'
|
joyce2rgb 'orange'
|
||||||
else joyce2rgb 'black'
|
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 .= 0n
|
||||||
numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname
|
numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname
|
||||||
return .= 'Geo' + numCode.toString(36);
|
return .= 'Geo' + numCode.toString(36);
|
||||||
return += '1' while return.value in elements
|
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
|
// All of the detailed semantics of each available command lies in this
|
||||||
// function.
|
// function.
|
||||||
classHandler: Record<JoyceClass, ClassHandler> :=
|
classHandler: Record<JoyceClass, ClassHandler> :=
|
||||||
point: (name, method, args, index, is3d): Commander =>
|
point: (name, method, args, index, cdata): Commander =>
|
||||||
return := freshCommander()
|
return := freshCommander()
|
||||||
{commands, callbacks, parts, auxiliaries} := return.value
|
{commands, callbacks, parts, auxiliaries} := return.value
|
||||||
|
zeroVector := cdata.is3d ? 'Vector((0,0,0))' : 'Vector((0,0))'
|
||||||
aux := name + 'aUx'
|
aux := name + 'aUx'
|
||||||
|
pivotable := cdata.pivot and name !== pivotData[cdata.pivot].pivot
|
||||||
parts[0].push name
|
parts[0].push name
|
||||||
switch method
|
switch method
|
||||||
/angle(?:Bisector|Divider)/
|
/angle(?:Bisector|Divider)/
|
||||||
@ -433,30 +525,41 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||||||
commands.push `${destination} = Segment(${start}, ${end})`
|
commands.push `${destination} = Segment(${start}, ${end})`
|
||||||
else destination = args.line[0]
|
else destination = args.line[0]
|
||||||
n := method is 'angleBisector' ? 2 : args.scalar?[0]
|
n := method is 'angleBisector' ? 2 : args.scalar?[0]
|
||||||
inPlane := is3d ? `, Plane(${start}, ${center}, ${end})` : ''
|
inPlane := cdata.is3d ? `, Plane(${start}, ${center}, ${end})` : ''
|
||||||
commands.push
|
commands.push
|
||||||
`${aux}2 = Angle(${start}, ${center}, ${end}${inPlane})`
|
`${aux}2 = Angle(${start}, ${center}, ${end}${inPlane})`
|
||||||
`${aux}3 = If(${aux}2 > pi, ${aux}2 - 2*pi, ${aux}2)`
|
`${aux}3 = If(${aux}2 > pi, ${aux}2 - 2*pi, ${aux}2)`
|
||||||
`${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center}${inPlane})`
|
`${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center}${inPlane})`
|
||||||
`${name} = Intersect(${destination}, Ray(${center}, ${aux}4))`
|
`${name} = Intersect(${destination}, Ray(${center}, ${aux}4))`
|
||||||
auxiliaries.push ...[2..4].map (i) => `${aux}${i}`
|
auxiliaries.push ...[2..4].map (i) => `${aux}${i}`
|
||||||
'extend'
|
'circleSlider'
|
||||||
sp := args.subpoints
|
unless args.circle then return
|
||||||
unless sp then return
|
circ := args.circle[0]
|
||||||
direction .= `UnitVector(Vector(${sp[0]},${sp[1]}))`
|
commands.push `${name} = Point(${circ})`
|
||||||
if args.line and (
|
if (pivotable
|
||||||
not args.point or args.point[0] !== sp[0])
|
and cdata.elements[circ].ends?[0] // center
|
||||||
direction = `UnitVector(${args.line[0]})`
|
is pivotData[cdata.pivot].pivot)
|
||||||
displacement := `Distance(${sp[2]}, ${sp[3]})*${direction}`
|
pivotData[cdata.pivot].rotatable.push name
|
||||||
commands.push `${name} = Translate(${sp[1]}, ${displacement})`
|
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'
|
'first'
|
||||||
unless args.subpoints then return
|
unless args.subpoints then return
|
||||||
commands.push `${name} = ${args.subpoints[0]}`
|
commands.push `${name} = ${args.subpoints[0]}`
|
||||||
/fixed|free/
|
/fixed|free/
|
||||||
coords := args.scalar
|
coords := args.scalar
|
||||||
unless coords then return
|
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 method is 'fixed'
|
||||||
|
if pivotable then pivotData[cdata.pivot].fixed[name] = true
|
||||||
callbacks.push (api: AppletObject) => api.setFixed name, true
|
callbacks.push (api: AppletObject) => api.setFixed name, true
|
||||||
'foot'
|
'foot'
|
||||||
pt := args.subpoints
|
pt := args.subpoints
|
||||||
@ -476,7 +579,8 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||||||
commands.push `${name} = Intersect(${l1},${e2})`
|
commands.push `${name} = Intersect(${l1},${e2})`
|
||||||
'last'
|
'last'
|
||||||
unless args.subpoints then return
|
unless args.subpoints then return
|
||||||
commands.push `${name} = ${args.subpoints.at(-1)}`
|
commands.push
|
||||||
|
`${name} = ${args.subpoints.at(-1)}`
|
||||||
'lineSegmentSlider'
|
'lineSegmentSlider'
|
||||||
segment .= args.line?[0]
|
segment .= args.line?[0]
|
||||||
unless segment
|
unless segment
|
||||||
@ -500,9 +604,26 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||||||
[center, direction] := args.subpoints
|
[center, direction] := args.subpoints
|
||||||
// Note clockwise 90° rotation (3π/2) confirmed in Joyce source
|
// Note clockwise 90° rotation (3π/2) confirmed in Joyce source
|
||||||
commands.push `${name} = Rotate(${direction}, 3*pi/2, ${center})`
|
commands.push `${name} = Rotate(${direction}, 3*pi/2, ${center})`
|
||||||
'proportion'
|
/proportion|similar/
|
||||||
pt := args.subpoints
|
pt .= args.subpoints
|
||||||
unless pt then return
|
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]})`
|
len := `Distance(${pt[2]},${pt[3]})*Distance(${pt[4]},${pt[5]})`
|
||||||
+ `/ Distance(${pt[0]},${pt[1]})`
|
+ `/ Distance(${pt[0]},${pt[1]})`
|
||||||
direction := `UnitVector(Vector(${pt[6]}, ${pt[7]}))`
|
direction := `UnitVector(Vector(${pt[6]}, ${pt[7]}))`
|
||||||
@ -511,7 +632,7 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||||||
commands.push
|
commands.push
|
||||||
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
|
`${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
|
||||||
|
|
||||||
line: (name, method, args) =>
|
line: (name, method, args, index, cdata) =>
|
||||||
return := freshCommander()
|
return := freshCommander()
|
||||||
return.value.ends = ['', '']
|
return.value.ends = ['', '']
|
||||||
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
||||||
@ -519,10 +640,62 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||||||
parts[1].push name
|
parts[1].push name
|
||||||
madeSegment .= false
|
madeSegment .= false
|
||||||
switch method
|
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'
|
'connect'
|
||||||
unless args.subpoints and args.subpoints.length is 2 then return
|
unless args.subpoints and args.subpoints.length is 2 then return
|
||||||
ends[0] = args.subpoints[0]
|
ends[0] = args.subpoints[0]
|
||||||
ends[1] = args.subpoints[1]
|
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'
|
'parallel'
|
||||||
unless args.subpoints then return
|
unless args.subpoints then return
|
||||||
[newStart, oldStart, oldEnd] := args.subpoints
|
[newStart, oldStart, oldEnd] := args.subpoints
|
||||||
@ -538,24 +711,41 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||||||
madeSegment = true
|
madeSegment = true
|
||||||
else
|
else
|
||||||
commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)`
|
commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)`
|
||||||
'chord'
|
'perpendicular'
|
||||||
// To match Joyce, we need to get the ordering here correct.
|
pt := args.subpoints
|
||||||
// ends[0] should be the one closer to args.subpoints[0]
|
unless pt then return
|
||||||
unless args.subpoints and args.circle then return
|
inPlane .= ''
|
||||||
line := `Line(${args.subpoints.join ','})`
|
if cdata.is3d
|
||||||
pt := args.subpoints[0]
|
unless args.plane?.length is 1 then return
|
||||||
commands.push ...[1..2].map (n) =>
|
inPlane = `,${args.plane[0]}`
|
||||||
`${aux}${n} = Intersect(${args.circle}, ${line}, ${n})`
|
switch pt.length
|
||||||
condition := `Distance(${aux}2,${pt}) < Distance(${aux}1,${pt})`
|
when 2
|
||||||
// NOTE: Joyce's code has special case for when pt is almost
|
ends[0] = pt[0]
|
||||||
// at midpoint of chord; in that case, it starts at endpoint
|
ends[1] = aux
|
||||||
// closer to the second subpoint... postponing that nicety
|
auxiliaries.push aux
|
||||||
|
if args.line?.length is 1
|
||||||
|
ln := args.line[0]
|
||||||
commands.push
|
commands.push
|
||||||
`${aux}3 = If(${condition}, ${aux}2, ${aux}1)`
|
`${name} = Rotate(${ln}, 3*pi/2, ${pt[0]}${inPlane})`
|
||||||
`${aux}4 = If(${condition}, ${aux}1, ${aux}2)`
|
`${aux} = Vertex(${name}, 2)`
|
||||||
ends[0] = aux + 3
|
madeSegment = true
|
||||||
ends[1] = aux + 4
|
else
|
||||||
auxiliaries.push ...[1..4].map (n) => aux + n
|
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
|
unless madeSegment
|
||||||
commands.push `${name} = Segment(${ends[0]},${ends[1]})`
|
commands.push `${name} = Segment(${ends[0]},${ends[1]})`
|
||||||
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
|
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
|
||||||
@ -563,14 +753,27 @@ classHandler: Record<JoyceClass, ClassHandler> :=
|
|||||||
|
|
||||||
circle: (name, method, args) =>
|
circle: (name, method, args) =>
|
||||||
return := freshCommander()
|
return := freshCommander()
|
||||||
{commands, callbacks, parts, auxiliaries} := return.value
|
return.value.ends = ['', '']
|
||||||
|
{commands, callbacks, parts, auxiliaries, ends} := return.value
|
||||||
parts[2].push name
|
parts[2].push name
|
||||||
parts[1].push name
|
parts[1].push name
|
||||||
switch method
|
switch method
|
||||||
'radius'
|
'radius'
|
||||||
unless args.subpoints then return
|
pt := args.subpoints
|
||||||
[center, point] := args.subpoints
|
unless pt then return
|
||||||
commands.push `${name} = Circle(${center}, ${point})`
|
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
|
callbacks.push (api: AppletObject) => api.setLabelVisible name, true
|
||||||
|
|
||||||
polygon: (name, method, args, index) =>
|
polygon: (name, method, args, index) =>
|
||||||
|
@ -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 FlagType = (typeof flags)[number]
|
||||||
export type ConfigType = Partial<Record<FlagType, boolean>>
|
export type ConfigType = Partial<Record<FlagType, boolean>>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user