feat: sync all remaining python source board configurations

This commit is contained in:
yczpf2019
2026-01-24 16:19:55 +08:00
parent 1990bee9a1
commit 20bde81bbb
519 changed files with 93119 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import * as path from 'path';
import { FileTree, Debug } from 'mixly';
import FileSystemFS from './filesystem-fs';
export default class FileSystemFileTree extends FileTree {
constructor() {
super(new FileSystemFS());
}
async readFolder(inPath) {
let output = [];
try {
const fs = this.getFS();
const status = await fs.isDirectory(inPath);
if (!status) {
return output;
}
const children = await fs.readDirectory(inPath);
for (let data of children) {
const dataPath = path.join(inPath, data);
const isDirectory = await fs.isDirectory(dataPath);
if (isDirectory) {
const isDirEmpty = await fs.isDirectoryEmpty(dataPath);
output.push({
type: 'folder',
id: dataPath,
children: !isDirEmpty,
title: `/${this.getRootFolderName()}${dataPath}`
});
} else {
output.push({
type: 'file',
id: dataPath,
children: false,
title: `/${this.getRootFolderName()}${dataPath}`
});
}
}
} catch (error) {
Debug.error(error);
}
return output;
}
}

View File

@@ -0,0 +1,129 @@
import { WebAccessFS } from '@zenfs/dom';
import { get, set } from 'idb-keyval';
import { FS } from 'mixly';
class WebAccessFSExt extends WebAccessFS {
constructor(handle) {
super(handle);
}
async readFile(path) {
const handle = await this.getHandle(path);
if (handle instanceof window.FileSystemFileHandle) {
const file = await handle.getFile();
const text = await file.text();
return text;
}
return '';
}
}
export default class FileSystemFS extends FS {
#fs_ = null;
#encoder_ = new TextEncoder();
constructor() {
super();
}
async showDirectoryPicker() {
const directoryHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
const permissionStatus = await directoryHandle.requestPermission({ mode: 'readwrite' });
if (permissionStatus !== 'granted') {
throw new Error('readwrite access to directory not granted');
}
await this.setFSCache(directoryHandle);
this.#fs_ = new WebAccessFSExt(directoryHandle);
return directoryHandle;
}
async loadFS() {
let directoryHandle = await this.getFSCache();
if (!directoryHandle) {
return null;
}
const permissionStatus = await directoryHandle.requestPermission({ mode: 'readwrite' });
if (permissionStatus !== 'granted') {
throw new Error('readwrite access to directory not granted');
}
this.#fs_ = new WebAccessFSExt(directoryHandle);
return directoryHandle;
}
async getFSCache() {
return get('mixly-pyodide-fs');
}
async setFSCache(data) {
await set('mixly-pyodide-fs', data);
}
async createFile(filePath) {
return this.#fs_.createFile(filePath, '');
}
async readFile(path) {
return this.#fs_.readFile(path);
}
async writeFile(path, data) {
const encodedArray = this.#encoder_.encode(data);
return this.#fs_.writeFile(path, encodedArray);
}
async isFile(path) {
const stats = await this.#fs_.stat(path);
if (stats && stats.mode === 33279) {
return true;
}
return false;
}
async renameFile(oldFilePath, newFilePath) {
return await this.#fs_.rename(oldFilePath, newFilePath);
}
async moveFile(oldFilePath, newFilePath) {
return this.renameFile(oldFilePath, newFilePath);
}
async deleteFile(filePath) {
return this.#fs_.unlink(filePath);
}
async createDirectory(folderPath) {
return this.#fs_.mkdir(folderPath, 0o777);
}
async readDirectory(path) {
const result = await this.#fs_.readdir(path);
return result;
}
async isDirectory(path) {
const stats = await this.#fs_.stat(path);
if (stats && stats.mode === 16895) {
return true;
}
return false;
}
async isDirectoryEmpty(path) {
const result = await this.readDirectory(path);
return !result?.length;
}
async renameDirectory(oldFolderPath, newFolderPath) {
return this.#fs_.rename(oldFolderPath, newFolderPath);
}
async moveDirectory(oldFolderPath, newFolderPath) {
return this.#fs_.rename(oldFolderPath, newFolderPath);
}
async deleteDirectory(folderPath) {
return this.#fs_.rmdir(folderPath);
}
}

View File

@@ -0,0 +1,333 @@
import NavExt from './nav-ext';
import * as tf from '@tensorflow/tfjs';
import './tensorflow';
import './sound.js';
import * as Blockly from 'blockly/core';
NavExt.init();
window.tf = tf;
// let featureExtractor;
// featureExtractor = await tf.loadGraphModel("../common/media/tfmodel/model.json");
// window.featureExtractor = featureExtractor;
function closeModal() {
document.getElementById('modalOverlay').style.display = 'none';
}
// 从IndexedDB删除单个模型
async function deleteModel(modelName) {
try {
await tf.io.removeModel(`indexeddb://${modelName}`);
// 从UI移除
const modelItem = document.querySelector(`.model-item[data-model-name="${modelName}"]`);
if (modelItem) modelItem.remove();
} catch (error) {
console.error('删除模型失败:', error);
alert('删除模型失败: ' + error.message);
}
}
// 显示单个模型项
function displayModelItem(modelName) {
const modelsList = document.getElementById('imported-models');
if ([...modelsList.children].some(item => item.dataset.modelName === modelName)) {
return;
}
const modelItem = document.createElement('div');
modelItem.className = 'model-item';
modelItem.dataset.modelName = modelName;
modelItem.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; border-bottom: 1px solid #eee;">
<span style="font-size: 1em; color: #333;">${modelName}</span>
<button class="delete-model" style="
background: #f44336;
color: white;
border: none;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
">删除</button>
</div>
`;
// 绑定删除事件
modelItem.querySelector('.delete-model').addEventListener('click', () => {
deleteModel(modelName);
});
modelsList.appendChild(modelItem);
}
// 清空所有模型
async function clearAllModels() {
try {
const modelInfos = await tf.io.listModels();
const deletePromises = Object.keys(modelInfos)
.map(path => path.replace('indexeddb://', ''))
.map(modelName => tf.io.removeModel(`indexeddb://${modelName}`));
await Promise.all(deletePromises);
document.getElementById('imported-models').innerHTML = '';
} catch (error) {
console.error('清空模型失败:', error);
alert('清空模型失败: ' + error.message);
}
}
// 加载并显示所有模型
async function loadAndDisplayAllModels() {
try {
const modelInfos = await tf.io.listModels();
document.getElementById('imported-models').innerHTML = '';
for (const [path] of Object.entries(modelInfos)) {
const modelName = path.replace('indexeddb://', '');
displayModelItem(modelName);
}
} catch (error) {
console.error('加载模型列表失败:', error);
}
}
async function createModal() {
const overlay = document.createElement('div');
overlay.id = 'modalOverlay';
Object.assign(overlay.style, {
display: 'none',
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.5)',
zIndex: '20011216',
pointerEvents: 'auto'
});
const content = document.createElement('div');
Object.assign(content.style, {
backgroundColor: 'white',
width: '60%',
maxHeight: '80%',
margin: '12vh auto',
padding: '20px 30px',
borderRadius: '12px'
});
content.innerHTML = `
<h2 style="margin-bottom: 20px;">选择本地模型</h2>
<div style="margin-bottom: 25px; position: relative; min-height: 200px;"> <!-- 新增 min-height 和 position -->
<input type="file" id="model-upload" accept=".json,.bin" multiple style="display: none;">
<label for="model-upload" style="
background: #1216ab;
color: white;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
">选择模型文件</label>
<div style="display: flex; gap: 10px; margin-top: 15px; align-items: center;">
<span style="line-height: 36px;">导入模型名称:</span>
<input type="text" id="model-name" placeholder="模型名称" value="my-model" style="
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
flex: 1;
max-width: 200px;
">
<button id="model-handle" style="
background: #1216ab;
color: white;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
">保存模型</button>
</div>
<div style="margin-top: 15px; display: flex; gap: 20px;">
<div id="json-status" style="
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
flex: 1;
">
<span>❌ 模型结构描述文件model.json</span>
<div style="color: #666; font-size: 0.9em; margin-top: 5px;">未选择</div>
</div>
<div id="weights-status" style="
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
flex: 1;
">
<span>❌ 权重文件model.weights.bin</span>
<div style="color: #666; font-size: 0.9em; margin-top: 5px;">0 个已选择</div>
</div>
</div>
<div id="output" style="margin-bottom: 15px; min-height: 40px;"></div>
<div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3>已导入模型</h3>
<div style="display: flex; gap: 10px;">
<button id="refresh-models" style="
background: #4CAF50;
color: white;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
border: none;
">刷新</button>
<button id="clear-models" style="
background: #f44336;
color: white;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
border: none;
">清空</button>
</div>
</div>
<div id="imported-models" style="
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
height: 200px;
overflow-y: auto;
">
<div style="color: #666; font-style: italic;">加载中...</div>
</div>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 15px;">
<button class="close-btn" style="
background: #e0e0e0;
border: none;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
">关闭</button>
</div>
</div>
`;
overlay.appendChild(content);
document.body.appendChild(overlay);
content.querySelector('.close-btn').addEventListener('click', closeModal);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeModal();
});
// 获取DOM元素
const modelUpload = document.getElementById('model-upload');
const modelHandle = document.getElementById('model-handle');
const outputDiv = document.getElementById('output');
let jsonFile = null;
let weightFiles = [];
modelUpload.addEventListener('change', async (event) => {
const files = event.target.files;
// 获取状态元素
const jsonStatus = document.getElementById('json-status');
const weightsStatus = document.getElementById('weights-status');
// 重置状态显示(保持完整文件名描述)
jsonStatus.querySelector('span').textContent = '❌ 模型结构描述文件model.json';
jsonStatus.querySelector('div').textContent = '未选择';
weightsStatus.querySelector('span').textContent = '❌ 权重文件model.weights.bin';
weightsStatus.querySelector('div').textContent = '0 个已选择';
// 分离 JSON 和权重文件
weightFiles = [];
for (let i = 0; i < files.length; i++) {
if (files[i].name.endsWith('.json')) {
jsonFile = files[i];
} else {
weightFiles.push(files[i]);
}
}
if (!jsonFile) {
alert('未找到 model.json 文件');
return;
}
outputDiv.innerHTML = '正在处理上传的模型文件...';
if (jsonFile) {
jsonStatus.querySelector('span').textContent = '✅ 模型结构描述文件model.json';
jsonStatus.querySelector('div').textContent = '已选择';
const modelName = jsonFile.name.replace('.json', '');
document.getElementById('model-name').value = modelName;
}
if (weightFiles.length > 0) {
weightsStatus.querySelector('span').textContent = '✅ 权重文件model.weights.bin';
weightsStatus.querySelector('div').textContent = `${weightFiles.length} 个已选择`;
}
});
modelHandle.addEventListener('click', async () => {
try {
const modelNameInput = document.getElementById('model-name');
const modelName = modelNameInput.value || 'mixly-model';
const model = await tf.loadLayersModel(
tf.io.browserFiles([jsonFile, ...weightFiles])
);
await model.save(`indexeddb://${modelName}`);
loadAndDisplayAllModels();
outputDiv.innerHTML = `模型已成功保存为 ${modelName}`;
} catch (error) {
outputDiv.innerHTML = `保存模型出错: ${error.message}`;
console.error(error);
}
})
content.querySelector('#refresh-models').addEventListener('click', loadAndDisplayAllModels);
content.querySelector('#clear-models').addEventListener('click', async () => {
if (confirm('确定要删除所有模型吗?此操作不可恢复!')) {
await clearAllModels();
}
});
}
createModal();
// 使用立即执行的异步函数,避免 top-level await 导致模块异步加载
(async () => {
await loadAndDisplayAllModels();
})();
function openModal() {
loadAndDisplayAllModels();
document.getElementById('modalOverlay').style.display = 'block';
}
const workspace = Blockly.getMainWorkspace();
workspace.registerButtonCallback('handleModels', function () {
openModal();
});
async function prepare_qmyixtxi(imgTensor) {
let net = null;
if (window.featureExtractor) {
net = window.featureExtractor;
} else {
net = await tf.loadGraphModel("../common/media/tfmodel/model.json");
window.featureExtractor = net;
}
const preprocessedImg = imgTensor
.resizeBilinear([224, 224])
.toFloat()
.div(tf.scalar(127.5))
.sub(tf.scalar(1))
.expandDims(0);
const features = window.featureExtractor.predict(preprocessedImg);
let activation = features;
return activation;
}
window.prepare_qmyixtxi = prepare_qmyixtxi;

