Implement chopping

This commit is contained in:
DrHaid
2025-12-13 20:59:48 +01:00
parent 5a8dc3b057
commit 40872adafe
3 changed files with 77 additions and 7 deletions

5
package-lock.json generated
View File

@@ -54,6 +54,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -515,6 +516,7 @@
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -582,6 +584,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -1370,6 +1373,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -1617,6 +1621,7 @@
"integrity": "sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==", "integrity": "sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.14.47", "esbuild": "^0.14.47",
"postcss": "^8.4.14", "postcss": "^8.4.14",

View File

@@ -1,6 +1,7 @@
import { StickyNote, Image } from "@mirohq/websdk-types"; import { StickyNote, Image } from "@mirohq/websdk-types";
import { getStickiesToChopInTree } from "./todo-tree-helper";
export async function init() { export const init = async () => {
miro.board.ui.on('icon:click', async () => { miro.board.ui.on('icon:click', async () => {
await miro.board.ui.openPanel({ url: 'app.html' }); await miro.board.ui.openPanel({ url: 'app.html' });
}); });
@@ -42,19 +43,18 @@ const playAxeAnimation = async ({axe, stickyNote, onChopped}: AxeAnimationArgs)
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
axe.rotation = 90; axe.rotation = 90;
await axe.sync(); await axe.sync();
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 300));
axe.rotation = 0; axe.rotation = 0;
await axe.sync(); await axe.sync();
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 300));
} }
onChopped(); onChopped();
} }
async function postInfoNotification(message: string) { const postInfoNotification = async (message: string) =>
await miro.board.notifications.showInfo(message); await miro.board.notifications.showInfo(message);
}
const handleChoppingAction = async ({items}: {items:StickyNote[]}) => { const handleChoppingAction = async ({items}: {items:StickyNote[]}) => {
const axe = await findExistingAxe(); const axe = await findExistingAxe();
@@ -73,15 +73,22 @@ const handleChoppingAction = async ({items}: {items:StickyNote[]}) => {
playAxeAnimation({ playAxeAnimation({
axe, axe,
stickyNote: target, stickyNote: target,
onChopped: async () => await miro.board.remove(target) onChopped: () => removeTree(target)
}); });
}; };
const removeTree = async (root: StickyNote) => {
const stickiesToRemove = await getStickiesToChopInTree(root, ["light_green", "red"]);
for (const item of stickiesToRemove) {
await miro.board.remove(item);
}
};
export const findExistingAxe = async () => { export const findExistingAxe = async () => {
const items = await miro.board.get({ type: 'image' }); const items = await miro.board.get({ type: 'image' });
const axe = items.find(item => item.title === 'todo-tree-axe'); const axe = items.find(item => item.title === 'todo-tree-axe');
return axe; return axe;
}; };
init(); init();

58
src/todo-tree-helper.ts Normal file
View File

@@ -0,0 +1,58 @@
import { Connector, Item, StickyNote } from "@mirohq/websdk-types";
const getTypedMiroItem = async <T extends Item>(id: string, type: T['type']): Promise<T | undefined> => {
const item = await miro.board.getById(id);
return item?.type === type ? item as T : undefined;
};
interface ConnectorDirection {
parentId?: string;
childId?: string;
}
const getConnectorDirection = (connector: Connector): ConnectorDirection | undefined => {
const hasEndArrow = connector.style.endStrokeCap !== 'none';
const hasStartArrow = connector.style.startStrokeCap !== 'none';
if (hasEndArrow && !hasStartArrow) {
return { parentId: connector.start?.item, childId: connector.end?.item };
}
if (hasStartArrow && !hasEndArrow) {
return { parentId: connector.end?.item, childId: connector.start?.item };
}
return;
};
export const getStickiesToChopInTree = async (root: StickyNote, removableColors: string[]): Promise<StickyNote[]> => {
if (!removableColors.includes(root.style.fillColor)) return [];
const toRemove: StickyNote[] = [];
const visitedIds = new Set<string>();
const traverseTree = async (node: StickyNote) => {
if (visitedIds.has(node.id)){
// prevent infinite loops of circular arrows
return;
}
visitedIds.add(node.id);
for (const connectorId of node.connectorIds || []) {
const connector = await getTypedMiroItem<Connector>(connectorId, 'connector');
if (!connector) continue;
const direction = getConnectorDirection(connector);
if (!direction || !direction.childId || direction.parentId !== node.id) continue;
const childSticky = await getTypedMiroItem<StickyNote>(direction.childId, 'sticky_note');
if (childSticky) {
if (removableColors.includes(childSticky.style.fillColor)) {
toRemove.push(childSticky);
}
await traverseTree(childSticky);
}
}
};
await traverseTree(root);
return toRemove;
};