Skip to content

Commit

Permalink
Merge branch 'master' into ext-index-json
Browse files Browse the repository at this point in the history
  • Loading branch information
dragazo committed May 7, 2024
2 parents 1f99e3c + a06e5a4 commit 2f84b13
Show file tree
Hide file tree
Showing 198 changed files with 17,219 additions and 5,191 deletions.
2 changes: 0 additions & 2 deletions .githooks/pre-commit

This file was deleted.

26 changes: 26 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Publish

on:
push:
branches: ["master"]
pull_request:

permissions:
pages: write
id-token: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm i && node build.js
- uses: actions/upload-pages-artifact@v1
with:
path: build
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
- uses: actions/deploy-pages@v2
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
node_modules
.DS_Store
<<<<<<< HEAD
package-lock.json
.DS_Store
build/
=======
.DS_Store
.vscode
>>>>>>> master
33 changes: 6 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,13 @@

This repository contains the NetsBlox Extensions to be hosted on https://extensions.netsblox.org, allowing NetsBlox to recognize them as first-party extensions.

Extensions currently included in this repository:

- [BeatsBlox](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/BeatsBlox/index.js%22]#) - BeatsBlox extends Music Functionality within NetsBlox

- [BetterShare](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/BetterShare/index.js%22]#) - WIP - Provides a few utilities that can make sharing projects easier

- [HandGestures](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/HandGestures/index.js%22]#) - Track 3D hand gestures in images/video using MediaPipe!

- [HideCategories](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/HideCategories/index.js%22]#) - This extension allows you to automatically hide categories and is particularly useful when setting different visible categories for collaborating users.

- [🤖 RoboScape Online](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/RoboScapeOnline/index.js%22]#) - Networked robotics simulation in the browser! (WIP)

- [TimeSync](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/TimeSync/index.js%22]#) - calculate time sync info from the NetsBlox server

- [TuneScope](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/TuneScope/index.js%22]#) - Music Notation, Instruments, Drums, Tones, Chords, Tracks, from the University of Virginia (Glen Bull)

- [WebSerial](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/WebSerial/index.js%22]#) - Provides blocks for connecting to a device, e.g. an Arduino, over WebSerial