View File

@@ -0,0 +1,43 @@
import { app, Nav, Debug } from 'mixly';
import * as Blockly from 'blockly/core';
import PythonShell from './python-shell';
const NavExt = {};
NavExt.init = async function () {
const nav = app.getNav();
nav.register({
icon: 'icon-play-circled',
title: '',
id: 'python-run-btn',
displayText: Blockly.Msg.MSG['run'],
preconditionFn: () => {
return true;
},
callback: () => {
PythonShell.run().catch(Debug.error);
},
scopeType: Nav.Scope.LEFT,
weight: 4
});
nav.register({
icon: 'icon-cancel',
title: '',
id: 'python-stop-btn',
displayText: Blockly.Msg.MSG['stop'],
preconditionFn: () => {
return true;
},
callback: () => {
PythonShell.stop().catch(Debug.error);
},
scopeType: Nav.Scope.LEFT,
weight: 5
});
await PythonShell.init();
}
export default NavExt;

View File

@@ -0,0 +1,664 @@
import PIXI from 'pixi.js';
import $ from 'jquery';
const mixlySprite = {
stage: new PIXI.Container(),
pointer: { x: 0, y: 0 },
backgroundSprite: null,
sprites: {},
texts: {},
counter: 0,
keys: {},
state: false,
running: false,
repeatPlay: () => { },
displayTag: false,
processingDisplayEvent: null,
successDisplayEvents: [],
successProcessingDisplayEvents: [],
startTime: performance.now(),
timer: 0,
lastFrameTime: null,
lastSecond: null,
targetFPS: 60,
frameCount: 0,
currentFPS: 60,
canvasHeight: 450,
canvasWidth: 800
};
mixlySprite.gameLoop = () => {
if (mixlySprite.state == true) {
mixlySprite.repeatPlay();
mixlySprite.gameLoopDisplay();
}
mixlySprite.timer = performance.now() - mixlySprite.startTime;
}
mixlySprite.animate = (currentTime) => {
const deltaTime = currentTime - mixlySprite.lastFrameTime;
if (deltaTime >= 1000 / mixlySprite.targetFPS) {
mixlySprite.frameCount++;
mixlySprite.gameLoop();
mixlySprite.renderer.render(mixlySprite.stage);
mixlySprite.lastFrameTime = currentTime;
}
if (currentTime - mixlySprite.lastSecond >= 1000) {
mixlySprite.currentFPS = mixlySprite.frameCount;
mixlySprite.frameCount = 0;
mixlySprite.lastSecond = currentTime;
}
requestAnimationFrame(mixlySprite.animate);
}
mixlySprite.createBackground = (img, mode = 0) => {
var player;
if (mode == 0) {
// eslint-disable-next-line new-cap
player = new PIXI.Sprite.fromImage(`../common/media/spriteimg/${img}.png`);
}
player.name = 'background';
player.anchor.set(0.5);
player.x = mixlySprite.canvasWidth / 2;
player.y = mixlySprite.canvasHeight / 2;
// const $canvas = $('#spriteContainer canvas');
// const canvasWidth = $canvas.width();
// const canvasHeight = $canvas.height();
// player.width = ($('body').width() / 2);
// player.height = ($('body').width() / 2)/canvasWidth*canvasHeight;
player.width = mixlySprite.canvasWidth;
player.height = mixlySprite.canvasHeight;
player.interactive = true;
player.buttonMode = true;
player.isDown = false;
player.isUp = true;
player.on('mousedown', function () {
this.isDown = true;
this.isUp = false;
if (mixlySprite.state) this.runningMouseDown();
})
.on('mouseup', function () {
this.isDown = false;
this.isUp = true;
})
.on('mouseupoutside', function () {
this.isDown = false;
this.isUp = true;
});
player.runningMouseDown = new Function("");
if (mixlySprite.backgroundSprite && mixlySprite.backgroundSprite.parent) {
// 如果子节点已经在父节点中,需要先移除
mixlySprite.stage.removeChild(mixlySprite.backgroundSprite);
}
mixlySprite.backgroundSprite = player;
mixlySprite.stage.addChildAt(mixlySprite.backgroundSprite, 0);
return 0;
}
mixlySprite.createASprite = (img, x = mixlySprite.canvasWidth / 2, y = mixlySprite.canvasHeight / 2, name = '', mode = 0) => {
if (name == '') {
name = 'sprite' + (++mixlySprite.counter);
}
var player;
if (mode == 0) {
// eslint-disable-next-line new-cap
player = new PIXI.Sprite.fromImage(`../common/media/spriteimg/${img}.png`);
}
if (!mixlySprite.sprites[name] && !mixlySprite.texts[name]) {
player.name = name;
player.anchor.set(0.5);
player.x = x;
player.y = y;
player.interactive = true;
player.buttonMode = true;
player.isDown = false;
player.isUp = true;
player.on('mousedown', function (event) {
this.isDown = true;
this.isUp = false;
if (!mixlySprite.state) {
this.data = event.data;
this.alpha = 0.5;
this.dragging = true;
} else this.runningMouseDown();
})
.on('mouseup', function () {
this.isDown = false;
this.isUp = true;
if (!mixlySprite.state) {
this.alpha = 1;
this.dragging = false;
this.data = null;
}
})
.on('mouseupoutside', function () {
this.isDown = false;
this.isUp = true;
if (!mixlySprite.state) {
this.alpha = 1;
this.dragging = false;
this.data = null;
}
})
.on('mousemove', function () {
if (!mixlySprite.state)
if (this.dragging) {
var newPosition = this.data.getLocalPosition(this.parent);
this.position.x = newPosition.x;
this.position.y = newPosition.y;
}
})
player.runningMouseDown = new Function("");
player.show = function () {
this.visible = true;
};
player.hide = function () {
this.visible = false;
};
player.enlarge = function (s) {
const ratio = this.height / this.width;
var measure = Math.sqrt(this.height * this.width);
measure += s;
this.width = Math.sqrt(measure * measure / ratio);
this.height = this.width * ratio;
};
player.enlargeTo = function (s) {
var ratio = this.height / this.width;
this.width = Math.sqrt(s * s / ratio);
this.height = this.width * ratio;
};
player.expandTo = async function (s, time = 1) {
if (mixlySprite.running) {
mixlySprite.displayTag = true;
mixlySprite.processingDisplayEvent = {
sprite: this,
targetS: s,
totalTime: time * 1000,
startTime: performance.now(),
displayType: 'expand'
};
var prom = new Promise((resolve) => {
if (mixlySprite.displayTag === false) {
resolve();
} else {
const checkTagInterval = setInterval(() => {
if (mixlySprite.displayTag === false) {
clearInterval(checkTagInterval);
resolve();
}
}, 10);
}
});
return await prom;
}
mixlySprite.successProcessingDisplayEvents.push({
sprite: this,
targetS: s,
totalTime: time * 1000,
startTime: performance.now(),
displayType: 'expand'
});
return 0;
};
player.move = function (step) {
this.x += step * Math.cos(this.rotation);
this.y += step * Math.sin(this.rotation);
};
player.moveTo = function (x, y) {
this.x = x;
this.y = y;
};
player.slideTo = async function (x, y, time = 1) {
if (mixlySprite.running) {
mixlySprite.displayTag = true;
mixlySprite.processingDisplayEvent = {
sprite: this,
targetX: x,
targetY: y,
totalTime: time * 1000,
startTime: performance.now(),
displayType: 'slide'
};
var prom = new Promise((resolve) => {
const checkTagInterval = setInterval(() => {
if (mixlySprite.displayTag === false) {
clearInterval(checkTagInterval);
resolve();
}
}, 10);
});
return await prom;
}
mixlySprite.successProcessingDisplayEvents.push({
sprite: this,
targetX: x,
targetY: y,
totalTime: time * 1000,
startTime: performance.now(),
displayType: 'slide'
});
return 0;
};
player.addX = function (step) {
this.x += step;
};
player.addY = function (step) {
this.y += step;
};
player.rotate = function (degree) {
this.rotation += Math.PI / 180 * degree;
};
player.rotateTo = function (degree) {
this.rotation = Math.PI / 180 * degree;
};
player.circleTo = async function (degree, time = 1) {
if (mixlySprite.running) {
mixlySprite.displayTag = true;
mixlySprite.processingDisplayEvent = {
sprite: this,
targetDegree: degree,
totalTime: time * 1000,
startTime: performance.now(),
displayType: 'circle'
};
var prom = new Promise((resolve) => {
if (mixlySprite.displayTag === false) {
resolve();
} else {
const checkTagInterval = setInterval(() => {
if (mixlySprite.displayTag === false) {
clearInterval(checkTagInterval);
resolve();
}
}, 10);
}
});
return await prom;
}
mixlySprite.successProcessingDisplayEvents.push({
sprite: this,
targetDegree: degree,
totalTime: time * 1000,
startTime: performance.now(),
displayType: 'circle'
});
return 0;
};
player.hit = function (sprite) {
return mixlySprite.hitTestRectangle(this, sprite);
};
player.outOfScreen = function () {
return this.y >= mixlySprite.renderer.height || this.y <= 0 || this.x <= 0 || this.x >= mixlySprite.renderer.width;
};
player.mouseAction = function (func) {
this.runningMouseDown = func;
};
// new
player.setScale = function (h = 0, w = 0) {
if (h == 0) h = this.height;
if (w == 0) w = this.width;
this.height = h;
this.width = w;
}
player.filterGray = function () {
const grayscaleFilter = new PIXI.filters.ColorMatrixFilter();
grayscaleFilter.blackAndWhite();
this.filters = [grayscaleFilter];
}
player.filterBrighter = function () {
const brightnessFilter = new PIXI.filters.ColorMatrixFilter();
brightnessFilter.brightness(1.25); // 增加亮度
this.filters = [brightnessFilter];
}
player.filterOrigin = function () {
this.filters = null;
}
mixlySprite.stage.addChild(player);
mixlySprite.sprites[name] = player;
}
return name;
}
mixlySprite.clearAllSprites = () => {
if (mixlySprite.backgroundSprite && mixlySprite.backgroundSprite.parent) mixlySprite.backgroundSprite.parent.removeChild(mixlySprite.backgroundSprite);
for (const name in mixlySprite.sprites) {
mixlySprite.sprites[name].parent.removeChild(mixlySprite.sprites[name]);
delete mixlySprite.sprites[name];
}
for (const name in mixlySprite.texts) {
mixlySprite.texts[name].parent.removeChild(mixlySprite.texts[name]);
delete mixlySprite.texts[name];
}
mixlySprite.counter = 0;
mixlySprite.clearTimer();
return 0;
}
mixlySprite.createText = (text, x = mixlySprite.canvasWidth / 2, y = mixlySprite.canvasHeight / 2, name = '') => {
if (name == '') {
name = 'text' + (++mixlySprite.counter);
}
if (!mixlySprite.sprites[name] && !mixlySprite.texts[name]) {
var textObj = new PIXI.Text(text);
textObj.name = name;
textObj.x = x;
textObj.y = y;
textObj.interactive = true;
textObj.buttonMode = true;
textObj.on('mousedown', function (event) {
this.isDown = true;
this.isUp = false;
if (!mixlySprite.state) {
this.data = event.data;
this.alpha = 0.5;
this.dragging = true;
} else this.runningMouseDown();
})
.on('mouseup', function () {
this.isDown = false;
this.isUp = true;
if (!mixlySprite.state) {
this.alpha = 1;
this.dragging = false;
this.data = null;
}
})
.on('mouseupoutside', function () {
this.isDown = false;
this.isUp = true;
if (!mixlySprite.state) {
this.alpha = 1;
this.dragging = false;
this.data = null;
}
})
.on('mousemove', function () {
if (!mixlySprite.state)
if (this.dragging) {
var newPosition = this.data.getLocalPosition(this.parent);
this.position.x = newPosition.x;
this.position.y = newPosition.y;
}
});
textObj.runningMouseDown = new Function("");
textObj.changeText = function (text) {
this.text = text;
};
textObj.show = function () {
this.visible = true;
};
textObj.hide = function () {
this.visible = false;
};
mixlySprite.stage.addChild(textObj);
mixlySprite.texts[name] = textObj;
}
return name;
}
mixlySprite.hitTestRectangle = (r1, r2) => {
let hit, combinedHalfWidths, combinedHalfHeights, vx, vy;
hit = false;
r1.centerX = r1.x + r1.width / 2;
r1.centerY = r1.y + r1.height / 2;
r2.centerX = r2.x + r2.width / 2;
r2.centerY = r2.y + r2.height / 2;
r1.halfWidth = r1.width / 2;
r1.halfHeight = r1.height / 2;
r2.halfWidth = r2.width / 2;
r2.halfHeight = r2.height / 2;
vx = r1.centerX - r2.centerX;
vy = r1.centerY - r2.centerY;
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
if (Math.abs(vx) < combinedHalfWidths) {
if (Math.abs(vy) < combinedHalfHeights) {
hit = true;
} else {
hit = false;
}
} else {
hit = false;
}
return hit;
};
mixlySprite.repeat = (func) => {
mixlySprite.repeatPlay = func;
}
mixlySprite.isKeyboardHit = (keyvalue) => {
if (!mixlySprite.keys[keyvalue]) {
let key = mixlySprite.keyboard(keyvalue);
mixlySprite.keys[keyvalue] = key;
}
return mixlySprite.keys[keyvalue].isDown;
}
mixlySprite.keyboardListener = (keyvalue, func) => {
if (!mixlySprite.keys[keyvalue]) {
let key = mixlySprite.keyboard(keyvalue);
key.press = function () {
if (mixlySprite.state) func();
};
mixlySprite.keys[keyvalue] = key;
} else {
mixlySprite.keys[keyvalue].press = function () {
if (mixlySprite.state) func();
};
}
}
mixlySprite.keyboard = (value) => {
let key = {};
key.value = value;
key.isDown = false;
key.isUp = true;
key.press = undefined;
key.release = undefined;
key.downHandler = event => {
if (event.key === key.value) {
if (key.isUp && key.press) key.press();
key.isDown = true;
key.isUp = false;
event.preventDefault();
}
};
key.upHandler = event => {
if (event.key === key.value) {
if (key.isDown && key.release) key.release();
key.isDown = false;
key.isUp = true;
event.preventDefault();
}
};
const downListener = key.downHandler.bind(key);
const upListener = key.upHandler.bind(key);
window.addEventListener(
"keydown", downListener, false
);
window.addEventListener(
"keyup", upListener, false
);
key.unsubscribe = () => {
window.removeEventListener("keydown", downListener);
window.removeEventListener("keyup", upListener);
};
return key;
}
mixlySprite.clearTimer = () => {
mixlySprite.startTime = performance.now();
}
mixlySprite.gameLoopDisplay = () => {
if (mixlySprite.processingDisplayEvent) {
const pSE = mixlySprite.processingDisplayEvent;
switch (pSE.displayType) {
case 'slide':
if (performance.now() >= pSE.totalTime + pSE.startTime) {
pSE.sprite.moveTo(pSE.targetX, pSE.targetY);
mixlySprite.displayTag = false;
mixlySprite.processingDisplayEvent = null;
} else {
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
if (leftLoops >= 1) {
pSE.sprite.addX((pSE.targetX - pSE.sprite.x) / leftLoops);
pSE.sprite.addY((pSE.targetY - pSE.sprite.y) / leftLoops);
}
}
break;
case 'expand':
if (performance.now() >= pSE.totalTime + pSE.startTime) {
pSE.sprite.enlargeTo(pSE.targetS);
mixlySprite.displayTag = false;
mixlySprite.processingDisplayEvent = null;
} else {
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
if (leftLoops >= 1) {
pSE.sprite.enlarge((pSE.targetS - Math.sqrt(pSE.sprite.height * pSE.sprite.width)) / leftLoops);
}
}
break;
case 'circle':
if (performance.now() >= pSE.totalTime + pSE.startTime) {
pSE.sprite.rotateTo(pSE.targetDegree);
mixlySprite.displayTag = false;
mixlySprite.processingDisplayEvent = null;
} else {
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
if (leftLoops >= 1) {
pSE.sprite.rotate((pSE.targetDegree - pSE.sprite.rotation * 180 / Math.PI) / leftLoops);
}
}
break;
}
}
if (!mixlySprite.running) {
if (mixlySprite.successProcessingDisplayEvents.length) {
for (var pSEindex = mixlySprite.successProcessingDisplayEvents.length - 1; pSEindex >= 0; pSEindex--) {
const pSE = mixlySprite.successProcessingDisplayEvents[pSEindex];
switch (pSE.displayType) {
case 'slide':
if (performance.now() >= pSE.totalTime + pSE.startTime) {
pSE.sprite.moveTo(pSE.targetX, pSE.targetY);
mixlySprite.successProcessingDisplayEvents.splice(pSEindex, 1);
} else {
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
if (leftLoops >= 1) {
pSE.sprite.addX((pSE.targetX - pSE.sprite.x) / leftLoops);
pSE.sprite.addY((pSE.targetY - pSE.sprite.y) / leftLoops);
}
}
break;
case 'expand':
if (performance.now() >= pSE.totalTime + pSE.startTime) {
pSE.sprite.enlargeTo(pSE.targetS);
mixlySprite.successProcessingDisplayEvents.splice(pSEindex, 1);
} else {
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
if (leftLoops >= 1) {
pSE.sprite.enlarge((pSE.targetS - Math.sqrt(pSE.sprite.height * pSE.sprite.width)) / leftLoops);
}
}
break;
case 'circle':
if (performance.now() >= pSE.totalTime + pSE.startTime) {
pSE.sprite.rotateTo(pSE.targetDegree);
mixlySprite.successProcessingDisplayEvents.splice(pSEindex, 1);
} else {
var leftLoops = mixlySprite.currentFPS * (pSE.totalTime + pSE.startTime - performance.now()) / 1000;
if (leftLoops >= 1) {
pSE.sprite.rotate((pSE.targetDegree - pSE.sprite.rotation * 180 / Math.PI) / leftLoops);
}
}
break;
}
}
}
}
}
mixlySprite.changeWidth = (w) => {
const $canvas = $(mixlySprite.renderer.view);
const canvasWidth = $canvas.width();
const canvasHeight = $canvas.height();
$canvas.width(w);
$canvas.height(w / canvasWidth * canvasHeight);
}
mixlySprite.kill = () => {
mixlySprite.state = false;
mixlySprite.repeatPlay = new Function();
for (let i in mixlySprite.keys) {
mixlySprite.keys[i].unsubscribe();
delete mixlySprite.keys[i];
}
mixlySprite.processingDisplayEvent = null;
mixlySprite.displayTag = false;
mixlySprite.running = false;
mixlySprite.clearTimer();
}
mixlySprite.runit = (container) => {
const $container = $(container);
$container.empty();
// Keep the scale mode to nearest
PIXI.SCALE_MODES.DEFAULT = PIXI.SCALE_MODES.NEAREST;
mixlySprite.renderer = PIXI.autoDetectRenderer(mixlySprite.canvasWidth, mixlySprite.canvasHeight, { backgroundColor: 0x00FFFFFF });
$container.append(mixlySprite.renderer.view);
// Create mixlySprite.stage container
// mixlySprite.stage = new PIXI.Container();
mixlySprite.pointer = { x: 0, y: 0 };
mixlySprite.stage.sortableChildren = true;
mixlySprite.stage.interactive = true;
mixlySprite.stage.on("mousemove", (event = PIXI.InteractionEvent) => {
mixlySprite.pointer.x = event.data.global.x;
mixlySprite.pointer.y = event.data.global.y;
});
mixlySprite.lastFrameTime = 0;
mixlySprite.running = true;
if (!mixlySprite.lastFrameTime) {
mixlySprite.lastFrameTime = performance.now();
mixlySprite.lastSecond = performance.now();
}
mixlySprite.animate(performance.now());
mixlySprite.repeatPlay = new Function();
for (let i in mixlySprite.keys) {
mixlySprite.keys[i].unsubscribe();
delete mixlySprite.keys[i];
}
if (mixlySprite.backgroundSprite) {
mixlySprite.backgroundSprite.runningMouseDown = new Function();
}
for (let i in mixlySprite.sprites) {
mixlySprite.sprites[i].runningMouseDown = new Function();
}
for (let i in mixlySprite.texts) {
mixlySprite.texts[i].runningMouseDown = new Function();
}
mixlySprite.processingDisplayEvent = null;
mixlySprite.displayTag = false;
mixlySprite.clearTimer();
mixlySprite.changeWidth($('body').width() / 2);
mixlySprite.state = true;
}
window.mixlySprite = mixlySprite;
export default mixlySprite;

