Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
782 changes: 782 additions & 0 deletions README.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions css/activities.css
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,8 @@ table {
max-width: 80%;
}



.chatInterface{
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -2070,3 +2072,17 @@ table {
align-self: flex-end;
background-color: #DCF8C6;
}


.lego-brick {
display: inline-block;
background-color: #FF0000;
border: 1px solid #880000;
margin: 2px;
}

.lego-size-1 { width: 20px; height: 10px; }
.lego-size-2 { width: 40px; height: 10px; }
/* ... more sizes ... */


12 changes: 12 additions & 0 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,15 @@ input[type="range"]:focus::-ms-fill-lower {
input[type="range"]:focus::-ms-fill-upper {
background: #90c100;
}

.lego-brick {
display: inline-block;
background-color: #FF0000;
border: 1px solid #880000;
margin: 2px;
}

.lego-size-1 { width: 20px; height: 10px; }
.lego-size-2 { width: 40px; height: 10px; }
/* ... more sizes ... */

5 changes: 5 additions & 0 deletions header-icons/upload-button.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions header-icons/webcam-button.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions js/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ if (_THIS_IS_MUSIC_BLOCKS_) {
"widgets/oscilloscope",
"widgets/sampler",
"widgets/reflection",
"widgets/legobricks",
"activity/lilypond",
"activity/abc",
"activity/midi",
Expand Down
1 change: 1 addition & 0 deletions js/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -4246,6 +4246,7 @@ class Block {
case "pitch staircase":
case "status":
case "phrase maker":
case "lego bricks":
case "custom mode":
case "music keyboard":
case "pitch drum":
Expand Down
2 changes: 2 additions & 0 deletions js/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,7 @@ class Blocks {
case "pitch staircase":
case "status":
case "phrase maker":
case "lego bricks":
case "custom mode":
case "music keyboard":
case "pitch drum":
Expand Down Expand Up @@ -2035,6 +2036,7 @@ class Blocks {
case "pitch staircase":
case "status":
case "phrase maker":
case "lego bricks":
case "custom mode":
case "music keyboard":
case "pitch drum":
Expand Down
11 changes: 11 additions & 0 deletions js/blocks/PitchBlocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,17 @@ function setupPitchBlocks(activity) {
// convert hertz to note/octave
const note = obj;
tur.singer.lastNotePlayed = [note[0] + note[1], 4];
} else if (logo.inLegoWidget) {
logo.legoWidget.addRowBlock(blk);
if (!logo.pitchBlocks.includes(blk)) {
logo.pitchBlocks.push(blk);
}

logo.legoWidget.rowLabels.push(activity.blocks.blockList[blk].name);
logo.legoWidget.rowArgs.push(arg.toFixed(0));
// convert hertz to note/octave
const note = obj;
tur.singer.lastNotePlayed = [note[0] + note[1], 4];
} else if (logo.inMusicKeyboard) {
logo.musicKeyboard.instruments.push(last(tur.singer.instrumentNames));
logo.musicKeyboard.noteNames.push("hertz");
Expand Down
5 changes: 5 additions & 0 deletions js/blocks/RhythmBlockPaletteBlocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ function setupRhythmBlockPaletteBlocks(activity) {
if (logo.inMatrix || logo.tuplet) {
if (logo.inMatrix) {
logo.phraseMaker.addColBlock(blk, arg0);

// Add individual entries for each beat to avoid extra × blocks
for (let i = 0; i < arg0; i++) {
logo.tupletRhythms.push(["individual", 1, noteBeatValue]);
}
}

for (let i = 0; i < args[0]; i++) {
Expand Down
100 changes: 93 additions & 7 deletions js/blocks/WidgetBlocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
RhythmRuler, FILTERTYPES, instrumentsFilters, DEFAULTFILTERTYPE,
TemperamentWidget, TimbreWidget, ModeWidget, PitchSlider,
MusicKeyboard, PitchStaircase, SampleWidget, _THIS_IS_MUSIC_BLOCKS_,
Arpeggio
Arpeggio, LegoWidget
*/

/*
Expand Down Expand Up @@ -60,8 +60,10 @@
MusicKeyboard
- js/widgets/pitchstaircase.js
PitchStaircase
- js/widgets/aidebugger.js
- js/widgets/aidebugger.js
AIDebuggerWidget
- js/widgets/legobricks.js
LegoWidget

*/

Expand Down Expand Up @@ -1639,8 +1641,7 @@ function setupWidgetBlocks(activity) {
return [args[0], 1];
}
}



class ReflectionBlock extends StackClampBlock {
/**
* Creates a ReflectionBlock instance.
Expand Down Expand Up @@ -1695,7 +1696,93 @@ function setupWidgetBlocks(activity) {
}
}



/**
* Represents a block for controlling LEGO brick parameters and visualization.
* @extends StackClampBlock
*/
class LegoBricksBlock extends StackClampBlock {
constructor() {
super("legobricks");
this.setPalette("widgets", activity);
this.parameter = true;
this.beginnerBlock(true);

this.setHelpString([
_("The LEGO Bricks block opens a widget for designing virtual LEGO creations."),
"documentation",
null,
"legobricks"
]);

//.TRANS: LEGO bricks designer
this.formBlock({ name: _("LEGO Bricks"), canCollapse: true });
this.makeMacro((x, y) => [
[0, "legobricks", x, y, [null, 1, 18]],
[1, "pitch", 0, 0, [0, 2, 3, 4]],
[2, ["solfege", { value: "do" }], 0, 0, [1]],
[3, ["number", { value: 4 }], 0, 0, [1]],
[4, "pitch", 0, 0, [1, 5, 6, 7]],
[5, ["solfege", { value: "re" }], 0, 0, [4]],
[6, ["number", { value: 4 }], 0, 0, [4]],
[7, "pitch", 0, 0, [4, 8, 9, 10]],
[8, ["solfege", { value: "mi" }], 0, 0, [7]],
[9, ["number", { value: 4 }], 0, 0, [7]],
[10, "pitch", 0, 0, [7, 11, 12, 13]],
[11, ["solfege", { value: "fa" }], 0, 0, [10]],
[12, ["number", { value: 4 }], 0, 0, [10]],
[13, "pitch", 0, 0, [10, 14, 15, 16]],
[14, ["solfege", { value: "sol" }], 0, 0, [13]],
[15, ["number", { value: 4 }], 0, 0, [13]],
[16, "playdrum", 0, 0, [13, 17, null]],
[17, ["drumname", { value: "kick drum" }], 0, 0, [16]],
[18, "hiddennoflow", 0, 0, [0, null]]
]);
}

/**
* Handles the flow of data for the LEGO bricks block.
* @param {any[]} args - The arguments passed to the block.
* @param {object} logo - The logo object.
* @param {object} turtle - The turtle object.
* @param {object} blk - The block object.
* @returns {number[]} - The output values.
*/
flow(args, logo, turtle, blk) {
logo.inLegoWidget = true;

if (logo.legoWidget === null) {
logo.legoWidget = new LegoWidget();
}
logo.legoWidget.blockNo = blk;

logo.legoWidget.rowLabels = [];
logo.legoWidget.rowArgs = [];
logo.legoWidget.clearBlocks();

const listenerName = "_legobricks_" + turtle;
logo.setDispatchBlock(blk, turtle, listenerName);

const __listener = () => {
if (logo.legoWidget.rowLabels.length === 0) {
activity.errorMsg(
_("You must have at least one pitch block in the LEGO bricks widget."),
blk
);
} else {
logo.legoWidget.blockNo = blk;
logo.legoWidget.init(activity);
}
logo.inLegoWidget = false;
};

logo.setTurtleListener(turtle, listenerName, __listener);

if (args.length === 1) return [args[0], 1];
}
}


class AIDebugger extends StackClampBlock {
constructor() {
super("aidebugger");
Expand Down Expand Up @@ -1745,8 +1832,6 @@ function setupWidgetBlocks(activity) {
return [args[0], 1];
}
}


// Set up blocks if this is Music Blocks environment
if (_THIS_IS_MUSIC_BLOCKS_) {
new EnvelopeBlock().setup(activity);
Expand All @@ -1762,6 +1847,7 @@ function setupWidgetBlocks(activity) {
new oscilloscopeWidgetBlock().setup(activity);
new PitchSliderBlock().setup(activity);
new ChromaticBlock().setup(activity);
new LegoBricksBlock().setup(activity);
new ReflectionBlock().setup(activity);
// new AIMusicBlocks().setup(activity);
new MusicKeyboard2Block().setup(activity);
Expand Down
4 changes: 4 additions & 0 deletions js/logo.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class Logo {
// Widgets
this.reflection = null;
this.phraseMaker = null;
this.legoWidget = null;
this.pitchDrumMatrix = null;
this.arpeggio = null;
this.rhythmRuler = null;
Expand All @@ -126,6 +127,7 @@ class Logo {
this.oscilloscopeTurtles = [];
this.meterWidget = null;
this.statusMatrix = null;
this.legobricks = null;

this.evalFlowDict = {};
this.evalArgDict = {};
Expand Down Expand Up @@ -184,6 +186,7 @@ class Logo {

// pitch-rhythm matrix
this.inMatrix = false;
this.inLegoWidget = false;
this.tupletRhythms = [];
this.addingNotesToTuplet = false;
this.drumBlocks = [];
Expand Down Expand Up @@ -1064,6 +1067,7 @@ class Logo {

this.inPitchDrumMatrix = false;
this.inMatrix = false;
this.inLegoWidget = false;
this.inMusicKeyboard = false;
this.inTimbre = false;
this.inArpeggio = false;
Expand Down
65 changes: 65 additions & 0 deletions js/turtle-singer.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ class Singer {

logo.phraseMaker.rowLabels.push(activity.logo.blocks.blockList[blk].name);
logo.phraseMaker.rowArgs.push(args[0]);
} else if (logo.inLegoWidget && !logo.inMatrix) {
logo.legoWidget.addRowBlock(blk);
if (!logo.pitchBlocks.includes(blk)) {
logo.pitchBlocks.push(blk);
}

logo.legoWidget.rowLabels.push(activity.logo.blocks.blockList[blk].name);
logo.legoWidget.rowArgs.push(args[0]);
} else if (logo.inPitchSlider) {
logo.pitchSlider.frequency = args[0];
} else {
Expand Down Expand Up @@ -910,6 +918,56 @@ class Singer {
activity.logo.phraseMaker.rowArgs.push(noteObj[1]);
}
}
} else if (activity.logo.inLegoWidget && !activity.logo.inMatrix) {
if (note.toLowerCase() !== "rest") {
activity.logo.legoWidget.addRowBlock(blk);
if (!activity.logo.pitchBlocks.includes(blk)) {
activity.logo.pitchBlocks.push(blk);
}
}

const duplicateFactor =
tur.singer.duplicateFactor.length > 0 ? tur.singer.duplicateFactor : 1;

for (let i = 0; i < duplicateFactor; i++) {
// Apply transpositions
const transposition = 2 * delta + tur.singer.transposition;
const alen = tur.singer.arpeggio.length;
let atrans = transposition + cents;
if (alen > 0 && i < alen) {
atrans += tur.singer.arpeggio[i];
}
const noteObj = getNote(
note,
octave,
atrans, // transposition,
tur.singer.keySignature,
tur.singer.movable,
null,
activity.errorMsg,
activity.logo.synth.inTemperament
);
tur.singer.previousNotePlayed = tur.singer.lastNotePlayed;
tur.singer.lastNotePlayed = [noteObj[0] + noteObj[1], 4];

if (
tur.singer.keySignature[0] === "C" &&
tur.singer.keySignature[1].toLowerCase() === "major" &&
noteIsSolfege(note)
) {
noteObj[0] = getSolfege(noteObj[0]);
}

// If we are in a setdrum clamp, override the pitch.
if (tur.singer.drumStyle.length > 0) {
activity.logo.legoWidget.rowLabels.push(last(tur.singer.drumStyle));
activity.logo.legoWidget.rowArgs.push(-1);
} else {
// Don't bother with the name conversions.
activity.logo.legoWidget.rowLabels.push(noteObj[0]);
activity.logo.legoWidget.rowArgs.push(noteObj[1]);
}
}
} else if (tur.singer.inNoteBlock.length > 0) {
// maybe of interest
tur.singer.inverted = tur.singer.invertList.length > 0;
Expand Down Expand Up @@ -1479,6 +1537,13 @@ class Singer {
);
}
}
// Note: tupletRhythms will be populated by the rhythm blocks themselves
} else if (activity.logo.inLegoWidget && !activity.logo.inMatrix) {
// For LEGO widget, we don't need to handle rhythm blocks currently
// Just store the note information
if (tur.singer.inNoteBlock.length > 0) {
// Could add timing information here if needed in the future
}

noteBeatValue *= tur.singer.beatFactor;
if (activity.logo.tuplet) {
Expand Down
Loading