Skip to content

Commit f0be2bc

Browse files
committed
refactor(phrasemaker): extract grid logic into PhraseMakerGrid module
1 parent 971a9c6 commit f0be2bc

File tree

3 files changed

+272
-161
lines changed

3 files changed

+272
-161
lines changed

js/activity.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ if (_THIS_IS_MUSIC_BLOCKS_) {
165165
"widgets/modewidget",
166166
"widgets/meterwidget",
167167
"widgets/PhraseMakerUtils",
168+
"widgets/PhraseMakerGrid",
168169
"widgets/phrasemaker",
169170
"widgets/arpeggio",
170171
"widgets/aiwidget",

js/widgets/PhraseMakerGrid.js

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
// Copyright (c) 2026 Music Blocks contributors
2+
//
3+
// This program is free software; you can redistribute it and/or
4+
// modify it under the terms of the The GNU Affero General Public
5+
// License as published by the Free Software Foundation; either
6+
// version 3 of the License, or (at your option) any later version.
7+
//
8+
// You should have received a copy of the GNU Affero General Public
9+
// License along with this library; if not, write to the Free Software
10+
// Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA
11+
12+
/**
13+
* @file PhraseMakerGrid.js
14+
* @description Matrix/grid state handling and data structure updates for PhraseMaker.
15+
*/
16+
17+
const PhraseMakerGrid = {
18+
/**
19+
* Clears block references within the PhraseMaker.
20+
* @param {Object} pm - The PhraseMaker instance.
21+
*/
22+
clearBlocks(pm) {
23+
// When creating a new matrix, we want to clear out any old
24+
// block references.
25+
pm._rowBlocks = [];
26+
pm._colBlocks = [];
27+
pm._rowMap = [];
28+
pm._rowOffset = [];
29+
},
30+
31+
/**
32+
* Adds a row block to the PhraseMaker matrix.
33+
* @param {Object} pm - The PhraseMaker instance.
34+
* @param {number} rowBlock - The pitch or drum block identifier to add to the matrix row.
35+
*/
36+
addRowBlock(pm, rowBlock) {
37+
// When creating a matrix, we add rows whenever we encounter a
38+
// pitch or drum block (and some graphics blocks).
39+
pm._rowMap.push(pm._rowBlocks.length);
40+
pm._rowOffset.push(0);
41+
// In case there is a repeat block, use a unique block number
42+
// for each instance.
43+
while (pm._rowBlocks.includes(rowBlock)) {
44+
rowBlock = rowBlock + 1000000;
45+
}
46+
47+
pm._rowBlocks.push(rowBlock);
48+
},
49+
50+
/**
51+
* Adds a column block to the PhraseMaker matrix.
52+
* @param {Object} pm - The PhraseMaker instance.
53+
* @param {number} rhythmBlock - The rhythm block identifier to add to the matrix column.
54+
* @param {number} n - The index of the rhythm block within the matrix column.
55+
*/
56+
addColBlock(pm, rhythmBlock, n) {
57+
// When creating a matrix, we add columns when we encounter
58+
// rhythm blocks.
59+
// Search for previous instance of the same block (from a
60+
// repeat).
61+
let startIdx = 0;
62+
let obj;
63+
for (let i = 0; i < pm._colBlocks.length; i++) {
64+
obj = pm._colBlocks[i];
65+
if (obj[0] === rhythmBlock) {
66+
startIdx += 1;
67+
}
68+
}
69+
70+
for (let i = startIdx; i < n + startIdx; i++) {
71+
pm._colBlocks.push([rhythmBlock, i]);
72+
}
73+
},
74+
75+
/**
76+
* Adds a node to the PhraseMaker matrix.
77+
* @param {Object} pm - The PhraseMaker instance.
78+
* @param {number} rowBlock - The pitch or drum block associated with the node.
79+
* @param {number} rhythmBlock - The rhythm block associated with the node.
80+
* @param {number} n - The index of the rhythm block within its column.
81+
* @param {number} blk - The block identifier representing the matrix cell.
82+
*/
83+
addNode(pm, rowBlock, rhythmBlock, n, blk) {
84+
// A node exists for each cell in the matrix. It is used to
85+
// preserve and restore the state of the cell.
86+
if (pm._blockMap[blk] === undefined) {
87+
pm._blockMap[blk] = [];
88+
}
89+
90+
let j = 0;
91+
let obj;
92+
for (let i = 0; i < pm._blockMap[blk].length; i++) {
93+
obj = pm._blockMap[blk][i];
94+
if (obj[0] === rowBlock && obj[1][0] === rhythmBlock && obj[1][1] === n) {
95+
j += 1;
96+
}
97+
}
98+
99+
pm._blockMap[blk].push([rowBlock, [rhythmBlock, n], j]);
100+
},
101+
102+
/**
103+
* Removes a node from the PhraseMaker matrix.
104+
* @param {Object} pm - The PhraseMaker instance.
105+
* @param {number} rowBlock - The pitch or drum block associated with the node to remove.
106+
* @param {number} rhythmBlock - The rhythm block associated with the node to remove.
107+
* @param {number} n - The index of the rhythm block within its column.
108+
*/
109+
removeNode(pm, rowBlock, rhythmBlock, n) {
110+
// When the matrix is changed, we may need to remove nodes.
111+
const blk = pm.blockNo;
112+
let obj;
113+
for (let i = 0; i < pm._blockMap[blk].length; i++) {
114+
obj = pm._blockMap[blk][i];
115+
if (obj[0] === rowBlock && obj[1][0] === rhythmBlock && obj[1][1] === n) {
116+
pm._blockMap[blk].splice(i, 1);
117+
}
118+
}
119+
},
120+
121+
/**
122+
* Maps and collects blocks with a specified block name or type.
123+
* @param {Object} pm - The PhraseMaker instance.
124+
* @param {string} blockName - The name of the block to map and collect.
125+
* @param {boolean} [withName=false] - Indicates whether to include the block name in the map.
126+
* @returns {Array<number | Array<number, string>>} An array of block IDs or [block ID, block name] pairs.
127+
*/
128+
mapNotesBlocks(pm, blockName, withName) {
129+
const notesBlockMap = [];
130+
let blk = pm.activity.blocks.blockList[pm.blockNo].connections[1];
131+
let myBlock = pm.activity.blocks.blockList[blk];
132+
133+
let bottomBlockLoop = 0;
134+
if (
135+
myBlock.name === blockName ||
136+
(blockName === "all" &&
137+
myBlock.name !== "hidden" &&
138+
myBlock.name !== "vspace" &&
139+
myBlock.name !== "hiddennoflow")
140+
) {
141+
if (withName) {
142+
notesBlockMap.push([blk, myBlock.name]);
143+
} else {
144+
notesBlockMap.push(blk);
145+
}
146+
}
147+
148+
while (pm._deps.last(myBlock.connections) != null) {
149+
bottomBlockLoop += 1;
150+
if (bottomBlockLoop > 2 * pm.activity.blocks.blockList) {
151+
// Could happen if the block data is malformed.
152+
break;
153+
}
154+
155+
blk = pm._deps.last(myBlock.connections);
156+
myBlock = pm.activity.blocks.blockList[blk];
157+
if (
158+
myBlock.name === blockName ||
159+
(blockName === "all" &&
160+
myBlock.name !== "hidden" &&
161+
myBlock.name !== "vspace" &&
162+
myBlock.name !== "hiddennoflow")
163+
) {
164+
if (withName) {
165+
notesBlockMap.push([blk, myBlock.name]);
166+
} else {
167+
notesBlockMap.push(blk);
168+
}
169+
}
170+
}
171+
172+
return notesBlockMap;
173+
},
174+
175+
/**
176+
* Checks for note blocks or repeat blocks within the current block map.
177+
* Sets the '_noteBlocks' flag to 'true' if any 'newnote' or 'repeat' block is found.
178+
* @param {Object} pm - The PhraseMaker instance.
179+
*/
180+
lookForNoteBlocksOrRepeat(pm) {
181+
pm._noteBlocks = false;
182+
const bno = pm.blockNo;
183+
let blk;
184+
for (let i = 0; i < pm._blockMap[bno].length; i++) {
185+
blk = pm._blockMap[bno][i][1][0];
186+
if (blk === -1) {
187+
continue;
188+
}
189+
190+
if (pm.activity.blocks.blockList[blk] === null) {
191+
continue;
192+
}
193+
194+
if (pm.activity.blocks.blockList[blk] === undefined) {
195+
//eslint-disable-next-line no-console
196+
console.debug("block " + blk + " is undefined");
197+
continue;
198+
}
199+
200+
if (
201+
pm.activity.blocks.blockList[blk].name === "newnote" ||
202+
pm.activity.blocks.blockList[blk].name === "repeat"
203+
) {
204+
pm._noteBlocks = true;
205+
break;
206+
}
207+
}
208+
},
209+
210+
/**
211+
* Synchronizes marked blocks based on block mapping and helper information.
212+
* @param {Object} pm - The PhraseMaker instance.
213+
*/
214+
syncMarkedBlocks(pm) {
215+
const newBlockMap = [];
216+
const blk = pm.blockNo;
217+
for (let i = 0; i < pm._blockMap[blk].length; i++) {
218+
if (pm._blockMap[blk][i][0] === -1) {
219+
continue;
220+
}
221+
222+
for (let j = 0; j < pm._blockMapHelper.length; j++) {
223+
if (
224+
JSON.stringify(pm._blockMap[blk][i][1]) ===
225+
JSON.stringify(pm._blockMapHelper[j][0])
226+
) {
227+
for (let k = 0; k < pm._blockMapHelper[j][1].length; k++) {
228+
newBlockMap.push([
229+
pm._blockMap[blk][i][0],
230+
pm._colBlocks[pm._blockMapHelper[j][1][k]],
231+
pm._blockMap[blk][i][2]
232+
]);
233+
}
234+
}
235+
}
236+
}
237+
238+
pm._blockMap[blk] = newBlockMap.filter((el, i) => {
239+
return (
240+
i ===
241+
newBlockMap.findIndex(ele => {
242+
return JSON.stringify(ele) === JSON.stringify(el);
243+
})
244+
);
245+
});
246+
}
247+
};
248+
249+
// Export for global use
250+
window.PhraseMakerGrid = PhraseMakerGrid;
251+
252+
// Export for RequireJS/AMD
253+
if (typeof define === "function" && define.amd) {
254+
define([], function () {
255+
return PhraseMakerGrid;
256+
});
257+
}
258+
259+
// Export for Node.js/CommonJS (for testing)
260+
if (typeof module !== "undefined" && module.exports) {
261+
module.exports = PhraseMakerGrid;
262+
}

0 commit comments

Comments
 (0)