View File

@@ -0,0 +1,274 @@
import * as Blockly from 'blockly/core';
import * as path from 'path';
import $ from 'jquery';
import { createApp } from 'vue';
import mixlySprite from './pixi-sprite';
import {
Workspace,
Env,
Msg,
HTMLTemplate,
Debug,
app
} from 'mixly';
import { KernelLoader } from '@basthon/kernel-loader';
import StatusBarImage from './statusbar-image';
import StatusBarFileSystem from './statusbar-filesystem';
import StatusBarTool from './statusbar-tool';
import StatusBarGame from './statusbar-game';
import TeachableMachineApp from './teachableMachine/App.vue';
import LOADER_TEMPLATE from '../templates/html/loader.html';
import 'element-plus/dist/index.css';
export default class PythonShell {
static {
HTMLTemplate.add(
'html/statusbar/loader.html',
new HTMLTemplate(LOADER_TEMPLATE)
);
this.pythonShell = null;
this.kernelLoaded = false;
this.$loader = $(HTMLTemplate.get('html/statusbar/loader.html').render({
msg: {
loading: Blockly.Msg.PYTHON_PYODIDE_LOADING
}
}));
this.statusBarImage = null;
this.statusBarFileSystem = null;
this.init = async function () {
const footerBar = app.getFooterBar();
const $content = footerBar.getContent();
$content.after(this.$loader);
const projectPath = path.relative(Env.indexDirPath, Env.boardDirPath);
const loader = new KernelLoader({
rootPath: path.join(projectPath, 'deps'),
language: 'python3',
});
const kernel = await loader.kernelAvailable();
if (!kernel) {
return;
}
await kernel.init();
await kernel.loaded();
this.loader = loader;
this.kernel = kernel;
this.statusBarImage = StatusBarImage.init();
this.statusBarFileSystem = StatusBarFileSystem.init();
this.statusBarTool = StatusBarTool.init();
const teachableMachineApp = createApp(TeachableMachineApp);
teachableMachineApp.mount(this.statusBarTool.getContent()[0]);
this.statusBarGame = StatusBarGame.init();
this.pythonShell = new PythonShell();
this.pyodide = window.pyodide;
this.interruptBuffer = new Uint8Array(new ArrayBuffer(1));
this.pyodide.setInterruptBuffer(this.interruptBuffer);
this.kernelLoaded = true;
this.$loader.remove();
if (this.$loader && this.$loader.remove) {
this.$loader.remove();
}
this.$loader = null;
}
this.run = async function () {
if (!this.kernelLoaded) {
return;
}
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
return this.pythonShell.run(code);
}
this.stop = async function () {
if (!this.kernelLoaded) {
return;
}
return this.pythonShell.stop();
}
}
#statusBarTerminal_ = null;
#statusBarImage_ = null;
#statusBarsManager_ = null;
#cursor_ = {
row: 0,
column: 0
};
#prompt_ = '';
#inputResolve_ = null;
#inputReject_ = null;
#waittingForInput_ = false;
#running_ = false;
#kernel_ = null;
#onCursorChangeEvent_ = () => this.#onCursorChange_();
#commands_ = [
{
name: 'REPL-Enter',
bindKey: 'Enter',
exec: (editor) => {
const session = editor.getSession();
const cursor = session.selection.getCursor();
if (cursor.row === this.#cursor_.row) {
const newPos = this.#statusBarTerminal_.getEndPos();
let str = this.#statusBarTerminal_.getValueRange(this.#cursor_, newPos);
str = str.replace(this.#prompt_, '');
this.#inputResolve_?.(str);
this.#inputResolve_ = null;
this.#inputReject_ = null;
this.#statusBarTerminal_.addValue('\n');
this.#exitInput_();
}
return false;
}
}, {
name: 'REPL-ChangeEditor',
bindKey: 'Delete|Ctrl-X|Backspace',
exec: (editor) => {
const session = editor.getSession();
const cursor = session.selection.getCursor();
if (cursor.row < this.#cursor_.row || cursor.column <= this.#cursor_.column) {
return true;
}
return false;
}
}
];
constructor() {
const mainWorkspace = Workspace.getMain();
this.#statusBarsManager_ = mainWorkspace.getStatusBarsManager();
this.#statusBarTerminal_ = this.#statusBarsManager_.getStatusBarById('output');
this.#statusBarImage_ = this.#statusBarsManager_.getStatusBarById('images');
this.#kernel_ = PythonShell.kernel;
this.#addEventsListener_();
}
#addEventsListener_() {
this.#kernel_.addEventListener('eval.finished', () => {
this.#running_ = false;
this.#statusBarTerminal_.addValue(`\n==${Msg.Lang['shell.finish']}==`);
this.syncfs(false).catch(Debug.error);
});
this.#kernel_.addEventListener('eval.output', (data) => {
this.#statusBarTerminal_.addValue(data.content);
});
this.#kernel_.addEventListener('eval.error', () => {
this.#running_ = false;
this.#statusBarTerminal_.addValue(`\n==${Msg.Lang['shell.finish']}==`);
});
this.#kernel_.addEventListener('eval.input', (data) => {
const prompt = String(data?.content?.prompt);
this.#statusBarTerminal_.addValue(prompt);
this.#prompt_ = prompt;
this.#inputResolve_ = data.resolve;
this.#inputReject_ = data.reject;
this.#enterInput_();
});
this.#kernel_.addEventListener('eval.display', (data) => {
this.#statusBarsManager_.changeTo('images');
this.#statusBarImage_.display(data);
});
}
#onCursorChange_() {
const editor = this.#statusBarTerminal_.getEditor();
const session = editor.getSession();
const cursor = session.selection.getCursor();
editor.setReadOnly(
cursor.row < this.#cursor_.row || cursor.column < this.#cursor_.column
);
}
#enterInput_() {
if (!this.#running_) {
return;
}
this.#waittingForInput_ = true;
this.#cursor_ = this.#statusBarTerminal_.getEndPos();
const editor = this.#statusBarTerminal_.getEditor();
editor.setReadOnly(false);
editor.focus();
const session = editor.getSession();
session.selection.on('changeCursor', this.#onCursorChangeEvent_);
editor.commands.addCommands(this.#commands_);
}
#exitInput_() {
this.#waittingForInput_ = false;
const editor = this.#statusBarTerminal_.getEditor();
const session = editor.getSession();
session.selection.off('changeCursor', this.#onCursorChangeEvent_);
editor.commands.removeCommands(this.#commands_);
this.#prompt_ = '';
this.#inputResolve_?.('');
// this.#inputReject_?.({});
this.cursor_ = { row: 0, column: 0 };
editor.setReadOnly(true);
}
async run(code) {
await this.stop();
await this.syncfs(true);
if (code.indexOf('import turtle') !== -1) {
code += '\nturtle.done()\n';
}
if (code.indexOf('import matplotlib.pyplot') !== -1) {
code += '\nplt.clf()\n';
}
mixlySprite.runit(this.#statusBarImage_.getContent()[0]);
if (code.indexOf('import sprite') !== -1 || code.indexOf('from sprite import') !== -1) {
this.#statusBarsManager_.changeTo('images');
} else {
this.#statusBarsManager_.changeTo('output');
}
this.#statusBarsManager_.show();
this.#statusBarTerminal_.setValue(`${Msg.Lang['shell.running']}...\n`);
this.#running_ = true;
this.#kernel_.dispatchEvent('eval.request', {
code,
interactive: false,
});
}
async stop() {
if (this.#waittingForInput_) {
this.#exitInput_();
}
if (this.#running_) {
const timeout = 5;
PythonShell.interruptBuffer[0] = 2;
const startTime = Number(new Date());
while (Number(new Date()) - startTime < timeout * 1000) {
if (this.#running_) {
PythonShell.interruptBuffer[0] = 2;
await this.sleep(100);
} else {
break;
}
}
this.#running_ = false;
}
}
async syncfs(populate = false) {
return new Promise((resolve) => {
window.pyodide.FS.syncfs(populate, resolve);
});
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,496 @@
import * as Blockly from 'blockly/core';
import { layer } from 'layui';
import * as path from 'path';
import $ from 'jquery';
import {
PageBase,
Msg,
HTMLTemplate,
DragV,
StatusBar,
Menu,
Debug,
StatusBarsManager,
Workspace
} from 'mixly';
import FileSystemFileTree from './filesystem-file-tree';
import FILE_SYSTEM_TEMPLATE from '../templates/html/statusbar-filesystem.html';
import FILE_SYSTEM_OPEN_FS_TEMPLATE from '../templates/html/statusbar-filesystem-open-fs.html';
import FILE_SYSTEM_EDITOR_EMPTY_TEMPLATE from '../templates/html/statusbar-filesystem-editor-empty.html';
export default class StatusBarFileSystem extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-filesystem.html',
new HTMLTemplate(FILE_SYSTEM_TEMPLATE)
);
HTMLTemplate.add(
'html/statusbar/statusbar-filesystem-open-fs.html',
new HTMLTemplate(FILE_SYSTEM_OPEN_FS_TEMPLATE)
);
HTMLTemplate.add(
'html/statusbar/statusbar-filesystem-editor-empty.html',
new HTMLTemplate(FILE_SYSTEM_EDITOR_EMPTY_TEMPLATE)
);
this.init = function () {
StatusBarsManager.typesRegistry.register(['file-system'], StatusBarFileSystem);
const mainWorkspace = Workspace.getMain();
const statusBarsManager = mainWorkspace.getStatusBarsManager();
statusBarsManager.add({
type: 'file-system',
id: 'file-system',
name: Blockly.Msg.PYTHON_PYODIDE_FILE_SYSTEM,
title: Blockly.Msg.PYTHON_PYODIDE_FILE_SYSTEM
});
statusBarsManager.changeTo('output');
return statusBarsManager.get('file-system');
}
}
#$close_ = null;
#$fileTree_ = null;
#$editor_ = null;
#$openFS_ = null;
#$editorEmpty_ = null;
#editor_ = null;
#fileTree_ = null;
#drag_ = null;
#fileTreeShown_ = false;
#editorShown_ = false;
#changed_ = false;
#nativefs_ = null;
constructor() {
super();
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-filesystem.html').render());
this.setContent($content);
this.#fileTree_ = new FileSystemFileTree();
this.#$fileTree_ = $content.children('.file-tree');
this.#$openFS_ = $(HTMLTemplate.get('html/statusbar/statusbar-filesystem-open-fs.html').render({
msg: {
loadFS: Blockly.Msg.PYTHON_PYODIDE_LOAD_FILE_SYSTEM
}
}));
this.#$fileTree_.append(this.#$openFS_);
this.#editor_ = new StatusBar();
this.#$editor_ = $content.children('.editor');
this.#$editorEmpty_ = $(HTMLTemplate.get('html/statusbar/statusbar-filesystem-editor-empty.html').render());
this.#$editor_.append(this.#$editorEmpty_);
}
#addEventsListener_() {
this.#drag_ = new DragV(this.getContent()[0], {
min: '150px',
startSize: '15%',
full: [false, false]
});
this.#drag_.bind('sizeChanged', () => {
this.resize();
});
this.#$openFS_.children('button').click(() => {
this.selectFS().catch(Debug.error);
});
this.#fileTree_.bind('beforeSelectLeaf', (selected) => {
const filePath = selected[0].id;
const mode = this.#editor_.getFileMode(path.extname(filePath));
if (!mode) {
layer.msg(Msg.Lang['statusbar.ampy.cannotEdit'], { time: 1000 });
return false;
}
this.#editor_.setMode(mode);
return true;
});
this.#fileTree_.bind('afterSelectLeaf', async (selected) => {
const filePath = selected[0].id;
this.#fileTree_.showProgress();
const fs = this.#fileTree_.getFS();
try {
const result = await fs.readFile(filePath);
this.showEditor();
this.#editor_.setValue(result);
this.#editor_.scrollToTop();
this.#editor_.focus();
this.setStatus(false);
} catch (error) {
Debug.error(error);
this.hideEditor();
this.#fileTree_.deselectAll();
}
this.#fileTree_.hideProgress();
});
this.#fileTree_.bind('afterCreateNode', (folderPath) => {
this.#fileTree_.refreshFolder(folderPath);
});
this.#fileTree_.bind('afterDeleteNode', (folderPath) => {
this.#fileTree_.refreshFolder(folderPath);
});
this.#fileTree_.bind('afterRenameNode', (folderPath) => {
this.#fileTree_.refreshFolder(folderPath);
});
this.#fileTree_.bind('afterRefreshNode', () => {
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
if (!selectedNodeId) {
this.hideEditor();
}
});
const fileTreeContextMenu = this.#fileTree_.getContextMenu();
const fileTreeMenu = fileTreeContextMenu.getItem('menu');
fileTreeMenu.add({
weight: 7,
id: 'copy_path',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['fileTree.copyPath']),
callback: (_, { $trigger }) => {
let outPath = null;
let type = $trigger.attr('type');
if (type === 'root') {
outPath = this.#fileTree_.getRootFolderTitle();
} else {
outPath = $trigger.attr('title');
}
navigator.clipboard.writeText(outPath).catch(Debug.error);
}
}
});
fileTreeMenu.add({
weight: 14,
id: 'sep5',
preconditionFn: ($trigger) => {
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
let type = $trigger.attr('type');
let id = $trigger.attr('id');
if (type === 'file' && selectedNodeId !== id) {
return false;
}
return true;
},
data: '---------'
});
fileTreeMenu.add({
weight: 15,
id: 'refresh',
preconditionFn: ($trigger) => {
const selectedNodeId = this.#fileTree_.getSelectedNodeId();
let type = $trigger.attr('type');
let id = $trigger.attr('id');
if (type === 'file' && selectedNodeId !== id) {
return false;
}
return true;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.ampy.refresh']),
callback: (_, { $trigger }) => {
let type = $trigger.attr('type');
if (type === 'root') {
this.#fileTree_.openRootFolder();
this.#fileTree_.refreshFolder('/');
} else if (type === 'folder') {
let id = $trigger.attr('id');
this.#fileTree_.openNode(id);
this.#fileTree_.refreshFolder(id);
} else {
const nodes = this.#fileTree_.getSelectedNodes();
this.#fileTree_.runEvent('afterSelectLeaf', nodes);
}
}
}
});
fileTreeMenu.add({
weight: 16,
id: 'sep6',
preconditionFn: ($trigger) => {
let type = $trigger.attr('type');
return ['root'].includes(type);
},
data: '---------'
});
fileTreeMenu.add({
weight: 17,
id: 'exit',
preconditionFn: ($trigger) => {
let type = $trigger.attr('type');
return ['root'].includes(type);
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['statusbar.ampy.exit']),
callback: () => {
this.closeFS();
}
}
});
fileTreeMenu.remove('copy');
fileTreeMenu.remove('cut');
fileTreeMenu.remove('paste');
fileTreeMenu.remove('sep2');
const editorContextMenu = this.#editor_.getContextMenu();
const editorMenu = editorContextMenu.getItem('code');
editorMenu.empty();
editorMenu.add({
weight: 0,
id: 'cut',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.cut'], 'Ctrl+X'),
callback: () => this.#editor_.cut()
}
});
editorMenu.add({
weight: 1,
id: 'copy',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.copy'], 'Ctrl+C'),
callback: () => this.#editor_.copy()
}
});
editorMenu.add({
weight: 2,
id: 'paste',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.paste'], 'Ctrl+V'),
callback: () => this.#editor_.paste()
}
});
editorMenu.add({
weight: 3,
id: 'sep1',
data: '---------'
});
editorMenu.add({
weight: 4,
id: 'togglecomment',
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['editor.contextMenu.togglecomment'], 'Ctrl+/'),
callback: () => this.#editor_.commentLine()
}
});
editorMenu.add({
weight: 6,
id: 'sep2',
preconditionFn: () => {
return this.#changed_;
},
data: '---------'
});
editorMenu.add({
weight: 7,
id: 'save',
preconditionFn: () => {
return this.#changed_;
},
data: {
isHtmlName: true,
name: Menu.getItem(Msg.Lang['file.save'], 'Ctrl+S'),
callback: async () => {
await this.put();
}
}
});
const { commands } = this.#editor_.getEditor();
commands.addCommand({
name: "save",
bindKey: "Ctrl-S",
exec: async () => {
if (!this.#changed_) {
return;
}
await this.put();
}
});
}
async put() {
this.#fileTree_.showProgress();
const id = this.#fileTree_.getSelectedNodeId();
const fs = this.#fileTree_.getFS();
try {
await fs.writeFile(id, this.#editor_.getValue());
this.setStatus(false);
} catch (error) {
Debug.error(error);
} finally {
this.#fileTree_.hideProgress();
}
}
showFileTree() {
if (this.#fileTreeShown_) {
return;
}
this.#$openFS_.detach();
this.#$fileTree_.empty();
this.#$fileTree_.append(this.#fileTree_.getContent());
this.#fileTreeShown_ = true;
}
hideFileTree() {
if (!this.#fileTreeShown_) {
return;
}
this.#fileTree_.getContent().detach();
this.#$fileTree_.empty();
this.#$fileTree_.append(this.#$openFS_);
this.#fileTreeShown_ = false;
}
showEditor() {
if (this.#editorShown_) {
return;
}
this.#$editorEmpty_.detach();
this.#$editor_.empty();
this.#$editor_.append(this.#editor_.getContent());
this.#editorShown_ = true;
}
hideEditor() {
if (!this.#editorShown_) {
return;
}
this.#editor_.getContent().detach();
this.#$editor_.empty();
this.#$editor_.append(this.#$editorEmpty_);
this.#editorShown_ = false;
this.setStatus(false);
}
getDrag() {
return this.#drag_;
}
init() {
super.init();
this.hideCloseBtn();
this.#editor_.init();
this.#addEventsListener_();
const editor = this.#editor_.getEditor();
editor.setReadOnly(false);
editor.renderer.setShowGutter(true);
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
newLineMode: 'unix'
});
editor.on('change', () => {
this.setStatus(true);
});
this.loadFS().catch(Debug.error);
}
async loadFS() {
const fs = this.#fileTree_.getFS();
const directoryHandle = await fs.loadFS();
await this.openFS(directoryHandle);
}
async selectFS() {
const fs = this.#fileTree_.getFS();
const directoryHandle = await fs.showDirectoryPicker();
await this.openFS(directoryHandle);
}
async openFS(directoryHandle) {
if (!directoryHandle?.name) {
return;
}
const rootPath = '/' + directoryHandle.name;
this.#fileTree_.setFolderPath('/');
this.#fileTree_.setRootFolderTitle(rootPath);
this.#fileTree_.setRootFolderName(directoryHandle.name);
this.#fileTree_.openRootFolder();
this.showFileTree();
this.#nativefs_ = await window.pyodide.mountNativeFS(rootPath, directoryHandle);
}
closeFS() {
const rootPath = this.#fileTree_.getRootFolderTitle();
const rootContents = Object.keys(window.pyodide.FS.root.contents);
if (rootContents.includes(path.basename(rootPath))) {
const lookup = window.pyodide.FS.lookupPath(rootPath, {
follow_mount: false
});
if (window.pyodide.FS.isMountpoint(lookup.node)) {
window.pyodide.FS.unmount(rootPath);
}
}
const fs = this.#fileTree_.getFS();
fs.setFSCache(null);
this.#fileTree_.deselectAll();
this.hideFileTree();
this.hideEditor();
this.setStatus(false);
}
onMounted() {
super.onMounted();
this.#editor_.onMounted();
this.#fileTree_.onMounted();
// this.#fileTree_.refreshFolder('/');
}
onUnmounted() {
// this.hideEditor();
// this.#fileTree_.deselectAll();
super.onUnmounted();
this.#editor_.onUnmounted();
this.#fileTree_.onUnmounted();
}
resize() {
super.resize();
this.#editor_.resize();
this.#fileTree_.resize();
}
setStatus(isChanged) {
if (this.#changed_ === isChanged) {
return;
}
this.#changed_ = isChanged;
}
getNativeFS() {
return this.#nativefs_;
}
dispose() {
this.#editor_.dispose();
this.#editor_ = null;
this.#fileTree_.dispose();
this.#fileTree_ = null;
super.dispose();
}
}

