kopia lustrzana https://github.com/backface/turtlestitch
1271 wiersze
39 KiB
JavaScript
1271 wiersze
39 KiB
JavaScript
/*
|
|
|
|
extensions.js
|
|
|
|
additional primitives for SNAP!
|
|
|
|
written by Jens Mönig
|
|
|
|
Copyright (C) 2022 by Jens Mönig
|
|
|
|
This file is part of Snap!.
|
|
|
|
Snap! is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of
|
|
the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
// Global settings /////////////////////////////////////////////////////
|
|
|
|
/*global modules, List, StageMorph, Costume, SpeechSynthesisUtterance, Sound,
|
|
IDE_Morph, CamSnapshotDialogMorph, SoundRecorderDialogMorph, isSnapObject, nop,
|
|
Color, Process, contains*/
|
|
|
|
/*jshint esversion: 11, bitwise: false*/
|
|
|
|
modules.extensions = '2022-February-08';
|
|
|
|
// Global stuff
|
|
|
|
var SnapExtensions = {
|
|
primitives: new Map(),
|
|
menus: new Map(),
|
|
scripts: [],
|
|
urls: [ // allow-list of trusted servers
|
|
'libraries/',
|
|
'https://snap.berkeley.edu/',
|
|
'https://ecraft2learn.github.io/ai/', // Uni-Oxford, Ken Kahn
|
|
'https://microworld.edc.org' // EDC, E. Paul Goldenberg
|
|
]
|
|
};
|
|
|
|
/*
|
|
SnapExtensions is a set of two global dictionaries of named functions to be
|
|
used as extension primitives for blocks or dynamic dropdown menus. Block
|
|
extensions are stored in the "primitives" dictionary of SnapExtensions,
|
|
dynamic dropdown menus in the "menus" section.
|
|
|
|
You can also extend Snap! with your own externally hosted JavaScript file(s)
|
|
and have them add your own extension primitives and menus to the global
|
|
SnapExtensions dictionaries. This lets you provide libraries to support
|
|
special APIs and custom hardware.
|
|
|
|
|
|
1. Primitives (additional blocks)
|
|
=================================
|
|
The names under which primitives are stored will apear in the dropdown
|
|
menus of the hidden extension "primitive" blocks sorted alphabetically.
|
|
(You can find those extension primitives in Snap's search bar or in dev
|
|
mode. There are two version of the primitive block, a command version and
|
|
a reporter one, both show the same list of available extensions.
|
|
|
|
naming conventions
|
|
------------------
|
|
domain-prefix_function-name(parameter-list)
|
|
example: 'lst_sort(list, fn)'
|
|
|
|
- domain-prefix: max 3-letter lowercase identifier
|
|
followed by an underscore
|
|
e.g.: err_, lst_, txt_, dta_, map_, tts_, xhr_, geo_, mda_
|
|
|
|
- function-name: short, single word if possible, lowercase
|
|
- parameter-list: comma separated names or type indicators
|
|
|
|
function semantics
|
|
------------------
|
|
- functions are called by the "primitive" blocks with any arguments provided
|
|
- use the "function () {}" notation to define functions, not the ES6 arrow
|
|
notation, otherwise "this" will not get scoped correctly
|
|
- "this" refers to the current snap object (sprite or stage) at call-time
|
|
- a reference to the current process is always passed as last argument
|
|
|
|
|
|
2. Menus (for input slots)
|
|
==========================
|
|
The names of the available dynamic drowdown menus can be written into the
|
|
"options" dialog when defining an input slot. Additionally you can choose
|
|
from a list of available menus when holding down the shift-key while
|
|
clicking on the partial-gear button in Snap's input-slot dialog.
|
|
|
|
naming conventions
|
|
------------------
|
|
domain-prefix_function-name
|
|
example: 'clr_number'
|
|
|
|
- domain-prefix: max 3-letter lowercase identifier
|
|
followed by an underscore
|
|
e.g.: clr_, txt_, lst_
|
|
|
|
- function-name: short, single word if possible, lowercase
|
|
- NOTE: dynamic menu functions cannot have any inputs
|
|
|
|
function semantics
|
|
------------------
|
|
- use the "function () {}" notation to define functions, not the ES6 arrow
|
|
notation, otherwise "this" will not get scoped correctly
|
|
- "this" refers to the current input-slot at call-time (when the menu is
|
|
requested by the user by clicking on the down-arrow symbol)
|
|
- to get a handle on the current block use "this.parentThatIsA(BlockMorph)"
|
|
- likewise to get a handle on the current sprite use
|
|
"this.parentThatIsA(IDE_Morph).currentSprite"
|
|
- if you want the menu of one input slot to depend on the contents of
|
|
another input slot of the same block, you can get a handle to the block
|
|
using the above method, and then access all inputs by calling
|
|
"block.inputs()". This will give you an array of all input slots.
|
|
You can access the contents of an input slot by calling "slot.evaluate()"
|
|
|
|
|
|
3. External JavaScript files
|
|
============================
|
|
You can provide extensions for your custom hardware or for arbitrary APIs
|
|
or extend Snap! with JavaScript libraries from other parties. You can
|
|
load additional JavaScript files using the
|
|
|
|
src_load(url)
|
|
|
|
extension primitive inside Snap, which you can find using Snap's search bar
|
|
in the IDE. The loading primitive will wait until the source file has fully
|
|
loaded and its defined functions are ready to be called.
|
|
Snap remembers the external extensions that have been already loaded and
|
|
will ignore any subsequent calls to load the same external extension again.
|
|
This lets you lazily initialize your extension by simply adding a
|
|
"src_load(url)" command for your external JS file before calling any of its
|
|
added functions.
|
|
|
|
|
|
4. Miscellaneous
|
|
================
|
|
|
|
calling extension primitives in other JavaScript functions
|
|
----------------------------------------------------------
|
|
you can call other extension primitives from your own JavaScript functions,
|
|
especially if you want to reuse them in your own extensions. Just make sure
|
|
to use apply() instead of calling them directly, so "this" gets scoped
|
|
correctly, e.g.:
|
|
|
|
SnapExtensions.primitives.get('var_declare(scope, name)').apply(
|
|
this,
|
|
['global', '_my var', proc]
|
|
);
|
|
|
|
Don't forget to pass in a reference to the current process as last parameter
|
|
in case the callee requires it.
|
|
|
|
adding primitives to SnapExtensions
|
|
-----------------------------------
|
|
It is the suggested best practice to expose your own extension primitives
|
|
by adding them to the global SnapExtensions libraries (for primitives and
|
|
menus) using the very same conventions described herein, and then to offer
|
|
a library of custom blocks that make calls to your additional operations.
|
|
|
|
developing an extension
|
|
-----------------------
|
|
Running the "src_load(url)" primitive will throw an error unless you first
|
|
check the "Enable JavaScript extensions" setting in Snap's preferences menu,
|
|
or if your JavaScript extension comes from a list of trusted hosts.
|
|
While you develop your JavaScript extension it's recommended to turn on the
|
|
"Enable JavaScript extensions" setting to load the extension once, and
|
|
then to turn it off again, so you can make sure your custom blocks are not
|
|
using any "JS Function" blocks (because those will be caught if the
|
|
preference is turned off).
|
|
|
|
publishing an extension
|
|
-----------------------
|
|
|
|
When you're ready to publish your extension you can contact us to allow-list
|
|
the url hosting your JS file, or you can send me a Github pull-request to
|
|
include it in the main Snap branch.
|
|
We recommend submitting your extensions to the main Snap! Github repository
|
|
so they can be made available in the offline versions (source download
|
|
and PWA).
|
|
External extensions are a powerful tools to change, override and generally
|
|
mold Snap into anything you want, so please use these capabilities sensibly.
|
|
We look forward to your innovations and don't plan to restrict the scope of
|
|
what extensions are allowed to modify. For security reasons we do ask you to
|
|
refrain from exposing any form of JS eval(), including "new Function()" to
|
|
end users (if you want to use eval() internally in your extension we'll
|
|
frown on you but not reject your contribution).
|
|
*/
|
|
|
|
// Primitives
|
|
|
|
// errors & exceptions (err_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'err_error(msg)',
|
|
function (msg) {
|
|
throw new Error(msg, {cause: 'user'});
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'err_try(cmd, catch, err)',
|
|
function (action, exception, errVarName, proc) {
|
|
proc.tryCatch(action, exception, errVarName);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'err_reset',
|
|
function (proc) {
|
|
proc.resetErrorHandling();
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'err_ignore',
|
|
nop
|
|
);
|
|
|
|
// list utils (lst_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'lst_sort(list, fn)',
|
|
function (data, fn, proc) {
|
|
return proc.reportAtomicSort(data, fn);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'lst_linked(list)',
|
|
function (data) {
|
|
return data.isLinked;
|
|
}
|
|
);
|
|
|
|
// text utils (txt_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'txt_lowercase(txt)',
|
|
function (txt) {
|
|
return txt.toLowerCase();
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'txt_indexof(sub, txt)',
|
|
function (sub, txt) {
|
|
return txt.indexOf(sub) + 1;
|
|
}
|
|
);
|
|
|
|
// bitwise operations
|
|
|
|
SnapExtensions.primitives.set(
|
|
'bit_and(a, b)',
|
|
function (a, b) {
|
|
return a & b;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'bit_or(a, b)',
|
|
function (a, b) {
|
|
return a | b;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'bit_xor(a, b)',
|
|
function (a, b) {
|
|
return a ^ b;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'bit_not(a)',
|
|
function (a) {
|
|
return ~ a;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'bit_left_shift(a, b)',
|
|
function (a, b) {
|
|
return a << b;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'bit_right_shift(a, b)',
|
|
function (a, b) {
|
|
return a >> b;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'bit_unsigned_right_shift(a, b)',
|
|
function (a, b) {
|
|
return a >>> b;
|
|
}
|
|
);
|
|
|
|
// data sciene & frequency distribution analysis (dta_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'dta_analyze(list)',
|
|
function (list, proc) {
|
|
var dict = new Map(),
|
|
result = [],
|
|
data = list.itemsArray(),
|
|
len = data.length,
|
|
item, i;
|
|
for (i = 0; i < len; i += 1) {
|
|
item = proc.reportIsA(data[i], 'number') ?
|
|
data[i].toString() : data[i];
|
|
if (dict.has(item)) {
|
|
dict.set(item, dict.get(item) + 1);
|
|
} else {
|
|
dict.set(item, 1);
|
|
}
|
|
}
|
|
dict.forEach(function (value, key) {
|
|
result.push(new List([key, value]));
|
|
});
|
|
return new List(result);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'dta_group(list, fn)',
|
|
function (data, fn, proc) {
|
|
return proc.reportAtomicGroup(data, fn);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'dta_transpose(list)',
|
|
function (data, proc) {
|
|
proc.assertType(data, 'list');
|
|
return data.transpose();
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'dta_crossproduct(list)',
|
|
function (data, proc) {
|
|
proc.assertType(data, 'list');
|
|
return data.crossproduct();
|
|
}
|
|
);
|
|
|
|
// World map (map_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_zoom',
|
|
function () {
|
|
return this.parentThatIsA(StageMorph).worldMap.zoom;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_zoom(n)',
|
|
function (num) {
|
|
this.parentThatIsA(StageMorph).worldMap.setZoom(num);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_lon(x)',
|
|
function (x) {
|
|
return this.parentThatIsA(StageMorph).worldMap.lonFromSnapX(x);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_lat(y)',
|
|
function (y) {
|
|
return this.parentThatIsA(StageMorph).worldMap.latFromSnapY(y);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_view(lon, lat)',
|
|
function (lon, lat) {
|
|
this.parentThatIsA(StageMorph).worldMap.setView(lon, lat);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_y(lat)',
|
|
function (lat) {
|
|
return this.parentThatIsA(StageMorph).worldMap.snapYfromLat(lat);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_x(lon)',
|
|
function (lon) {
|
|
return this.parentThatIsA(StageMorph).worldMap.snapXfromLon(lon);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_pan(x, y)',
|
|
function (x, y) {
|
|
this.parentThatIsA(StageMorph).worldMap.panBy(x, y);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_dist(lat1, lon1, lat2, lon2)',
|
|
function (lat1, lon1, lat2, lon2) {
|
|
return this.parentThatIsA(StageMorph).worldMap.distanceInKm(
|
|
lat1,
|
|
lon1,
|
|
lat2,
|
|
lon2
|
|
);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_update',
|
|
function () {
|
|
var stage = this.parentThatIsA(StageMorph);
|
|
stage.worldMap.extent = stage.dimensions;
|
|
stage.worldMap.render();
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_loaded',
|
|
function () {
|
|
return !this.parentThatIsA(StageMorph).worldMap.loading;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_costume',
|
|
function () {
|
|
return new Costume(
|
|
this.parentThatIsA(StageMorph).worldMap.canvas,
|
|
'map'
|
|
);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'map_style(name)',
|
|
function (name) {
|
|
this.parentThatIsA(StageMorph).worldMap.setHost(name);
|
|
}
|
|
);
|
|
|
|
// text-to-speech (tts_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'tts_speak(txt, lang, pitch, rate)',
|
|
function (msg, accent, pitch, rate) {
|
|
var utter = new SpeechSynthesisUtterance(msg),
|
|
isDone = false;
|
|
utter.lang = accent;
|
|
utter.pitch = pitch;
|
|
utter.rate = rate;
|
|
utter.onend = () => isDone = true;
|
|
window.speechSynthesis.speak(utter);
|
|
return () => isDone;
|
|
}
|
|
);
|
|
|
|
// XHR:
|
|
|
|
SnapExtensions.primitives.set(
|
|
'xhr_request(mth, url, dta, hdrs)',
|
|
function (method, url, data, headers, proc) {
|
|
var response, i, header;
|
|
Process.prototype.checkURLAllowed(url);
|
|
if (!proc.httpRequest) {
|
|
proc.httpRequest = new XMLHttpRequest();
|
|
proc.httpRequest.open(method, url, true);
|
|
proc.assertType(headers, 'list');
|
|
for (i = 1; i <= headers.length(); i += 1) {
|
|
header = headers.at(i);
|
|
proc.assertType(header, 'list');
|
|
proc.httpRequest.setRequestHeader(
|
|
header.at(1),
|
|
header.at(2)
|
|
);
|
|
}
|
|
proc.httpRequest.send(data || null);
|
|
} else if (proc.httpRequest.readyState === 4) {
|
|
response = proc.httpRequest.responseText;
|
|
proc.httpRequest = null;
|
|
return response;
|
|
}
|
|
proc.pushContext('doYield');
|
|
proc.pushContext();
|
|
}
|
|
);
|
|
|
|
// Geo-location (geo_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'geo_location(acc?)',
|
|
function (includeAccuracy) {
|
|
var crd = new List(),
|
|
myself = this,
|
|
options = {
|
|
enableHighAccuracy: true,
|
|
timeout: 5000,
|
|
maximumAge: 0
|
|
};
|
|
|
|
function success(pos) {
|
|
crd = new List([
|
|
pos.coords.latitude,
|
|
pos.coords.longitude
|
|
]);
|
|
if (includeAccuracy) {
|
|
crd.add(pos.coords.accuracy);
|
|
}
|
|
}
|
|
|
|
function error(err) {
|
|
crd = new List([37.872099, -122.257852]);
|
|
myself.inform('Warning:\nGeolocation failed.');
|
|
}
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
success,
|
|
error,
|
|
options
|
|
);
|
|
return () => crd;
|
|
}
|
|
);
|
|
|
|
// MediaComp (mda_)
|
|
|
|
SnapExtensions.primitives.set(
|
|
'mda_snap',
|
|
function () {
|
|
var camDialog,
|
|
result = false;
|
|
camDialog = new CamSnapshotDialogMorph(
|
|
this.parentThatIsA(IDE_Morph),
|
|
this,
|
|
() => result = null,
|
|
function (costume) {
|
|
result = costume;
|
|
this.close();
|
|
}
|
|
);
|
|
camDialog.key = 'camera';
|
|
camDialog.popUp(this.world());
|
|
return () => result;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'mda_record',
|
|
function () {
|
|
var soundRecorder,
|
|
result = false;
|
|
soundRecorder = new SoundRecorderDialogMorph(
|
|
function (audio) {
|
|
if (audio) {
|
|
result = new Sound(audio, 'recording');
|
|
} else {
|
|
result = null;
|
|
this.destroy();
|
|
}
|
|
}
|
|
);
|
|
|
|
soundRecorder.cancel = function () {
|
|
result = null;
|
|
this.destroy();
|
|
};
|
|
|
|
soundRecorder.key = 'microphone';
|
|
soundRecorder.popUp(this.world());
|
|
return () => result;
|
|
}
|
|
);
|
|
|
|
// Database (db_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'db_store(key, val)',
|
|
function (key, value, proc) {
|
|
proc.assertType(key, ['text', 'number']);
|
|
proc.assertType(value, ['text', 'number']);
|
|
window.localStorage.setItem('-snap-project-' + key, '' + value);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'db_getall',
|
|
function () {
|
|
var str = window.localStorage,
|
|
len = str.length,
|
|
result = [],
|
|
key,
|
|
i;
|
|
for (i = 0; i < len; i += 1) {
|
|
key = str.key(i);
|
|
if (key.startsWith('-snap-project-')) {
|
|
result.push(new List([key.slice(14), str.getItem(key)]));
|
|
}
|
|
}
|
|
return new List(result);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'db_remove(key)',
|
|
function (key, proc) {
|
|
proc.assertType(key, ['text', 'number']);
|
|
window.localStorage.removeItem('-snap-project-' + key);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'db_get(key)',
|
|
function (key) {
|
|
var str = window.localStorage,
|
|
result = str.getItem('-snap-project-'+key);
|
|
if (!result) {
|
|
return false;
|
|
}
|
|
return result;
|
|
}
|
|
);
|
|
|
|
// Object properties (obj_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'obj_name(obj, name)',
|
|
function (obj, name, proc) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
proc.assertType(obj, ['sprite', 'stage', 'costume', 'sound']);
|
|
if (isSnapObject(obj)) {
|
|
obj.setName(ide.newSpriteName(name, obj));
|
|
ide.recordUnsavedChanges();
|
|
} else if (obj instanceof Costume) {
|
|
obj.name = this.newCostumeName(name, obj);
|
|
obj.version = Date.now();
|
|
ide.hasChangedMedia = true;
|
|
ide.recordUnsavedChanges();
|
|
} else if (obj instanceof Sound) {
|
|
obj.name = ide.newSoundName(name);
|
|
ide.hasChangedMedia = true;
|
|
ide.recordUnsavedChanges();
|
|
}
|
|
}
|
|
);
|
|
|
|
// Variables (var_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'var_declare(scope, name)',
|
|
function (scope, name, proc) {
|
|
var ide, frame;
|
|
proc.assertType(name, 'text');
|
|
if (name === '') {return; }
|
|
if (scope === 'script') {
|
|
frame = proc.context.isInCustomBlock() ?
|
|
proc.homeContext.variables
|
|
: proc.context.outerContext.variables;
|
|
} else if (scope === 'sprite') {
|
|
frame = this.variables;
|
|
} else {
|
|
frame = this.globalVariables();
|
|
}
|
|
if (frame.vars[name] === undefined) {
|
|
frame.addVar(name);
|
|
ide = this.parentThatIsA(IDE_Morph);
|
|
ide.flushBlocksCache('variables'); // b/c of inheritance
|
|
ide.refreshPalette();
|
|
}
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'var_delete(name)',
|
|
function (name, proc) {
|
|
var local;
|
|
proc.assertType(name, 'text');
|
|
if (name === '') {return; }
|
|
local = proc.context.isInCustomBlock() ?
|
|
proc.homeContext.variables
|
|
: proc.context.outerContext.variables;
|
|
if (local.vars[name] !== undefined) {
|
|
delete local.vars[name];
|
|
} else if (this.deletableVariableNames().indexOf(name) > -1) {
|
|
this.deleteVariable(name);
|
|
}
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'var_get(name)',
|
|
function (name, proc) {
|
|
proc.assertType(name, 'text');
|
|
return proc.homeContext.variables.getVar(name);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'var_set(name, val)',
|
|
function (name, val, proc) {
|
|
var local;
|
|
proc.assertType(name, 'text');
|
|
if (name === '') {return; }
|
|
local = proc.context.isInCustomBlock() ?
|
|
proc.homeContext.variables
|
|
: proc.context.outerContext.variables;
|
|
local.setVar(name, val);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'var_show(name)',
|
|
function (name, proc) {
|
|
proc.doShowVar(
|
|
name,
|
|
proc.context.isInCustomBlock() ?
|
|
proc.homeContext
|
|
: proc.context.outerContext
|
|
);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'var_hide(name)',
|
|
function (name, proc) {
|
|
proc.doHideVar(
|
|
name,
|
|
proc.context.isInCustomBlock() ?
|
|
proc.homeContext
|
|
: proc.context.outerContext
|
|
);
|
|
}
|
|
);
|
|
|
|
// IDE (ide_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'ide_hide(block)',
|
|
function (context, proc) {
|
|
proc.assertType(context, ['command', 'reporter', 'predicate']);
|
|
this.changeBlockVisibility(context.expression, true);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'ide_show(block)',
|
|
function (context, proc) {
|
|
proc.assertType(context, ['command', 'reporter', 'predicate']);
|
|
this.changeBlockVisibility(context.expression, false);
|
|
}
|
|
);
|
|
|
|
/*
|
|
SnapExtensions.primitives.set(
|
|
// not needed right now, commented out for possibly later
|
|
'ide_refreshpalette(name)',
|
|
function (name) {
|
|
var ide = this.parentThatIsA(IDE_Morph);
|
|
if (name !== 'variables') {
|
|
ide.flushBlocksCache(name);
|
|
}
|
|
ide.flushBlocksCache('variables'); // b/c of inheritance
|
|
ide.refreshPalette();
|
|
}
|
|
);
|
|
*/
|
|
|
|
// Colors (clr_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'clr_rgba(r, g, b, a)',
|
|
function (r, g, b, a) {
|
|
return new Color(r, g, b, a);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'clr_channel(clr, rgba)',
|
|
function (clr, rgba) {
|
|
if (contains(['r', 'g', 'b', 'a'], rgba)) {
|
|
return clr[rgba];
|
|
}
|
|
throw new Error('unknown rgba color channel "' + rgba + '"');
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'clr_hsv(clr)',
|
|
function (clr) {
|
|
return new List(clr.hsv());
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'clr_hsv(h, s, v)',
|
|
function (h, s, v) {
|
|
var c = new Color();
|
|
c.set_hsv(h, s, v);
|
|
return c;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'clr_hsl(clr)',
|
|
function (clr) {
|
|
return new List(clr.hsl());
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'clr_hsl(h, s, l)',
|
|
function (h, s, l) {
|
|
var c = new Color();
|
|
c.set_hsl(h, s, l);
|
|
return c;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'clr_setpen(clr)',
|
|
function (clr) {
|
|
this.setColor(clr);
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'clr_pen',
|
|
function () {
|
|
return this.color;
|
|
}
|
|
);
|
|
|
|
// web serial (srl_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'srl_open(baud, buffer)',
|
|
function (baud, buf, proc) {
|
|
var acc = proc.context.accumulator;
|
|
|
|
async function forceClose(port){
|
|
try {
|
|
if (!port?.writable) {return; } // already closed
|
|
// console.log("force close...", port);
|
|
if (port._reader) {await port._reader.cancel(); }
|
|
if (port?.readable) {await port.readable.cancel(); }
|
|
if (port?.writable) {await port.writable.abort(); }
|
|
if (port?.writable) {await port.close(); } // close if open
|
|
} catch (e) {
|
|
// console.log( e);
|
|
acc.result = e;
|
|
}
|
|
}
|
|
|
|
if (!acc) {
|
|
acc = proc.context.accumulator = {result: false};
|
|
(async function (baud) {
|
|
try {
|
|
var port;
|
|
port = await navigator.serial.requestPort();
|
|
await forceClose(port);
|
|
await port.open({
|
|
baudRate: baud,
|
|
bufferSize: buf || 15000
|
|
});
|
|
acc.result = port;
|
|
port._bklog = [];//backlog
|
|
} catch(e) {
|
|
acc.result = e;
|
|
}
|
|
}) (baud || 115200);
|
|
} else if (acc.result !== false) {
|
|
if (acc.result instanceof Error) {
|
|
throw acc.result;
|
|
}
|
|
return acc.result;
|
|
}
|
|
proc.pushContext('doYield');
|
|
proc.pushContext();
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'srl_close(port)',
|
|
function (port, proc) {
|
|
var acc = proc.context.accumulator;
|
|
|
|
if (!acc) {
|
|
acc = proc.context.accumulator = {result: false};
|
|
(async function (port) {
|
|
try {
|
|
// console.log("pending close...", port);
|
|
if (port._reader) {await port._reader.cancel(); }
|
|
if (port?.readable) {await port.readable.cancel(); }
|
|
if (port?.writable) {await port.writable.abort(); }
|
|
if (port?.readable || port?.writable) {await port.close(); }
|
|
acc.result = true;
|
|
} catch (e) {
|
|
// console.log(e);
|
|
acc.result = e;
|
|
}
|
|
}) (port);
|
|
} else if (acc.result !== false) {
|
|
if (acc.result instanceof Error) {
|
|
throw acc.result;
|
|
}
|
|
return;
|
|
}
|
|
proc.pushContext('doYield');
|
|
proc.pushContext();
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'srl_read(port)',
|
|
function (port, proc) {
|
|
var acc = {result: false};
|
|
if(!port?.readable) {throw Error( "Port not opened."); }
|
|
if( port.readable?.locked){ //No reentry
|
|
return (port._bklog?.length > 0) ? port._bklog.splice(0) : true;
|
|
}
|
|
(async function (port) {
|
|
var reader, data;
|
|
try {
|
|
reader = port._reader = port.readable.getReader();
|
|
data = await reader.read();
|
|
delete port._reader;
|
|
if( data.value){
|
|
port._bklog.push( ...data.value);
|
|
}
|
|
} catch (e) {
|
|
await reader.cancel();
|
|
acc.result = e;
|
|
}
|
|
if (reader) {await reader.releaseLock(); }
|
|
}) (port);
|
|
|
|
if (acc.result !== false) {
|
|
if (acc.result instanceof Error) {
|
|
throw acc.result;
|
|
}
|
|
return acc.result;
|
|
}
|
|
|
|
return (port._bklog?.length > 0) ?
|
|
new List( Array.from( port._bklog.splice(0)))
|
|
: true;
|
|
}
|
|
);
|
|
|
|
SnapExtensions.primitives.set(
|
|
'srl_write(port, bytes)',
|
|
function (port, bytes, proc) {
|
|
var acc = proc.context.accumulator;
|
|
|
|
if (!acc) {
|
|
acc = proc.context.accumulator = {result: false};
|
|
(async function (port, bytes) {
|
|
var writer;
|
|
try {
|
|
if (!port?.writable) {throw Error( "Port not opened."); }
|
|
try {
|
|
writer = port.writable.getWriter();
|
|
await writer.write(Uint8Array.from( bytes.itemsArray()));
|
|
acc.result = true;
|
|
} finally {
|
|
await writer.close();
|
|
}
|
|
} catch(e) {
|
|
acc.result = e;
|
|
}
|
|
}) (port, bytes);
|
|
|
|
} else if (acc.result !== false) {
|
|
if (acc.result instanceof Error) {
|
|
throw acc.result;
|
|
}
|
|
return;
|
|
}
|
|
proc.pushContext('doYield');
|
|
proc.pushContext();
|
|
}
|
|
);
|
|
|
|
// loading external scripts (src_):
|
|
|
|
SnapExtensions.primitives.set(
|
|
'src_load(url)',
|
|
function (url, proc) {
|
|
var scriptElement;
|
|
if (!proc.context.accumulator) {
|
|
proc.context.accumulator = {done: false};
|
|
if (contains(SnapExtensions.scripts, url)) {
|
|
return;
|
|
}
|
|
if (Process.prototype.enableJS || SnapExtensions.urls.some(
|
|
any => url.indexOf(any) === 0)
|
|
) {
|
|
scriptElement = document.createElement('script');
|
|
scriptElement.onload = () => {
|
|
SnapExtensions.scripts.push(url);
|
|
proc.context.accumulator.done = true;
|
|
};
|
|
document.head.appendChild(scriptElement);
|
|
scriptElement.src = url;
|
|
} else {
|
|
throw new Error(
|
|
'unlisted extension url:\n"' + url + '"\n' +
|
|
'JavaScript extensions for Snap!\nare turned off'
|
|
);
|
|
}
|
|
} else if (proc.context.accumulator.done) {
|
|
return;
|
|
}
|
|
proc.pushContext('doYield');
|
|
proc.pushContext();
|
|
}
|
|
);
|
|
|
|
// Menus
|
|
|
|
SnapExtensions.menus.set(
|
|
'clr_numbers', // Brian's browns and oranges, sigh...
|
|
function () {
|
|
var menuName = this.parent.inputs()[0].evaluate(), // first slot
|
|
output,
|
|
menus = {
|
|
'color number': [
|
|
"0 black=0",
|
|
"14 white=14",
|
|
"20 spectral red=20",
|
|
"25 darkest red=25",
|
|
"30 saddle brown=30",
|
|
"35 darkest brown=35",
|
|
"40 spectral orange=40",
|
|
"45 darkest orange=45",
|
|
"50 spectral yellow=50",
|
|
"55 darkest yellow=55",
|
|
"60 spectral green=60",
|
|
"65 darkest green=65",
|
|
"70 spectral cyan=70",
|
|
"75 darkest cyan=75",
|
|
"80 spectral blue=80",
|
|
"85 darkest blue=85",
|
|
"90 spectral violet=90",
|
|
"95 magenta=95"
|
|
],
|
|
'fair hue': [
|
|
"0 red=0",
|
|
"12.5 brown=12.5",
|
|
"25 orange=25",
|
|
"37.5 yellow=37.5",
|
|
"50 green=50",
|
|
"62.5 cyan=62.5",
|
|
"75 blue=75",
|
|
"87.5 violet=87.5"
|
|
],
|
|
'crayon': [
|
|
"grays",
|
|
[
|
|
"0 black #000000=0",
|
|
"1 gray7 #121212=1",
|
|
"2 gray14 #242424=2",
|
|
"3 gray21 #363636=3",
|
|
"4 gray28 #484848=4",
|
|
"5 gray36 #5c5c5c=5",
|
|
"6 gray43 #6d6d6d=6",
|
|
"7 gray50 #7f7f7f=7",
|
|
"8 gray57 #919191=8",
|
|
"9 gray64 #a3a3a3=9",
|
|
"10 gray71 #b5b5b5=10",
|
|
"11 gray78 #c8c8c8=11",
|
|
"12 gray85 #dadada=12",
|
|
"13 gray92 #ececec=13",
|
|
"14 white #ffffff=14"
|
|
],
|
|
"pinks",
|
|
[
|
|
"15 deep pink #ff1493=15",
|
|
"16 hot pink #ff69b4=16",
|
|
"17 bright pink #ff007f=17",
|
|
"18 raspberry #e30b5d=18",
|
|
"19 amaranth #e52b50=19"
|
|
],
|
|
"reds",
|
|
[
|
|
"20 red #ff0000=20",
|
|
"21 burgundy #900020=21",
|
|
"22 cherry #990000=22",
|
|
"23 dark candy apple red #a40000=23",
|
|
"24 sanguine #c00000=24",
|
|
"25 maroon #800000=25",
|
|
"26 crimson #c90016=26",
|
|
"27 Lists #d94d11=27",
|
|
"28 candy apple red #ff0800=28",
|
|
"29 coquelicot #ff3800=29"
|
|
],
|
|
"browns",
|
|
[
|
|
"30 saddle brown #8b4513=30",
|
|
"31 chocolate #7b3f00=31",
|
|
"32 kobicha #6b4423=32",
|
|
"33 sepia #704214=33",
|
|
"34 chestnut #954535=34",
|
|
"35 dark brown #654321=35",
|
|
"36 brown #964b00=36",
|
|
"37 golden brown #996515=37",
|
|
"38 cinnamon #b87333=38",
|
|
"39 copper #d2691e=39"
|
|
],
|
|
"oranges",
|
|
[
|
|
"40 orange #ff7f00=40",
|
|
"41 Pantone orange #ff5800=41",
|
|
"42 pumpkin #ff7518=42",
|
|
"43 Variables #f3761d=43",
|
|
"44 Spanish orange #e86100=44",
|
|
"45 burnt orange #cc5500=45",
|
|
"46 sinopia #cb410b=46",
|
|
"47 ochre #cc7722=47",
|
|
"48 carrot #ed9121=48",
|
|
"49 tangerine #f28500=49"
|
|
],
|
|
"yellows",
|
|
[
|
|
"50 yellow #ffff00=50",
|
|
"51 Control #e6a822=51",
|
|
"52 dark goldenrod #b8860b=52",
|
|
"53 goldenrod #daa520=53",
|
|
"54 saffron #f4c430=54",
|
|
"55 sandstorm #ecd540=55",
|
|
"56 mustard #ffdb58=56",
|
|
"57 gold #ffd700=57",
|
|
"58 egg yolk #fee33e=58",
|
|
"59 rubber duck #fbe108=59"
|
|
],
|
|
"greens",
|
|
[
|
|
"60 lime #00ff00=60",
|
|
"61 apple green #8db600=61",
|
|
"62 Operators #62c213=62",
|
|
"63 forest green #228b22=63",
|
|
"64 green #008000=64",
|
|
"65 dark green #006400=65",
|
|
"66 dark pastel green #03c03c=66",
|
|
"67 emerald #50c878=67",
|
|
"68 mint #3eb489=68",
|
|
"69 Pen #00a178=69"
|
|
],
|
|
"cyans",
|
|
[
|
|
"70 aqua (cyan) #00ffff=70",
|
|
"71 dark cyan #008b8b=71",
|
|
"72 cerulean #007ba7=72",
|
|
"73 iceberg #71a6d2=73",
|
|
"74 Sensing #0494dc=74",
|
|
"75 teal #008080=75",
|
|
"76 light sky blue #87cefa=76",
|
|
"77 deep sky blue #00bfff=77",
|
|
"78 dodger blue #1e90ff=78",
|
|
"79 azure #007fff=79"
|
|
],
|
|
"blues",
|
|
[
|
|
"80 blue #0000ff=80",
|
|
"81 midnight blue #191970=81",
|
|
"82 dark powder blue #003399=82",
|
|
"83 cobalt #0047ab=83",
|
|
"84 denim #1560bd=84",
|
|
"85 navy blue #000080=85",
|
|
"86 steel blue #4682b4=86",
|
|
"87 Motion #4a6cd4=87",
|
|
"88 cornflower #6495ed=88",
|
|
"89 slate blue #6a5acd=89"
|
|
],
|
|
"purples",
|
|
[
|
|
"90 violet #8000ff=90",
|
|
"91 Looks #8f56e3=91",
|
|
"92 grape #6f2da8=92",
|
|
"93 indigo #4b0082=93",
|
|
"94 x11 purple #a020f0=94",
|
|
"95 magenta (fuchia) #ff00ff=95",
|
|
"96 dark orchid #9932cc=96",
|
|
"97 Sound #cf4ad9=97",
|
|
"98 purple #7f007f=98",
|
|
"99 dark magenta #8b008b=99"
|
|
]
|
|
]
|
|
};
|
|
|
|
function makeMenuHelper(items, output) {
|
|
// in an array, walk through the items in pairs
|
|
var i = 0,
|
|
label, possiblyNested, hasEquals, nestingOutput;
|
|
while (i < items.length) {
|
|
label = items[i];
|
|
possiblyNested = items[i + 1];
|
|
// if possiblyNested is array, it is a nest under label
|
|
// if possiblyNested is string, it is just a sibling
|
|
if (possiblyNested === undefined) {
|
|
// label is actually the last element of the list
|
|
hasEquals = label.split("=");
|
|
if (hasEquals.length === 2) {
|
|
output[hasEquals[0]] = hasEquals[1];
|
|
i += 1;
|
|
} else if (hasEquals.length === 3) {
|
|
output[
|
|
hasEquals[0]+"\u00A0"+"="+"\u00A0"+hasEquals[2]
|
|
] = hasEquals[0]+"\u00A0"+"="+"\u00A0"+hasEquals[2];
|
|
i += 1;
|
|
} else {
|
|
output[label] = label;
|
|
i += 1;
|
|
}
|
|
} else if (typeof possiblyNested == "string") {
|
|
hasEquals = label.split("=");
|
|
if (hasEquals.length == 2) {
|
|
output[hasEquals[0]] = hasEquals[1];
|
|
i += 1;
|
|
} else if (hasEquals.length == 3) {
|
|
output[
|
|
hasEquals[0]+"\u00A0"+"="+"\u00A0"+hasEquals[2]
|
|
] = hasEquals[0]+"\u00A0"+"="+"\u00A0"+hasEquals[2];
|
|
i += 1;
|
|
} else {
|
|
output[label] = label;
|
|
i += 1;
|
|
}
|
|
} else if (Array.isArray(possiblyNested)) {
|
|
nestingOutput = {};
|
|
makeMenuHelper(possiblyNested, nestingOutput);
|
|
output[label] = nestingOutput;
|
|
i += 2;
|
|
} else {
|
|
throw new Error("Bad value at index " + i);
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
output = {};
|
|
makeMenuHelper(menus[menuName], output);
|
|
return output;
|
|
} catch(err) {
|
|
nop(err);
|
|
}
|
|
}
|
|
);
|