diff --git a/src/app/components/blockly-editor/blockly-editor.component.ts b/src/app/components/blockly-editor/blockly-editor.component.ts
index a46b0af..7b15177 100644
--- a/src/app/components/blockly-editor/blockly-editor.component.ts
+++ b/src/app/components/blockly-editor/blockly-editor.component.ts
@@ -23,7 +23,6 @@ export class BlocklyEditorComponent implements AfterViewInit {
private modeService = inject(ModeService);
@ViewChild('blocklyDiv') blocklyDiv!: ElementRef;
- //@ViewChild('blocklyArea') blocklyArea!: ElementRef;
@Input() toolbox = signal({
kind: 'flyoutToolbox',
@@ -84,12 +83,9 @@ export class BlocklyEditorComponent implements AfterViewInit {
});
this.workspace.registerButtonCallback('addNewBlock', () => {
- //this.openBlocksModal();
this.openModal.emit();
});
- //this.resizeBlockly();
-
const jsonWorkspace = JSON.parse(this.workspaceJSON);
Blockly.serialization.workspaces.load(jsonWorkspace, this.workspace);
}
diff --git a/src/app/components/scene/scene.component.html b/src/app/components/scene/scene.component.html
index 7ffd8b7..d90dd92 100644
--- a/src/app/components/scene/scene.component.html
+++ b/src/app/components/scene/scene.component.html
@@ -1,4 +1,4 @@
-
+
@if (modeService.getMode() === 'teacher') {
@@ -28,5 +28,14 @@
}
-
+
+ @for (obj of sceneObjects; track obj.id) {
+
![]()
+ }
+
+
+
diff --git a/src/app/components/scene/scene.component.ts b/src/app/components/scene/scene.component.ts
index e44623f..f1f340b 100644
--- a/src/app/components/scene/scene.component.ts
+++ b/src/app/components/scene/scene.component.ts
@@ -12,11 +12,13 @@ import {
import {SceneObject} from '../../models/scene-object';
import {ButtonComponent} from '../button/button.component';
import {ModeService} from '../../services/mode.service';
+import {NgClass} from '@angular/common';
@Component({
selector: 'blearn-scene',
imports: [
- ButtonComponent
+ ButtonComponent,
+ NgClass
],
templateUrl: './scene.component.html',
})
@@ -27,11 +29,15 @@ export class SceneComponent implements AfterViewInit {
@ViewChild('scene') scene!: ElementRef;
@Input() isRunning = signal
(false);
+ @Input() sceneObjects: SceneObject[] = [];
+ @Input() selectedObject = signal(undefined);
+
@Output() runCode = new EventEmitter();
- @Output() stopCode = new EventEmitter();
+ @Output() sceneObjectsChange = new EventEmitter();
+ @Output() objectAdded = new EventEmitter();
+ @Output() objectSelected = new EventEmitter();
private ctx: CanvasRenderingContext2D | null = null;
- protected sceneObjects: Array = [];
private draggingObject: SceneObject | null = null;
private offsetX: number = 0;
private offsetY: number = 0;
@@ -47,35 +53,32 @@ export class SceneComponent implements AfterViewInit {
this.canvas.nativeElement.width = this.scene.nativeElement.offsetWidth;
this.canvas.nativeElement.height = this.scene.nativeElement.offsetHeight;
- // Create an image object
- const img = new Image();
- img.src = 'https://avatars.githubusercontent.com/u/105555875?v=4'; // Replace with your image URL
- img.onload = () => {
- // Once the image is loaded, draw it to the canvas
- this.sceneObjects.push({img, x: 50, y: 50, rotation: 0, width: 100, height: 100});
- this.drawImages();
- };
+ const imageLoadPromises = this.sceneObjects.map(obj => {
+ return new Promise((resolve) => {
+ const img = new Image();
+ img.src = obj.imgSrc;
+ img.onload = () => {
+ obj.img = img;
+ resolve();
+ }
+ });
+ });
- // Set up mouse event listeners for dragging
+ Promise.all(imageLoadPromises).then(() => this.drawImages());
this.setupMouseEvents();
}
protected addObject() {
- const img = new Image();
- img.src = 'https://avatars.githubusercontent.com/u/105555875?v=4'; // Replace with your image URL
- img.onload = () => {
- // Once the image is loaded, draw it to the canvas
- this.sceneObjects.push({img, x: 50, y: 50, rotation: 0, width: 100, height: 100});
- this.drawImages();
- };
+ this.objectAdded.emit();
}
private setupMouseEvents() {
this.canvas.nativeElement.addEventListener('mousedown', (e: MouseEvent) => {
+ if (this.isRunning()) return;
+
const mouseX = e.offsetX;
const mouseY = e.offsetY;
- // Check if the mouse click is on one of the images
for (let sceneObj of this.sceneObjects) {
if (mouseX >= sceneObj.x && mouseX <= sceneObj.x + sceneObj.width && mouseY >= sceneObj.y && mouseY <= sceneObj.y + sceneObj.height) {
this.draggingObject = sceneObj;
@@ -86,6 +89,8 @@ export class SceneComponent implements AfterViewInit {
});
this.canvas.nativeElement.addEventListener('mousemove', (e: MouseEvent) => {
+ if (this.isRunning()) return;
+
if (this.draggingObject) {
const mouseX = e.offsetX;
const mouseY = e.offsetY;
@@ -99,50 +104,45 @@ export class SceneComponent implements AfterViewInit {
});
this.canvas.nativeElement.addEventListener('mouseup', () => {
+ if (this.isRunning()) return;
+
+ if (this.draggingObject !== null) this.sceneObjectsChange.emit();
this.draggingObject = null;
});
this.canvas.nativeElement.addEventListener('mouseleave', () => {
+ if (this.isRunning()) return;
+
this.draggingObject = null;
});
}
- private drawImages() {
+ public drawImages() {
if (!this.ctx) return;
- // Clear the canvas
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
- // Draw each image on the canvas
- for (let imgObj of this.sceneObjects) {
- this.ctx.drawImage(imgObj.img, imgObj.x, imgObj.y, imgObj.width, imgObj.height);
- }
- }
-
- moveTo(x: number, y: number) {
- if (this.sceneObjects.length > 0) {
- const obj = this.sceneObjects[0];
- obj.x = x;
- obj.y = y;
- this.drawImages();
- }
- }
-
- moveForward(steps: number) {
- const obj = this.sceneObjects[0];
- obj.x += steps;
- this.drawImages();
- }
-
- setDirection(angle: number) {
-
- }
+ for (let obj of this.sceneObjects) {
+ this.ctx.save();
- turnLeft(angle: number) {
-
- }
+ if (obj.id === this.selectedObject()) {
+ this.ctx.shadowColor = 'red';
+ this.ctx.shadowBlur = 20;
+ this.ctx.shadowOffsetX = 0;
+ this.ctx.shadowOffsetY = 0;
+ }
- turnRight(angle: number) {
+ if (!obj.img) {
+ const img = new Image();
+ img.src = obj.imgSrc;
+ img.onload = () => {
+ obj.img = img;
+ this.ctx?.drawImage(obj.img!, obj.x, obj.y, obj.width, obj.height);
+ }
+ } else
+ this.ctx.drawImage(obj.img!, obj.x, obj.y, obj.width, obj.height);
+ this.ctx.restore();
+ }
}
}
diff --git a/src/app/layout/header/header.component.html b/src/app/layout/header/header.component.html
index 0b30345..4b6dd3d 100644
--- a/src/app/layout/header/header.component.html
+++ b/src/app/layout/header/header.component.html
@@ -17,5 +17,6 @@
teacherStyle="bg-student text-white"
(click)="switchMode()"
data-testid="button"
+ routerLink=""
/>
diff --git a/src/app/models/activity.ts b/src/app/models/activity.ts
index bafd458..ef5d936 100644
--- a/src/app/models/activity.ts
+++ b/src/app/models/activity.ts
@@ -1,3 +1,5 @@
+import {SceneObject} from './scene-object';
+
export interface Activity {
id: string,
title: string,
@@ -7,5 +9,6 @@ export interface Activity {
toolboxInfo: {
BLOCK_LIMITS: { [key: string]: number },
toolboxDefinition: string,
- }
+ },
+ sceneObjects: SceneObject[]
}
diff --git a/src/app/models/scene-object.ts b/src/app/models/scene-object.ts
index 0fdd024..4a6dc8e 100644
--- a/src/app/models/scene-object.ts
+++ b/src/app/models/scene-object.ts
@@ -1,8 +1,34 @@
-export interface SceneObject {
- img: HTMLImageElement;
- x: number;
- y: number;
- rotation: number;
- width: number;
- height: number;
+export class SceneObject {
+ constructor(
+ public id: string,
+ public imgSrc: string,
+ public x: number,
+ public y: number,
+ public rotation: number,
+ public width: number,
+ public height: number,
+ public workspace: string,
+ public img?: HTMLImageElement,
+ ) {}
+
+ moveForward(steps: number) {
+ this.x += steps;
+ }
+
+ moveTo(x: number, y: number) {
+ this.x = x;
+ this.y = y;
+ }
+
+ setDirection(angle: number) {
+
+ }
+
+ turnLeft(angle: number) {
+
+ }
+
+ turnRight(angle: number) {
+
+ }
}
diff --git a/src/app/pages/activity-detail/activity-detail.component.html b/src/app/pages/activity-detail/activity-detail.component.html
index 924949a..bc04cc1 100644
--- a/src/app/pages/activity-detail/activity-detail.component.html
+++ b/src/app/pages/activity-detail/activity-detail.component.html
@@ -46,15 +46,19 @@
[workspaceJSON]="activity()!.workspace"
[BLOCKS_LIMITS]="BLOCK_LIMITS"
(openModal)="openBlocksModal()"
- (updateLimits)="updateToolboxLimits(workspace)"
+ (updateLimits)="updateToolboxLimits()"
(saveWorkspace)="saveWorkspace(false)"
/>
diff --git a/src/app/pages/activity-detail/activity-detail.component.ts b/src/app/pages/activity-detail/activity-detail.component.ts
index ad048c7..b4a1c8c 100644
--- a/src/app/pages/activity-detail/activity-detail.component.ts
+++ b/src/app/pages/activity-detail/activity-detail.component.ts
@@ -2,7 +2,6 @@ import {
AfterViewInit,
Component,
computed,
- ElementRef,
inject,
OnDestroy,
signal,
@@ -25,6 +24,8 @@ import {DescriptionModalComponent} from '../../components/description-modal/desc
import {NgClass} from '@angular/common';
import {SceneComponent} from '../../components/scene/scene.component';
import {javascriptGenerator} from 'blockly/javascript';
+import {SceneObject} from '../../models/scene-object';
+import genUniqueId from '../../utils/genUniqueId';
@Component({
selector: 'blearn-activity-detail',
@@ -47,8 +48,6 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy {
@ViewChild(BlocklyEditorComponent) blocklyEditorComponent!: BlocklyEditorComponent;
@ViewChild(SceneComponent) sceneComponent!: SceneComponent;
@ViewChild('modalHost', {read: ViewContainerRef}) modalHost!: ViewContainerRef;
- @ViewChild('scene') scene!: ElementRef;
- @ViewChild('canvas') canvas!: ElementRef;
protected workspace!: Blockly.WorkspaceSvg;
protected toolbox = signal({
@@ -58,12 +57,15 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy {
{kind: '', type: ''},
]
});
+ protected selectedObject = signal(undefined);
protected readonly BLOCK_LIMITS: Map;
- private interpreter: Interpreter | null = null;
+ private runningInterpreters = 0;
protected isRunning = signal(false);
+ public objectsCode = new Map();
+
private activityId = toSignal(
this.route.paramMap.pipe(
map(params => params.get('id') || null)
@@ -91,17 +93,79 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy {
this.activity.set(computedActivity());
this.toolbox.set(JSON.parse(this.activity()!.toolboxInfo.toolboxDefinition));
this.BLOCK_LIMITS = new Map(Object.entries(this.activity()!.toolboxInfo.BLOCK_LIMITS));
+
+ if (this.activity()!.sceneObjects.length > 0) {
+
+ const restoredObjects = this.activity()!.sceneObjects.map(obj =>
+ new SceneObject(obj.id, obj.imgSrc, obj.x, obj.y, obj.rotation, obj.width, obj.height, obj.workspace)
+ );
+
+ this.activity.set({...this.activity()!, sceneObjects: restoredObjects});
+ }
}
ngAfterViewInit(): void {
this.workspace = this.blocklyEditorComponent.getWorkspace();
+ const jsonWorkspace = JSON.stringify(Blockly.serialization.workspaces.save(this.workspace));
+ this.activity.set({...this.activity()!, workspace: jsonWorkspace});
+
+ if (this.activity()!.sceneObjects.length > 0) {
+ this.activity()!.sceneObjects.forEach(sceneObject => this.generateCode(sceneObject));
+ this.selectSceneObject(this.activity()!.sceneObjects[0].id);
+ }
}
ngOnDestroy(): void {
this.saveWorkspace(true);
}
+ public findSceneObjectById(id: string): SceneObject | undefined {
+ return this.activity()!.sceneObjects.find(obj => obj.id === id);
+ }
+
+ protected createSceneObject() {
+ const img = new Image();
+ img.src = 'https://avatars.githubusercontent.com/u/105555875?v=4';
+ img.onload = () => {
+ const newObject: SceneObject = new SceneObject(
+ genUniqueId(),
+ img.src,
+ 0,
+ 0,
+ 0,
+ 100,
+ 100,
+ this.activity()!.workspace,
+ img
+ );
+
+ Blockly.serialization.workspaces.load(JSON.parse(newObject.workspace), this.workspace);
+ this.workspace.updateToolbox(this.toolbox());
+
+ this.selectedObject.set(newObject.id);
+
+ console.log(this.objectsCode);
+
+ this.activity()!.sceneObjects.push(newObject);
+ this.sceneComponent.drawImages();
+ }
+ }
+
+ protected selectSceneObject(id: string) {
+ this.selectedObject.set(id);
+
+ const obj = this.findSceneObjectById(id);
+ Blockly.serialization.workspaces.load(JSON.parse(obj!.workspace), this.workspace);
+
+ this.sceneComponent.drawImages();
+ }
+
protected openBlocksModal() {
+ if (this.activity()!.sceneObjects.length === 0) {
+ alert('You need to create an object to start adding blocks');
+ return;
+ }
+
const modalRef = this.modalHost.createComponent(BlocksModalComponent);
modalRef.instance.BLOCKS_LIMIT = this.BLOCK_LIMITS;
@@ -117,7 +181,7 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy {
this.toolbox.set(newToolbox);
}
- this.updateToolboxLimits(this.workspace);
+ this.updateToolboxLimits();
});
modalRef.instance.blockRemoved.subscribe(type => {
@@ -133,7 +197,7 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy {
}
} else if (this.BLOCK_LIMITS.has(type) && this.BLOCK_LIMITS.get(type)! > 1) this.BLOCK_LIMITS.set(type, this.BLOCK_LIMITS.get(type)! - 1);
- this.updateToolboxLimits(this.workspace);
+ this.updateToolboxLimits();
});
modalRef.instance.close.subscribe(() => {
this.saveWorkspace(false);
@@ -150,9 +214,9 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy {
modalRef.instance.close.subscribe(() => modalRef.destroy());
}
- protected updateToolboxLimits(workspace: Blockly.WorkspaceSvg) {
+ protected updateToolboxLimits() {
const blockCounts = new Map();
- workspace.getAllBlocks(false).forEach(block => {
+ this.workspace.getAllBlocks(false).forEach(block => {
const type = block.type;
blockCounts.set(type, (blockCounts.get(type) || 0) + 1);
});
@@ -171,7 +235,7 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy {
})
}
- workspace.updateToolbox(newToolbox);
+ this.workspace.updateToolbox(newToolbox);
}
protected updateTitle(newTitle: string) {
@@ -195,73 +259,108 @@ export class ActivityDetailComponent implements AfterViewInit, OnDestroy {
}
}
+ private generateCode(sceneObject: SceneObject) {
+ Blockly.serialization.workspaces.load(JSON.parse(sceneObject.workspace), this.workspace);
+ javascriptGenerator.init(this.workspace);
+
+ const startBlock = this.workspace.getTopBlocks(true)
+ .find(block => block.type === 'event_start');
+
+ if (startBlock) {
+ const code = javascriptGenerator.blockToCode(startBlock) as string;
+ this.objectsCode.set(sceneObject.id, code);
+ }
+ }
+
saveWorkspace(onStorage: boolean) {
+ if (this.selectedObject()) {
+ javascriptGenerator.init(this.workspace);
+ const startBlock = this.workspace.getTopBlocks(true)
+ .find(block => block.type === 'event_start');
+
+ if (startBlock) {
+ const code = javascriptGenerator.blockToCode(startBlock) as string;
+ this.objectsCode.set(this.selectedObject()!, code);
+ }
+ }
+
const jsonWorkspace = Blockly.serialization.workspaces.save(this.workspace);
if (onStorage && this.modeService.getMode() === 'teacher') this.toolbox().contents.shift();
const jsonToolbox = JSON.stringify(this.toolbox());
const workspaceJSON = JSON.stringify(jsonWorkspace);
+
+ if (this.selectedObject()) {
+ const obj = this.findSceneObjectById(this.selectedObject()!);
+ obj!.workspace = workspaceJSON;
+ }
this.activity.set({
...this.activity()!,
- workspace: workspaceJSON,
toolboxInfo: {BLOCK_LIMITS: Object.fromEntries(this.BLOCK_LIMITS), toolboxDefinition: jsonToolbox}
});
- if (onStorage) this.activityService.updateActivity(this.activityId()!, this.activity()!);
+ if (onStorage) {
+ this.activity()!.sceneObjects.map(obj => obj.img = undefined);
+ this.activityService.updateActivity(this.activityId()!, this.activity()!);
+ }
}
- runInterpreter(code: string) {
- this.interpreter = new Interpreter(code, this.initApi.bind(this));
- this.stepExecution();
- }
+ protected onRunCode() {
+ this.runningInterpreters = this.objectsCode.size;
- stepExecution() {
- if (!this.interpreter || !this.isRunning()) return;
+ this.objectsCode.forEach((v, k) => {
+ console.log('Executing code of object with id ', k);
+ this.isRunning.set(true);
+ this.runInterpreter(v, k);
+ });
+ }
- const hasMoreCode = this.interpreter.step();
- if (hasMoreCode) {
- setTimeout(() => this.stepExecution(), 0.5);
- } else {
- this.isRunning.set(false);
- console.log('Execution finished');
- }
+ runInterpreter(code: string, id: string) {
+ const interpreterInstance = new Interpreter(code, this.initApi(id));
+ this.stepExecution(interpreterInstance);
}
- initApi(interpreter: Interpreter, globalObject: any) {
- const addFunction = (name: string, fn: Function) => {
+ private initApi(id: string) {
+ return (interpreter: Interpreter, globalObject: any) => {
+ const object = this.findSceneObjectById(id);
+ if (!object) return;
+
+ const addFunction = (name: string, fn: Function) => {
+ interpreter.setProperty(
+ globalObject,
+ name,
+ interpreter.createNativeFunction(fn.bind(object))
+ );
+ };
+
+ addFunction('moveForward', object.moveForward);
+ addFunction('moveTo', object.moveTo);
+ addFunction('setDirection', object.setDirection)
+ addFunction('turnLeft', object.turnLeft);
+ addFunction('turnRight', object.turnRight);
+
interpreter.setProperty(
globalObject,
- name,
- interpreter.createNativeFunction(fn.bind(this.sceneComponent))
+ 'waitSeconds',
+ interpreter.createAsyncFunction((seconds: number, callback: Function) => {
+ setTimeout(() => callback(), seconds * 1000);
+ })
);
};
-
- addFunction('moveForward', this.sceneComponent.moveForward);
- addFunction('moveTo', this.sceneComponent.moveTo);
- addFunction('setDirection', this.sceneComponent.setDirection)
- addFunction('turnLeft', this.sceneComponent.turnLeft);
- addFunction('turnRight', this.sceneComponent.turnRight);
-
- interpreter.setProperty(
- globalObject,
- 'waitSeconds',
- interpreter.createAsyncFunction((seconds: number, callback: Function) => {
- setTimeout(() => callback(), seconds * 1000);
- })
- );
}
- protected onRunCode() {
- javascriptGenerator.init(this.workspace);
- const startBlock = this.workspace.getTopBlocks(true)
- .find(block => block.type === 'event_start');
+ private stepExecution(interpreter: Interpreter) {
+ if (!this.isRunning()) return;
- if (!startBlock) {
- alert('You should start your program with the "Start program" block!');
- return;
- }
- const code = javascriptGenerator.blockToCode(startBlock) as string;
- console.log(code);
+ const hasMoreCode = interpreter.step();
+ if (hasMoreCode) {
+ this.sceneComponent.drawImages();
+ setTimeout(() => this.stepExecution(interpreter), 0.1);
+ } else {
+ this.runningInterpreters--;
- this.isRunning.set(true);
- this.runInterpreter(code);
+ if (this.runningInterpreters === 0) {
+ this.isRunning.set(false);
+ console.log('Execution finished');
+ }
+ }
}
}
diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts
index 0ef30ff..70692d2 100644
--- a/src/app/pages/home/home.component.ts
+++ b/src/app/pages/home/home.component.ts
@@ -42,7 +42,8 @@ export class HomeComponent {
toolboxInfo: {
toolboxDefinition: '{"kind": "flyoutToolbox", "contents": [{ "kind": "", "text": "", "callbackKey": ""}, {"kind": "", "type": ""}]}',
BLOCK_LIMITS: {}
- }
+ },
+ sceneObjects: []
};
this.activityService.addActivity(newActivity);
this.activityList.set(this.activityService.loadActivities());
@@ -93,6 +94,7 @@ export class HomeComponent {
dueDate: jsonData.dueDate,
workspace: jsonData.workspace,
toolboxInfo: jsonData.toolboxInfo,
+ sceneObjects: jsonData.sceneObjects,
}
}
}