View File

@@ -0,0 +1,227 @@
import $ from 'jquery';
import { Msg } from 'blockly/core';
import {
PageBase,
HTMLTemplate,
StatusBarsManager,
Workspace
} from 'mixly';
import '../language/loader';
import STATUS_BAR_GAME_TEMPLATE from '../templates/html/statusbar-game.html';
export default class StatusBarGame extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-game.html',
new HTMLTemplate(STATUS_BAR_GAME_TEMPLATE)
);
this.init = function () {
StatusBarsManager.typesRegistry.register(['game'], StatusBarGame);
const mainWorkspace = Workspace.getMain();
const statusBarsManager = mainWorkspace.getStatusBarsManager();
statusBarsManager.add({
type: 'game',
id: 'game',
name: Msg.PYTHON_PYODIDE_GAME,
title: Msg.PYTHON_PYODIDE_GAME
});
statusBarsManager.changeTo('output');
return statusBarsManager.get('game');
}
}
#$startBtn_ = null;
#$pauseBtn_ = null;
#$randomBtn_ = null;
#$resetBtn_ = null;
#$generation_ = null;
#$grid_ = null;
#GRID_SIZE_ = 10;
#SPEED_ = 500;
#grid_ = [];
#isRunning_ = false;
#generation_ = 0;
#intervalId_ = null;
constructor() {
super();
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-game.html').render({
epoch: Msg.PYTHON_PYODIDE_GAME_EPOCH,
start: Msg.PYTHON_PYODIDE_GAME_START,
pause: Msg.PYTHON_PYODIDE_GAME_PAUSE,
random: Msg.PYTHON_PYODIDE_GAME_RANDOM,
reset: Msg.PYTHON_PYODIDE_GAME_RESET
}));
this.setContent($content);
this.#$startBtn_ = $content.find('.start-btn');
this.#$pauseBtn_ = $content.find('.pause-btn');
this.#$randomBtn_ = $content.find('.random-btn');
this.#$resetBtn_ = $content.find('.reset-btn');
this.#$generation_ = $content.find('.generation');
this.#$grid_ = $content.find('.grid');
this.#addEventListeners_();
}
#addEventListeners_() {
this.#$startBtn_.click(() => this.startGame());
this.#$pauseBtn_.click(() => this.pauseGame());
this.#$randomBtn_.click(() => this.randomInitialize());
this.#$resetBtn_.click(() => this.resetGame());
}
// 初始化网格
initializeGrid() {
this.#$grid_.empty();
this.#grid_ = [];
for (let i = 0; i < this.#GRID_SIZE_; i++) {
this.#grid_[i] = [];
for (let j = 0; j < this.#GRID_SIZE_; j++) {
this.#grid_[i][j] = 0; // 0表示死亡1表示存活
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = i;
cell.dataset.col = j;
cell.addEventListener('click', () => this.toggleCell(i, j));
this.#$grid_.append(cell);
}
}
this.updateGridDisplay();
}
// 切换细胞状态
toggleCell(row, col) {
if (!this.#isRunning_) {
this.#grid_[row][col] = this.#grid_[row][col] === 0 ? 1 : 0;
this.updateGridDisplay();
}
}
// 更新网格显示
updateGridDisplay() {
const $cells = this.#$grid_.children('.cell');
for (let i = 0; i < $cells.length; i++) {
const cell = $cells[i];
const row = parseInt(cell.dataset.row);
const col = parseInt(cell.dataset.col);
if (this.#grid_[row][col] === 1) {
cell.classList.add('alive');
} else {
cell.classList.remove('alive');
}
}
}
// 计算下一代
nextGeneration() {
const newGrid = [];
for (let i = 0; i < this.#GRID_SIZE_; i++) {
newGrid[i] = [];
for (let j = 0; j < this.#GRID_SIZE_; j++) {
const neighbors = this.countNeighbors(i, j);
if (this.#grid_[i][j] === 1) {
// 存活细胞周围有2-3个存活细胞则继续存活
newGrid[i][j] = (neighbors === 2 || neighbors === 3) ? 1 : 0;
} else {
// 死亡细胞周围有3个存活细胞则复活
newGrid[i][j] = neighbors === 3 ? 1 : 0;
}
}
}
this.#grid_ = newGrid;
this.#generation_++;
this.#$generation_.text(this.#generation_);
this.updateGridDisplay();
}
// 计算周围存活细胞数量
countNeighbors(row, col) {
let count = 0;
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) continue; // 跳过自身
const newRow = row + i;
const newCol = col + j;
// 检查边界
if (newRow >= 0 && newRow < this.#GRID_SIZE_ && newCol >= 0 && newCol < this.#GRID_SIZE_) {
count += this.#grid_[newRow][newCol];
}
}
}
return count;
}
// 开始游戏
startGame() {
if (!this.#isRunning_) {
this.#isRunning_ = true;
this.#generation_ = 0;
this.#$generation_.text(this.#generation_);
this.#intervalId_ = setInterval(() => this.nextGeneration(), this.#SPEED_);
this.updateButtons();
}
}
// 暂停游戏
pauseGame() {
if (this.#isRunning_) {
this.#isRunning_ = false;
clearInterval(this.#intervalId_);
this.updateButtons();
}
}
// 随机初始化网格
randomInitialize() {
if (!this.#isRunning_) {
for (let i = 0; i < this.#GRID_SIZE_; i++) {
for (let j = 0; j < this.#GRID_SIZE_; j++) {
// 25%的概率生成存活细胞
this.#grid_[i][j] = Math.random() < 0.25 ? 1 : 0;
}
}
this.updateGridDisplay();
}
}
// 重置游戏
resetGame() {
this.#isRunning_ = false;
clearInterval(this.#intervalId_);
this.#generation_ = 0;
this.#$generation_.text(this.#generation_);
this.initializeGrid();
this.updateButtons();
}
// 更新按钮状态
updateButtons() {
this.#$startBtn_.attr('disabled', this.#isRunning_);
this.#$pauseBtn_.attr('disabled', !this.#isRunning_);
this.#$randomBtn_.attr('disabled', this.#isRunning_);
this.#$resetBtn_.attr('disabled', false);
}
init() {
super.init();
this.hideCloseBtn();
this.initializeGrid();
this.updateButtons();
}
onMounted() { }
onUnmounted() { }
resize() { }
}

View File

@@ -0,0 +1,173 @@
import $ from 'jquery';
import { Msg } from 'blockly/core';
import {
PageBase,
HTMLTemplate,
StatusBarsManager,
Workspace
} from 'mixly';
import '../language/loader';
import STATUS_BAR_IMAGE_TEMPLATE from '../templates/html/statusbar-image.html';
export default class StatusBarImage extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-image.html',
new HTMLTemplate(STATUS_BAR_IMAGE_TEMPLATE)
);
this.init = function () {
StatusBarsManager.typesRegistry.register(['images'], StatusBarImage);
const mainWorkspace = Workspace.getMain();
const statusBarsManager = mainWorkspace.getStatusBarsManager();
statusBarsManager.add({
type: 'images',
id: 'images',
name: Msg.PYTHON_PYODIDE_IMAGE,
title: Msg.PYTHON_PYODIDE_IMAGE
});
statusBarsManager.changeTo('output');
return statusBarsManager.get('images');
}
}
constructor() {
super();
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-image.html').render());
this.setContent($content);
}
init() {
super.init();
this.hideCloseBtn();
}
clean() {
this.getContent().empty();
}
display(data) {
const $content = this.getContent();
const autoFit = function (node) {
node.style.width = 'auto';
node.style.height = 'auto';
node.style.maxWidth = '100%';
node.style.maxHeight = '100%';
};
this.clean();
let root = data.content;
let canvas = null;
let iframe = null;
switch (data.display_type) {
case 'p5':
root.style.width = '100%';
root.style.height = '100%';
root.style.display = 'flex';
root.style.justifyContent = 'center';
root.style.alignItems = 'center';
// some canvas nodes can be added later so we observe...
new MutationObserver(function (mutationsList) {
mutationsList.forEach((mutation) =>
mutation.addedNodes.forEach((node) => {
const elem = node;
if (
elem.tagName != null &&
['canvas', 'video'].includes(elem.tagName.toLowerCase())
)
autoFit(elem);
})
);
}).observe(root, { childList: true });
root.querySelectorAll('canvas,video').forEach(autoFit);
$content.append(root);
break;
case 'matplotlib':
canvas = root.querySelector('canvas');
if (canvas) root = canvas;
root.style.width = '';
root.style.height = '';
root.style.maxWidth = '100%';
root.style.maxHeight = '100%';
$content.append(root);
break;
case 'ocaml-canvas':
root.style.width = '';
root.style.height = '';
root.style.maxWidth = '100%';
root.style.maxHeight = '100%';
$content.append(root);
break;
case 'turtle':
// Turtle result
root.setAttribute('width', '100%');
root.setAttribute('height', '100%');
$content.append(root.outerHTML);
break;
case 'sympy':
$content.append(data.content);
if (typeof window.MathJax === 'undefined') {
// dynamically loading MathJax
console.log('Loading MathJax (Sympy expression needs it).');
(function () {
let script = document.createElement('script');
script.type = 'text/javascript';
script.src =
'https://cdn.jsdelivr.net/npm/mathjax@3.0.5/es5/tex-mml-chtml.js';
document.getElementsByTagName('head')[0].appendChild(script);
})();
} else {
// otherwise, render it
window.MathJax.typeset();
}
break;
case 'multiple':
/* typically dispached by display() */
for (let mime of [
'image/svg+xml',
'image/png',
'text/html',
'text/plain',
]) {
if (mime in data.content) {
let content = data.content[mime];
if (mime === 'image/png') {
content =
'<img src="data:image/png;base64,' +
content +
'" style="max-width: 100%; max-height: 100%;">';
}
$content.append(content);
break;
}
}
break;
case 'tutor':
// hacky but iframe.document.body.style require to wait for
// iframe loading
$content.append($(data.content.replace('overflow-y%3A%20hidden%3B', '')));
iframe = this.getContent()[0].getElementsByTagName('iframe')[0];
if (iframe == null) return;
// trick to avoid taking height update into account
iframe.style.maxHeight = iframe.style.minHeight = '100%';
// force rendering when visible,
// otherwise, strange things happends
// since PythonTutor check for visibility at some point
new IntersectionObserver((entries, observer) => {
const entry = entries[0];
if (entry && !entry.isIntersecting) return;
iframe.contentWindow?.postMessage({ type: 'redraw' }, '*');
observer.disconnect();
}).observe(iframe);
break;
default:
console.error(
`Not supported node type '${data.display_type}' in eval.display result processing.`
);
}
}
}