- [WhenKeyPressedLogger](https://dev.netsblox.org/?extensions=[%22https://extensions.netsblox.org/extensions/WhenKeyPressedLogger/index.js%22]#) - Logs 'When [key] key pressed' block activations for Ben

View currently published extensions at https://extensions.netsblox.org/

## Contributing
After cloning the repository, configure the githooks with:
```
git config core.hooksPath .githooks
```
This will ensure that any automated preparation will happen automatically such as updating the website.

Next, create a new directory in `extensions/`. This should contain the following files:
To make a new extension, simply create a new directory in `extensions/`. This should contain the following files:

- `index.js`: JS code for the actual extension
- `extension.json`: Description of the extension
- `extension.json`: Description of the extension

You can copy from an existing extension to see the required format for these files.
40 changes: 40 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const EXTENSIONS_DIR = path.join(__dirname, 'extensions');
const OUT_DIR = path.join(__dirname, 'build');

const extensions = fs.readdirSync(EXTENSIONS_DIR).map(readExtension);

try { fs.mkdirSync(OUT_DIR); } catch {}

fs.writeFileSync(path.join(OUT_DIR, 'index.html'), renderTemplate('index.html', { extensions }));
fs.copyFileSync('index.js', path.join(OUT_DIR, 'index.js'));

// ------------------------------------------

function readExtension(name) {
const dirpath = path.join(EXTENSIONS_DIR, name);
const settings = JSON.parse(fs.readFileSync(path.join(dirpath, 'extension.json'), 'utf8'));
const description = settings['description'];
let linkUrl = "";
let scriptUrl = `https://extensions.netsblox.org/extensions/${name}/index.js`;

if (!settings['useDev']) {
linkUrl = `https://editor.netsblox.org/?extensions=[%22${scriptUrl}%22]#`;
} else {
linkUrl = `https://dev.netsblox.org/?extensions=[%22${scriptUrl}%22]#`;
}

return {
name,
displayName : settings['customName'] ?? name,
description,
linkUrl,
scriptUrl,
};
}

function renderTemplate(name, data) {
return _.template(fs.readFileSync(name, 'utf8'))(data).trim();
}
4 changes: 4 additions & 0 deletions extensions/AugmentedReality/extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"description": "Use QR codes to pin sprites to the world!",
"useDev": false
}
238 changes: 238 additions & 0 deletions extensions/AugmentedReality/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
(async function () {

let videoMirrored = false;

const dictionaries = ['APRILTAG_16h5', 'APRILTAG_16h5_mini','APRILTAG_16h5_duo'];

const localhost = window.location.search.includes('localhost');
const root = localhost? 'http://localhost:8000/' : 'https://extensions.netsblox.org/';

const rendererURL = root + 'extensions/AugmentedReality/js/renderModule.mjs';
const tagURL = root + 'extensions/AugmentedReality/js/tagHandler.mjs';

const renderModule = await import(rendererURL);
const tagModule = await import(tagURL);

function snapify(value) {
if (Array.isArray(value)) {
const res = [];
for (const item of value) res.push(snapify(item));
return new List(res);
} else if (typeof(value) === 'object') {
const res = [];
for (const key in value) res.push(new List([key, snapify(value[key])]));
return new List(res);
} else return value;
}

class AugmentedReality extends Extension {
constructor(ide) {
super('AugmentedReality');
this.ide = ide;
}

onOpenRole() {
videoMirrored = this.ide.stage.mirrorVideo;
}

getMenu() { return {

'Code Generator': function () {
new ArucoGenMorph().popUp(world);
},

}; }

getCategories() { return []; }

getPalette() {
const blocks = [
new Extension.Palette.Block('ARCodeTracker'),
new Extension.Palette.Block('ARCodeRender'),
'-',
new Extension.Palette.Block('ARCodeFlag'),
new Extension.Palette.Block('ARCodeVisibleArray'),
'-',
new Extension.Palette.Block('ARCodeSetDictionary'),
new Extension.Palette.Block('ARCodeDictionary').withWatcherToggle(),
'-',
new Extension.Palette.Block('ARCodeFlipVideo'),
new Extension.Palette.Block('ARCodeFlipped').withWatcherToggle(),
'-'
];
return [
new Extension.PaletteCategory('sensing', blocks, SpriteMorph),
new Extension.PaletteCategory('sensing', blocks, StageMorph),
];
}

getBlocks() {
function block(name, type, category, spec, defaults, action) {
return new Extension.Block(name, type, category, spec, defaults, action).for(SpriteMorph, StageMorph)
}
return [
block('ARCodeTracker', 'reporter', 'sensing', 'find AR code %s', [], function (image) {
return this.runAsyncFn(async () => {

image = image?.contents || image;
if (!image || typeof(image) !== 'object' || !image.width || !image.height)
throw TypeError('Expected an image as input');

const coordinates = await tagModule.getCoordinates(image);
const res = await tagModule.transformCoordinates(coordinates, image);

return snapify(res);

}, { args: [], timeout: 10000 });
}),


block('ARCodeRender', 'reporter', 'sensing', 'render %model on %s', ['box'], function (model, image) {
return this.runAsyncFn(async () => {

image = image?.contents || image;
if (!image || typeof(image) !== 'object' || !image.width || !image.height){
throw TypeError('Expected an image as input');
}
const res = await renderModule.renderScene(image, model);

return new Costume(res);

}, { args: [], timeout: 10000 });
}),


block('ARCodeFlag', 'predicate', 'sensing', 'AR code %n visible in %s ?', [], function (value, image) {
return this.runAsyncFn(async () => {

image = image?.contents || image;
if (!image || typeof(image) !== 'object' || !image.width || !image.height){
throw TypeError('Expected an image as input');
}

console.log(value);
value = value?.contents || value;

if(typeof(value) === 'number'){
const temp = Array();
temp.push(value);
value = temp;
}
if (!value || !value.length){
throw TypeError('Expected number or list');
}

for(let i = 0; i < value.length; i++){
if(typeof(value[i]) === 'string' && !(value[i] = parseInt(value[i]))){
throw TypeError('list elements must be numbers');
}
}

const visible = await tagModule.isTagVisible(image, value);

return snapify(visible);

}, { args: [], timeout: 10000 });
}),


block('ARCodeVisibleArray', 'reporter', 'sensing', 'All AR codes visible in %s', [], function (image) {
return this.runAsyncFn(async () => {

image = image?.contents || image;
if (!image || typeof(image) !== 'object' || !image.width || !image.height){
throw TypeError('Expected an image as input');
}

const visible = await tagModule.getVisibleTags(image);

return snapify(visible);

}, { args: [], timeout: 10000 });
}),


block('ARCodeSetDictionary', 'command', 'sensing', 'set dictionary to %dictionaries', ['APRILTAG_16h5'], function (dict) {
return this.runAsyncFn(async () => {

console.log(dict, dictionaries.indexOf(dict) );
if(dictionaries.indexOf(dict) === -1){
return new Error(dict, 'is not a valid dictionary.');
}

tagModule.setDictionary(dict)

}, { args: [], timeout: 10000 });
}),


block('ARCodeDictionary', 'reporter', 'sensing', 'dictionary', [], function () {
return tagModule.getDictionary();
}),


block('ARCodeFlipVideo', 'command', 'sensing', 'flip video', [], function () {
return this.runAsyncFn(async () => {

world.children[0].stage.mirrorVideo = !world.children[0].stage.mirrorVideo;
videoMirrored = world.children[0].stage.mirrorVideo;

}, { args: [], timeout: 10000 });
}),


block('ARCodeFlipped', 'reporter', 'sensing', 'flipped?', [], function () {

videoMirrored = world.children[0].stage.mirrorVideo;
return snapify(videoMirrored);
})
];
}


getLabelParts() {
function identityMap(s) {
const res = {};
for (const x of s) res[x] = x;
return res;
}

function unionMaps(maps) {
const res = {};
for (const map of maps) {
for (const key in map) res[key] = map[key];
}
return res;
}
return [
new Extension.LabelPart('model', () => new InputSlotMorph(
null, // text
false, // numeric
identityMap(['box', 'drum', 'piano']),
true
)),
new Extension.LabelPart('dictionaries', () => new InputSlotMorph(
null, // text
false, // numeric
identityMap(dictionaries),
true
)),
];
}
}

const sources = [root + 'extensions/AugmentedReality/js/ui-morphs.js'];

for(const source of sources){
const script = document.createElement('script');
script.class = 'ARUIScripts';
script.type = 'text/javascript';
script.src = source;
script.async = false;
script.crossOrigin = 'anonymous';
document.body.appendChild(script);
}

NetsBloxExtensions.register(AugmentedReality);
disableRetinaSupport();
})();
29 changes: 29 additions & 0 deletions extensions/AugmentedReality/js/filters.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {OneEuroFilter} from 'https://esm.run/@david18284/[email protected]';

class singleExpFilter {
constructor(startVals, alpha) {
this.oldVals = startVals;
this.a = alpha;
}

filter(...args) {
const res = new Array();
const diff = this.oldVals.length - args.length;
if(diff < 0){
for(let i = args.length; i > this.oldVals.length; i--){
this.oldVals.push(args[i-1]);
console.log(this.oldVals)
}
}

for(let i = 0; i < args.length; i++){
const newVal = (this.a * this.oldVals[i]) + ((1 - this.a) * args[i]);
res.push(newVal);
}
this.oldVals = res;

return res;
}
}

export {singleExpFilter, OneEuroFilter}
Loading

0 comments on commit 2f84b13

Please sign in to comment.