View File

@@ -0,0 +1,51 @@
import $ from 'jquery';
import { Msg } from 'blockly/core';
import {
PageBase,
HTMLTemplate,
StatusBarsManager,
Workspace
} from 'mixly';
import '../language/loader';
import STATUS_BAR_TOOL_TEMPLATE from '../templates/html/statusbar-tool.html';
export default class StatusBarTool extends PageBase {
static {
HTMLTemplate.add(
'html/statusbar/statusbar-tool.html',
new HTMLTemplate(STATUS_BAR_TOOL_TEMPLATE)
);
this.init = function () {
StatusBarsManager.typesRegistry.register(['tool'], StatusBarTool);
const mainWorkspace = Workspace.getMain();
const statusBarsManager = mainWorkspace.getStatusBarsManager();
statusBarsManager.add({
type: 'tool',
id: 'tool',
name: Msg.PYTHON_PYODIDE_TOOL,
title: Msg.PYTHON_PYODIDE_TOOL
});
statusBarsManager.changeTo('output');
return statusBarsManager.get('tool');
}
}
constructor() {
super();
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-tool.html').render());
this.setContent($content);
}
init() {
super.init();
this.hideCloseBtn();
}
onMounted() { }
onUnmounted() { }
resize() { }
}

View File

@@ -0,0 +1,29 @@
<script setup>
import { onMounted, provide, reactive, ref } from 'vue';
import { ElConfigProvider } from 'element-plus';
import teachableModel from './components/teachableModel.vue';
// import 'element-plus/theme-chalk/el-message.css';
// import 'element-plus/theme-chalk/el-message-box.css';
// import 'element-plus/theme-chalk/el-notification.css';
// import './styles/index.scss';
</script>
<template>
<ElConfigProvider>
<teachableModel />
</ElConfigProvider>
</template>
<style>
/* #app {
text-align: center;
color: var(--ep-text-color-primary);
}
.main-container {
height: calc(100vh - var(--ep-menu-item-height) - 4px);
background-color: #feffff;
} */
</style>

View File

@@ -0,0 +1,213 @@
<script setup>
import { provide, ref } from 'vue';
import { CirclePlusFilled } from '@element-plus/icons-vue';
import { ElMessage, ElSplitter, ElSplitterPanel, ElCard, ElButton, ElIcon } from 'element-plus';
import ClassBox from './teachableModel/ClassBox.vue';
import CameraBox from './teachableModel/CameraBox.vue';
import ModelArea from './teachableModel/ModelArea.vue';
import TrainArea from './teachableModel/TrainArea.vue';
// import 'element-plus/theme-chalk/src/message.scss';
// import 'element-plus/theme-chalk/src/splitter.scss';
// import 'element-plus/theme-chalk/src/splitter-panel.scss';
// import 'element-plus/theme-chalk/src/card.scss';
// import 'element-plus/theme-chalk/src/button.scss';
// import 'element-plus/theme-chalk/src/icon.scss';
// 存放所有拍摄的内容
const shotList = ref([])
provide('shotList', shotList)
// 存放类别和每个类别内含有的图片样本
const picList = ref([
{ title: '类别 1', list: [], disabled: false },
{ title: '类别 2', list: [], disabled: false },
])
provide('picList', picList)
// 训练状态
const states = ref({
isTraining: 0, // 0为未开始1为训练中2为训练完成
})
provide('states', states)
// 向某个类别添加一个样本
async function handleAddSample(idx, data) {
// 类别已被禁用
if (picList.value[idx].disabled) {
ElMessage.error('该类别已被禁用')
return
}
// 可能传入 base64 字符串上传图片
if (data) {
// data 是 base64说明是上传图片
picList.value[idx].list.push(data)
return
}
// 否则是拍摄,调用摄像头拍摄
await handleParentShot()
// 检查是否成功拍摄到图片
if (
!shotList.value[shotList.value.length - 1]
|| shotList.value[shotList.value.length - 1] === 'data:,'
) {
ElMessage.error('未获取到有效样本')
return
}
// 拍摄到的图片添加到类别中
picList.value[idx].list.push(shotList.value[shotList.value.length - 1])
}
// 添加一个类别
function handleAddClass() {
picList.value.push({
title: `类别 ${picList.value.length + 1}`,
list: [],
disabled: false,
})
}
// 删除一个类别
function handleDeleteClass(idx) {
picList.value.splice(idx, 1)
}
// 禁用/启用一个类别
function handleDisableClass(idx) {
picList.value[idx].disabled = !picList.value[idx].disabled
}
// 重命名一个类别
function handleRenameClass(idx, newTitle) {
picList.value[idx].title = newTitle
}
// 清空一个类别的样本
function handleClearSamples(idx) {
picList.value[idx].list = []
}
// 显示模型区域与否
const showModelArea = ref(true)
// 令摄像头拍摄一张图片加入shotList
const cameraBoxRef = ref(null)
async function handleParentShot() {
await cameraBoxRef.value?.captureShot()
}
// 训练模型
const modelAreaRef = ref(null)
async function handleTrain() {
await modelAreaRef.value.train()
}
</script>
<template>
<div class="main-splitter">
<ElSplitter>
<ElSplitterPanel size="70%" :min="200">
<div class="class-area">
<ClassBox v-for="(item, idx) in picList" :key="idx" :title="item.title" :pic-list="item.list"
:disabled="item.disabled" @add-sample="(data) => handleAddSample(idx, data)" @delete-sample="
(url) => (picList[idx].list = picList[idx].list.filter((i) => i !== url))
" @delete-class="handleDeleteClass(idx)" @disable-class="handleDisableClass(idx)"
@rename-class="(newTitle) => handleRenameClass(idx, newTitle)"
@clear-samples="handleClearSamples(idx)">
<ElButton type="primary" size="mini" @click="handleAddSample(idx)">
添加样本
</ElButton>
</ClassBox>
<ElCard class="add-class-card" @click="handleAddClass">
<div class="add-class-content">
添加一个类别
<ElIcon>
<CirclePlusFilled />
</ElIcon>
</div>
</ElCard>
</div>
</ElSplitterPanel>
<ElSplitterPanel :min="200">
<CameraBox ref="cameraBoxRef" />
<transition name="fade-scale">
<ModelArea v-if="showModelArea" ref="modelAreaRef" @shot="handleParentShot" />
</transition>
<TrainArea @train="handleTrain" />
</ElSplitterPanel>
</ElSplitter>
</div>
</template>
<style scoped>
.title {
font-size: 24px;
font-weight: 500;
color: #011216;
margin: 10px;
padding: 10px;
border-radius: 5px;
background: linear-gradient(to right, #d7dee1, transparent, transparent, transparent);
text-align: left;
}
.teachable-model-container {
width: 80%;
height: 80vh;
margin: 20px;
padding: 5px;
border: 2px solid #000;
border-radius: 5px;
box-shadow: 0px 0px 10px #000;
}
.main-splitter {
height: 100%;
width: 100%;
box-shadow: var(--el-border-color-light) 0px 0px 10px;
user-select: none;
}
.add-class-card {
margin: 10px auto;
border: 1px dotted #000;
max-width: 50vw;
cursor: default;
user-select: none;
box-sizing: border-box;
}
.add-class-card:hover {
cursor: pointer;
color: #409eff;
}
.fade-scale-enter-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
.fade-scale-leave-active {
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
}
.fade-scale-enter-from {
opacity: 0;
transform: scale(0.8);
}
.fade-scale-enter-to {
opacity: 1;
transform: scale(1);
}
.fade-scale-leave-from {
opacity: 1;
transform: scale(1);
}
.fade-scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
</style>

View File

@@ -0,0 +1,92 @@
<script setup>
import { defineExpose, inject, onMounted, ref } from 'vue';
const videoRef = ref(null)
const shotList = inject('shotList')
// 初始化摄像头
onMounted(() => {
navigator.mediaDevices
.getUserMedia({ video: true })
.then((stream) => {
if (videoRef.value) {
videoRef.value.srcObject = stream
}
})
.catch((err) => {
console.error('无法访问摄像头:', err)
})
})
// 拍摄照片转为正方形存入shotList
function captureShot() {
const video = videoRef.value
if (!video)
return
const vw = video.videoWidth
const vh = video.videoHeight
let sx = 0
let sy = 0
let sw = vw
let sh = vh
if (vw > vh) {
sx = (vw - vh) / 2
sw = vh
}
else if (vh > vw) {
sy = (vh - vw) / 2
sh = vw
}
const size = Math.min(sw, sh)
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const ctx = canvas.getContext('2d')
ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size)
shotList.value.push(canvas.toDataURL('image/png'))
return canvas.toDataURL('image/png')
}
// 暴露方法
defineExpose({ captureShot })
</script>
<template>
<div class="camera-box">
<video id="webcam" ref="videoRef" class="video-crop" autoplay playsinline muted height="100%" width="100%" />
</div>
</template>
<style scoped>
.camera-box {
margin: 10px auto;
padding: 10px;
border-radius: 20px;
width: 80%;
max-width: 250px;
aspect-ratio: 1/1;
display: flex;
justify-content: center;
align-items: center;
/* background-color: black; */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.canvas {
width: 100%;
}
.video-crop {
max-width: 250px;
aspect-ratio: 1/1;
border-radius: 10px;
object-fit: cover;
object-position: center center;
overflow: hidden;
}
.camera-box {
cursor: default;
user-select: none;
}
</style>

View File

@@ -0,0 +1,357 @@
<script setup>
import { Camera, CirclePlus, Delete, EditPen, More } from '@element-plus/icons-vue';
import { saveAs } from 'file-saver';
import JSZip from 'jszip';
import { nextTick, ref, watch, TransitionGroup } from 'vue';
import { ElCard, ElDropdown, ElDropdownMenu, ElDropdownItem, ElButton, ElUpload, ElImage, ElInput, ElIcon } from 'element-plus';
// import 'element-plus/theme-chalk/el-card.css';
// import 'element-plus/theme-chalk/el-dropdown.css';
// import 'element-plus/theme-chalk/el-dropdown-menu.css';
// import 'element-plus/theme-chalk/el-dropdown-item.css';
// import 'element-plus/theme-chalk/el-button.css';
// import 'element-plus/theme-chalk/el-upload.css';
// import 'element-plus/theme-chalk/el-image.css';
// import 'element-plus/theme-chalk/el-input.css';
const props = defineProps({
title: {
type: String,
default: '类别',
},
picList: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits([
'add-sample',
'delete-sample',
'rename-class',
'delete-class',
'disable-class',
'clear-samples',
])
const localTitle = ref(props.title)
const editing = ref(false)
const inputTitle = ref(localTitle.value)
const inputRef = ref(null)
const addSampleTimer = ref(null)
function editTitle() {
if (!editing.value) {
editing.value = true
inputTitle.value = localTitle.value
nextTick(() => {
if (inputRef.value && inputRef.value.input) {
inputRef.value.input.select()
}
})
}
}
function saveTitle() {
if (inputTitle.value.trim()) {
localTitle.value = inputTitle.value.trim()
emit('rename-class', localTitle.value)
}
editing.value = false
}
function handleAddSample() {
emit('add-sample')
}
function handleDeleteSample(url) {
emit('delete-sample', url)
}
function startAddSample() {
handleAddSample()
addSampleTimer.value = setInterval(() => {
handleAddSample()
}, 200) // 200ms 可根据需要调整
}
function stopAddSample() {
if (addSampleTimer.value) {
clearInterval(addSampleTimer.value)
addSampleTimer.value = null
}
}
watch(
() => props.title,
(val) => {
localTitle.value = val
},
)
function handleDeleteClass() {
emit('delete-class')
}
function handleDisableClass() {
emit('disable-class')
}
function handleClearSamples() {
emit('clear-samples')
}
function handleExportSamples() {
// 导出为zip
const zip = new JSZip()
props.picList.forEach((url, idx) => {
const base64 = url.split(',')[1]
zip.file(`sample_${idx + 1}.png`, base64, { base64: true })
})
zip.generateAsync({ type: 'blob' }).then((content) => {
saveAs(content, 'samples.zip')
})
}
// 新增:处理图片上传
// 修改处理函数
function handleUploadSample(file) {
return new Promise((resolve, reject) => {
// 新增ZIP文件处理
if (file.type === 'application/zip' || file.name.endsWith('.zip')) {
const zip = new JSZip()
zip
.loadAsync(file)
.then((zipContents) => {
const imageFiles = Object.values(zipContents.files).filter(
file => !file.dir && file.name.match(/\.(png|jpg|jpeg|gif|bmp)$/i),
)
Promise.all(
imageFiles.map((zipFile) => {
return zip
.file(zipFile.name)
.async('blob')
.then((blob) => {
const extractedFile = new File([blob], zipFile.name, {
type: blob.type,
})
const reader = new FileReader()
reader.readAsDataURL(extractedFile)
return new Promise(r => (reader.onload = r))
})
}),
).then((readers) => {
readers.forEach(e => emit('add-sample', e.target.result))
resolve(false)
})
})
.catch(reject)
}
else {
// 原有图片处理逻辑
const reader = new FileReader()
reader.onload = (e) => {
emit('add-sample', e.target.result)
resolve(false)
}
reader.onerror = reject
reader.readAsDataURL(file)
}
})
}
</script>
<template>
<ElCard class="class-box">
<template #header>
<div class="card-header">
<span class="card-header-title" style="cursor: pointer" @click="editTitle">
<template v-if="!editing">
{{ localTitle }}
<ElIcon @click.stop="editTitle">
<EditPen />
</ElIcon>
<span v-if="props.disabled" class="disabled-tag"
style="color: #f56c6c; margin-left: 8px; font-size: 13px">
禁用
</span>
</template>
<template v-else>
<ElInput ref="inputRef" v-model="inputTitle" style="width: 120px" @keyup.enter="saveTitle"
@blur="saveTitle" />
</template>
</span>
<ElDropdown class="card-header-title">
<span class="el-dropdown-link">
<ElIcon>
<More />
</ElIcon>
</span>
<template #dropdown>
<ElDropdownMenu class="class-box-dropdown">
<ElDropdownItem class="class-box-dropdown-item" @click="handleDeleteClass">
<span style="color: #f56c6c">删除类别</span>
</ElDropdownItem>
<ElDropdownItem class="class-box-dropdown-item" @click="handleDisableClass">
{{ props.disabled ? "启用" : "禁用" }}类别
</ElDropdownItem>
<ElDropdownItem class="class-box-dropdown-item" :disabled="props.picList.length === 0"
@click="handleClearSamples">
清空样本
</ElDropdownItem>
<ElDropdownItem class="class-box-dropdown-item" :disabled="props.picList.length === 0"
@click="handleExportSamples">
导出样本
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>
<div class="card-content" style="flex-direction: column; display: flex">
<div class="card-content-info">
图像样本 {{ props.picList.length }}
<div style="display: flex; gap: 4px">
<ElButton type="primary" size="small" :disabled="props.disabled" @mousedown="startAddSample"
@mouseup="stopAddSample" @mouseleave="stopAddSample">
拍摄样本
<ElIcon>
<Camera />
</ElIcon>
</ElButton>
<ElUpload :show-file-list="false" :before-upload="handleUploadSample" accept="image/*,.zip"
:disabled="props.disabled">
<ElButton plain type="primary" size="small" :disabled="props.disabled">
上传样本
<ElIcon>
<CirclePlus />
</ElIcon>
</ElButton>
</ElUpload>
</div>
</div>
<TransitionGroup name="fade" tag="div" class="card-content-img">
<div v-for="url in props.picList.slice().reverse()" :key="url" class="card-content-img-wrapper"
style="position: relative; display: inline-block">
<ElImage :src="url" class="card-content-img-item" :class="{ 'img-disabled': props.disabled }" />
<ElIcon class="delete-icon" @click="handleDeleteSample(url)">
<Delete />
</ElIcon>
</div>
</TransitionGroup>
</div>
</ElCard>
</template>
<style>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 10px;
}
.card-header-title:hover {
color: #409eff;
cursor: pointer;
}
.card-content {
display: flex;
flex-direction: column;
}
.card-content-img {
display: flex;
flex-direction: row;
width: 100%;
overflow-x: auto;
white-space: nowrap;
height: 70px;
}
.card-content-img-item {
margin: 2px;
height: 60px;
aspect-ratio: 1/1;
border-radius: 10px;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
object-fit: cover;
flex-shrink: 0;
}
.card-header-title {
display: flex;
align-items: center;
gap: 4px;
}
.card-content-info {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
.card-content-img-wrapper {
position: relative;
display: inline-block;
}
.delete-icon {
position: absolute;
top: 2px;
right: 2px;
font-size: 16px;
z-index: 2;
border-radius: 50%;
padding: 2px;
transition: background 0.2s;
color: #fff;
opacity: 0;
pointer-events: none;
}
.delete-icon:hover {
background: #f56c6c;
}
.card-content-img-wrapper:hover .delete-icon {
opacity: 1;
pointer-events: auto;
}
.img-disabled {
filter: grayscale(1) brightness(0.7);
pointer-events: none;
}
.disabled-tag {
color: #f56c6c;
margin-left: 8px;
font-size: 13px;
}
.class-box {
margin: 10px auto;
max-width: 50vw;
cursor: default;
user-select: none;
}
</style>

View File

@@ -0,0 +1,517 @@
<script setup>
// import * as mobilenet from "@tensorflow-models/mobilenet";
import * as tf from "@tensorflow/tfjs";
import * as tfvis from "@tensorflow/tfjs-vis";
import * as path from "path";
import { inject, ref } from "vue";
import {
ElMessage,
ElButton,
ElCard,
ElRow,
ElCol,
ElInput,
ElProgress,
ElUpload,
} from "element-plus";
import { Env } from "mixly";
// import 'element-plus/theme-chalk/el-message.css';
// import 'element-plus/theme-chalk/el-button.css';
// import 'element-plus/theme-chalk/el-card.css';
// import 'element-plus/theme-chalk/el-row.css';
// import 'element-plus/theme-chalk/el-col.css';
// import 'element-plus/theme-chalk/el-input.css';
// import 'element-plus/theme-chalk/el-progress.css';
// import 'element-plus/theme-chalk/el-upload.css';
const emit = defineEmits(["shot"]);
// 类别及其样本的列表
const picList = inject("picList");
// 图片列表
const shotList = inject("shotList");
// 训练状态
const states = inject("states");
// 用来显示进度条和名称
const classList = ref(
picList.value.map((item, idx) => ({
name: item.title,
progress: 0,
}))
);
const progressColors = ["#FF6F61", "#42A5F5", "#66BB6A", "#FFA726", "#AB47BC"];
// 用来存储模型
let featureExtractor;
// 用来存储损失值
let lossValues = [];
// 存储被训练的模型
let model;
// 单击训练按钮后训练模型
async function train() {
// 可视化相关
const visPanel = document.getElementById("vis-left-panel");
if (visPanel) visPanel.innerHTML = "训练准备中……";
showVisPanel.value = true;
console.log("正在加载Mobilenet……");
// net = await mobilenet.load();
featureExtractor = await tf.loadGraphModel(
path.join(Env.boardDirPath, "teachableModel/model.json")
);
console.log("Mobilenet加载完成。");
if (visPanel) visPanel.innerHTML = "";
// 准备数据
const NUM_CLASSES = picList.value.length;
let xs = null;
let ys = null;
// 准备模型
model = tf.sequential();
// 添加全连接层
model.add(
tf.layers.dense({
inputShape: [1280],
units: 128,
activation: "relu",
})
);
// 添加分类层
model.add(
tf.layers.dense({
units: NUM_CLASSES,
activation: "softmax",
})
);
// 编译模型
model.compile({
optimizer: tf.train.adam(0.001),
loss: "categoricalCrossentropy",
metrics: ["accuracy"],
});
for (let classId = 0; classId < picList.value.length; classId++) {
if (picList.value[classId].disabled) continue;
const images = picList.value[classId].list;
for (let i = 0; i < images.length; i++) {
// 加载图片
const imgElement = new Image();
imgElement.src = images[i];
await new Promise((resolve) => {
imgElement.onload = resolve;
});
// 将图片转化为张量
const imgTensor = tf.browser.fromPixels(imgElement);
// 使用神经网络模型进行推理,获取名为"conv_preds"的卷积层激活值
// let activation = net.infer(imgTensor, "conv_preds");
// 加载特征提取器和你的模型
// 预处理图像并提取特征
const preprocessedImg = imgTensor
.resizeBilinear([224, 224])
.toFloat()
.div(tf.scalar(127.5))
.sub(tf.scalar(1))
.expandDims(0);
const features = featureExtractor.predict(preprocessedImg);
// 将特征输入你的模型
// const predictions = net.predict(features);
let activation = features;
// if (activation.shape.length === 3) {
// activation = activation.reshape([1, 1024]);
// }
// let activation = net.predict(imgTensor);
// 检查激活值张量的维度如果是3维张量则进行形状重塑
// 3维形状通常为 [height, width, channels],重塑为 [1, 1024] 的张量
// 为了适配后续分类层或特征可视化的输入要求
// 转换为one-hot编码
const y = tf.oneHot(tf.tensor1d([classId]).toInt(), NUM_CLASSES);
// 初始化xs和ys
if (xs == null) {
xs = activation.clone();
ys = y.clone();
} else {
const oldXs = xs;
xs = oldXs.concat(activation, 0);
oldXs.dispose();
const oldYs = ys;
ys = oldYs.concat(y, 0);
oldYs.dispose();
}
y.dispose();
imgTensor.dispose();
}
}
// 训练过程可视化相关
lossValues = [];
const metrics = ["loss", "acc", "val_loss", "val_acc", "accuracy", "val_accuracy"];
const container = document.getElementById("vis-left-panel") || {
name: "训练过程",
tab: "训练",
};
// 训练模型
await model.fit(xs, ys, {
epochs: 20,
batchSize: 16,
shuffle: true,
validationSplit: 0.2,
callbacks: tfvis.show.fitCallbacks(container, metrics, {
callbacks: ["onEpochEnd"],
}),
});
console.log("训练完成");
// 与显示进度条相关
classList.value = picList.value
.filter((item) => item.disabled !== true)
.map((item, idx) => ({
name: item.title,
progress: 0,
}));
console.log(classList.value);
states.value.isTraining = 2;
}
setInterval(async () => {
// 没训练完成跳过
if (states.value.isTraining !== 2) return;
// 输入是“上传图片”时跳过
if (uploadedImg.value !== "") return;
// 通知摄像头拍摄照片
emit("shot");
// 加载拍摄的照片
const img = shotList.value[shotList.value.length - 1];
if (!img || img === "data:,") {
ElMessage.error("未获取到有效样本");
return;
}
const imgElement = new Image();
imgElement.src = img;
await new Promise((resolve) => {
imgElement.onload = resolve;
});
// 将图片转换为张量
const imgTensor = tf.browser.fromPixels(imgElement);
// let activation = net.infer(imgTensor, "conv_preds");
let resized = tf.image.resizeBilinear(imgTensor, [224, 224]);
let batched = resized.expandDims(0);
let normalized = batched.div(255);
// let activation = net.predict(imgTensor);
let activation = featureExtractor.predict(normalized);
const pred = model.predict(activation);
const predArr = await pred.data();
// console.log(predArr)
classList.value = [
...classList.value.map((item, idx) => ({
...item,
progress: Number((predArr[idx] * 100).toFixed(2)),
})),
];
// console.log(classList.value)
imgTensor.dispose();
activation.dispose();
pred.dispose();
}, 200);
const showVisPanel = ref(false);
const uploadedImg = ref("");
const uploadedResult = ref("");
const panelTop = ref("20%");
const panelRight = ref("20%");
async function exportModel() {
if (!model) {
ElMessage.error("模型尚未训练完成");
return;
}
try {
// 导出模型到本地文件系统
await model.save(
`downloads://${modelName.value == "" ? "my-model" : modelName.value}`
);
ElMessage.success("模型导出成功");
} catch (error) {
ElMessage.error(`模型导出失败: ${error.message}`);
}
}
async function handleUpload(file) {
const reader = new FileReader();
reader.onload = async (e) => {
uploadedImg.value = e.target.result;
if (!featureExtractor || !model) {
ElMessage.error("请先完成模型训练");
return false;
}
const imgElement = new window.Image();
imgElement.src = uploadedImg.value;
await new Promise((resolve) => (imgElement.onload = resolve));
const imgTensor = tf.browser.fromPixels(imgElement);
// let activation = net.infer(imgTensor, "conv_preds");
let resized = tf.image.resizeBilinear(imgTensor, [224, 224]);
let batched = resized.expandDims(0);
let normalized = batched.div(255);
// let activation = net.predict(imgTensor);
let activation = featureExtractor.predict(normalized);
const pred = model.predict(activation);
const predArr = await pred.data();
classList.value = [
...classList.value.map((item, idx) => ({
...item,
progress: Number((predArr[idx] * 100).toFixed(2)),
})),
];
const maxIdx = predArr.indexOf(Math.max(...predArr));
uploadedResult.value = classList.value[maxIdx]?.name || "未知";
imgTensor.dispose();
activation.dispose();
pred.dispose();
};
reader.readAsDataURL(file);
return false;
}
function switchToUpload() {
uploadedImg.value = "";
}
let startX = 0;
let startY = 0;
let startTop = 0;
let startRight = 0;
function onDragStart(e) {
if (e.button !== 0) return;
startX = e.clientX;
startY = e.clientY;
const topVal = document.getElementById("vis-left-panel-wrapper").style.top;
const rightVal = document.getElementById("vis-left-panel-wrapper").style.right;
startTop = topVal.endsWith("%")
? (window.innerHeight * Number.parseFloat(topVal)) / 100
: Number.parseFloat(topVal);
startRight = rightVal.endsWith("%")
? (window.innerWidth * Number.parseFloat(rightVal)) / 100
: Number.parseFloat(rightVal);
document.addEventListener("mousemove", onDragging);
document.addEventListener("mouseup", onDragEnd);
}
function onDragging(e) {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newTop = startTop + deltaY;
let newRight = startRight - deltaX;
newTop = Math.max(0, Math.min(window.innerHeight - 100, newTop));
newRight = Math.max(0, Math.min(window.innerWidth - 200, newRight));
panelTop.value = `${newTop}px`;
panelRight.value = `${newRight}px`;
}
function onDragEnd() {
document.removeEventListener("mousemove", onDragging);
document.removeEventListener("mouseup", onDragEnd);
}
defineExpose({ train });
const modelName = ref("");
async function saveModel() {
if (!model) {
ElMessage.error("模型尚未训练完成");
return;
}
try {
// 导出模型到本地文件系统
await model.save(
`indexeddb://${modelName.value == "" ? "my-model" : modelName.value}`
);
ElMessage.success("模型保存成功");
} catch (error) {
ElMessage.error(`模型保存失败: ${error.message}`);
}
}
</script>
<template>
<div>
<div
v-show="showVisPanel"
id="vis-left-panel-wrapper"
class="vis-left-panel-wrapper"
:style="`right: ${panelRight}; top: ${panelTop};`"
>
<div class="vis-left-panel-inner-wrapper">
<div class="vis-left-panel-title" style="" @mousedown="onDragStart">
<span> 训练过程可视化 </span>
<ElButton size="small" plain @click="showVisPanel = false"> 隐藏 </ElButton>
</div>
<div id="vis-left-panel" class="vis-left-panel">训练准备中</div>
</div>
</div>
<ElCard v-if="states.isTraining === 2" class="model-area">
<template #header>
<div class="card-header">
模型
<ElButton
v-if="!showVisPanel"
size="small"
style="margin-left: 10px"
type="primary"
plain
@click="showVisPanel = true"
>
显示训练过程
</ElButton>
<ElButton
v-if="states.isTraining === 2"
size="small"
style="margin-left: 10px"
type="success"
plain
@click="exportModel"
>
模型导出至本地
</ElButton>
</div>
</template>
<ElRow class="model-item" style="flex-direction: column; align-items: flex-center">
<ElRow>
<ElCol :span="6" style="display: flex; align-items: center; text-align: right">
名称
</ElCol>
<ElCol :span="12">
<ElInput v-model="modelName" placeholder="请输入模型名称" />
</ElCol>
<ElCol :span="4" style="display: flex; align-items: center">
<ElButton
style="margin: auto 5px"
type="primary"
plain
size="small"
@click="saveModel"
>
保存
</ElButton>
</ElCol>
</ElRow>
</ElRow>
<ElRow class="model-item">
<b>输入</b>
</ElRow>
<ElRow class="model-item">
<div v-if="!uploadedImg" style="margin: auto">
上方拍摄内容
<ElUpload
:show-file-list="false"
accept="image/*"
:before-upload="handleUpload"
>
<ElButton size="small" type="success"> 切换为上传图片 </ElButton>
</ElUpload>
</div>
<div v-else style="margin: auto">
下方上传图片 <br />
<ElButton size="small" type="success" plain @click="switchToUpload">
切换为上方拍摄内容
</ElButton>
<ElUpload
:show-file-list="false"
accept="image/*"
:before-upload="handleUpload"
>
<ElButton size="small" type="success"> 重新上传一张 </ElButton>
</ElUpload>
<img
:src="uploadedImg"
alt="用户上传图片"
style="max-width: 100%; max-height: 150px; border-radius: 10px"
/>
</div>
</ElRow>
<ElRow class="model-item">
<b>输出</b>
</ElRow>
<ElRow
v-for="(item, idx) in classList"
:key="`${item.name}-${idx}`"
class="model-item"
>
<ElCol :span="6">
{{ item.name }}
</ElCol>
<ElCol :span="18">
<ElProgress
class="progress"
:text-inside="true"
:stroke-width="20"
:percentage="item.progress"
:color="progressColors[idx % progressColors.length]"
striped
:format="(p) => `${p}%`"
/>
</ElCol>
</ElRow>
</ElCard>
</div>
</template>
<style scoped>
.vis-left-panel-wrapper {
transition: all 0.3s;
position: fixed;
width: 400px;
z-index: 1000;
box-shadow: 0 0 20px #0001;
}
.vis-left-panel-inner-wrapper {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px #0001;
padding: 10px 10px 0 10px;
border: 1px solid #eee;
}
.vis-left-panel-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
cursor: move;
user-select: none;
font-weight: bold;
font-size: 16px;
}
.vis-left-panel {
min-height: 300px;
}
.model-area {
margin: 10px auto;
width: 85%;
max-width: 300px;
border-radius: 30px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 10px;
}
.model-item {
display: flex;
align-items: center;
margin-bottom: 15px;
}
</style>

View File

@@ -0,0 +1,98 @@
<script setup>
import { inject, ref, watch } from 'vue';
import { Warning } from '@element-plus/icons-vue';
import { ElButton, ElPopover, ElCard, ElText, ElIcon } from 'element-plus';
// import 'element-plus/theme-chalk/el-button.css';
// import 'element-plus/theme-chalk/el-popover.css';
// import 'element-plus/theme-chalk/el-card.css';
// import 'element-plus/theme-chalk/el-text.css';
// import 'element-plus/theme-chalk/el-icon.css';
const emit = defineEmits(['train'])
// 获取类别及其样本
const picList = inject('picList')
// 同步训练状态
const states = inject('states')
// 训练按钮是否可用
const isTrainButtonEnabled = ref(false)
const note_text = ref('待训练……')
// 检查训练按钮是否可用
function checkTrainButtonEnabled() {
// 只考虑未被禁用的类别
const enabledList = Array.isArray(picList.value)
? picList.value.filter(item => item.disabled === false)
: []
return (
states.value.isTraining !== 1
&& enabledList.length > 1
&& enabledList.every(item => Array.isArray(item.list) && item.list.length > 0)
)
}
// 监听类别列表和训练状态变化,更新训练按钮的可用性
watch(
[picList.value, () => states.value.isTraining],
() => {
isTrainButtonEnabled.value = checkTrainButtonEnabled()
if (states.value.isTraining === 0) {
note_text.value = '待训练……'
}
else if (states.value.isTraining === 1) {
note_text.value = '训练中……'
}
else if (states.value.isTraining === 2) {
note_text.value = '训练完成'
}
},
{ immediate: true },
)
// 训练模型
function handleTrain() {
if (isTrainButtonEnabled.value) {
emit('train')
states.value.isTraining = 1
}
}
</script>
<template>
<ElCard class="train-area">
<template #header>
<div class="card-header">
训练
</div>
</template>
<ElPopover class="box-item" placement="bottom" :disabled="isTrainButtonEnabled">
<ElIcon>
<Warning />
</ElIcon>
至少有两个类别且每个类别都需有图像样本
<template #reference>
<ElButton :disabled="!isTrainButtonEnabled" type="primary" style="max-width: 100%" @click="handleTrain">
训练模型
</ElButton>
</template>
</ElPopover>
<br>
<ElText>{{ note_text }}</ElText>
</ElCard>
</template>
<style scoped>
.train-area {
margin: 10px auto;
width: 85%;
max-width: 300px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 10px;
}
</style>

View File

@@ -0,0 +1,4 @@
import { useDark, useToggle } from '@vueuse/core'
export const isDark = useDark()
export const toggleDark = useToggle(isDark)

View File

@@ -0,0 +1 @@
export * from './dark'

View File

@@ -0,0 +1,8 @@
// only scss variables
$--colors: (
"primary": ("base": #589ef8,
),
);
@forward "element-plus/theme-chalk/src/dark/var.scss" with ($colors: $--colors);

View File

@@ -0,0 +1,33 @@
$--colors: (
'primary': ('base': green,
),
'success': ('base': #21ba45,
),
'warning': ('base': #f2711c,
),
'danger': ('base': #db2828,
),
'error': ('base': #db2828,
),
'info': ('base': #42b8dd,
),
);
// we can add this to custom namespace, default is 'el'
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with ($namespace: 'ep'
);
// You should use them in scss, because we calculate it by sass.
// comment next lines to use default color
@forward 'element-plus/theme-chalk/src/common/var.scss' with ( // do not use same name, it will override.
$colors: $--colors,
$button-padding-horizontal: ('default': 50px));
// if you want to import all
// @use "element-plus/theme-chalk/src/index.scss" as *;
// You can comment it to hide debug info.
// @debug $--colors;
// custom dark variables
@use './dark.scss';

View File

@@ -0,0 +1,28 @@
// import dark theme
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
// :root {
// --ep-color-primary: red;
// }
code {
border-radius: 2px;
padding: 2px 4px;
background-color: var(--ep-color-primary-light-9);
color: var(--ep-color-primary);
}
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: rgb(13, 148, 136);
opacity: 0.75;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}

View File

@@ -0,0 +1,58 @@
import { Registry } from 'mixly';
import * as tf from '@tensorflow/tfjs';
const modelsValueRegistry = new Registry();
const customFetch = function (path) {
let result = {
ok: false,
buffer: null,
json: function () {
const decoder = new TextDecoder('utf-8');
const jsonText = decoder.decode(this.buffer);
return JSON.parse(jsonText);
},
arrayBuffer: function () {
return this.buffer;
}
}
if (!modelsValueRegistry.hasKey(path)) {
return result;
}
result.ok = true;
result.buffer = modelsValueRegistry.getItem(path);
return result;
};
const tensorflow = {};
tensorflow.modelsValue = {};
tensorflow.loadGraphModel = async function (path) {
const model = await tf.loadGraphModel(path, {
fromTFHub: false,
fetchFunc: (...args) => {
return customFetch(args[0]);
}
});
return model;
};
tensorflow.loadLayersModel = async function (path) {
const model = await tf.loadLayersModel(path, {
fromTFHub: false,
fetchFunc: (...args) => {
return customFetch(args[0]);
}
});
return model;
};
tensorflow.setModelsValue = function (path, value) {
if (modelsValueRegistry.hasKey(path)) {
modelsValueRegistry.unregister(path);
}
modelsValueRegistry.register(path, value);
tensorflow.modelsValue[path] = value;
};
window.tensorflow = tensorflow;