Skip to content

Commit

Permalink
adding support for excalidraw & canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
haouarihk committed Nov 27, 2023
1 parent 09d9242 commit 0515d84
Show file tree
Hide file tree
Showing 12 changed files with 580 additions and 47 deletions.
2 changes: 1 addition & 1 deletion manifest-beta.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "obsidian-textgenerator-plugin",
"name": "Text Generator",
"version": "0.5.16-beta",
"version": "0.5.17-beta",
"minAppVersion": "0.12.0",
"description": "Text generation using OpenAI",
"author": "Noureddine Haouari",
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@total-typescript/ts-reset": "^0.5.1",
"@types/debug": "^4.1.8",
"@types/lodash.get": "^4.4.7",
"@types/lodash.set": "^4.3.7",
Expand All @@ -29,14 +30,14 @@
"@typescript-eslint/parser": "^5.62.0",
"builtin-modules": "^3.3.0",
"daisyui": "^3.9.3",
"electron": "^27.0.4",
"esbuild": "0.13.12",
"esbuild-plugin-wasm": "^1.1.0",
"eslint-plugin-tailwindcss": "^3.13.0",
"obsidian": "^1.4.11",
"prettier": "^3.0.3",
"tslib": "2.6.2",
"typescript": "5.2.2",
"electron": "^27.0.4"
"typescript": "5.2.2"
},
"dependencies": {
"@codemirror/language": "^6.8.0",
Expand Down
94 changes: 94 additions & 0 deletions src/content-manager/canvas.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { App, ItemView } from 'obsidian'
import type { AllCanvasNodeData, CanvasData } from 'obsidian/canvas'
export { AllCanvasNodeData } from 'obsidian/canvas'
export interface CanvasNode {
id: string
app: App
canvas: Canvas
child: Partial<CanvasNode>
color: string
containerEl: HTMLElement
containerBlockerEl: HTMLElement
contentEl: HTMLElement
destroyted: boolean
height: number
initialized: boolean
isContentMounted: boolean
isEditing: boolean
nodeEl: HTMLElement
placeholderEl: HTMLElement
renderedZIndex: number
resizeDirty: boolean
text: string
unknownData: Record<string, string>
width: number
x: number
y: number
zIndex: number
convertToFile(): Promise<void>
focus(): void
getData(): AllCanvasNodeData
initialize(): void
moveAndResize(options: MoveAndResizeOptions): void
render(): void
setData(data: Partial<AllCanvasNodeData>): void
setText(text: string): Promise<void>
showMenu(): void
startEditing(): void
}

export interface MoveAndResizeOptions {
x?: number
y?: number
width?: number
height?: number
}

export interface CanvasEdge {
from: {
node: CanvasNode
}
to: {
node: CanvasNode
}
}

export interface Canvas {
edges: CanvasEdge[]
selection: Set<CanvasNode>
nodes: Map<string | undefined, CanvasNode>
wrapperEl: HTMLElement | null
addNode(node: CanvasNode): void
createTextNode(options: CreateNodeOptions): CanvasNode
deselectAll(): void
getData(): CanvasData
getEdgesForNode(node: CanvasNode): CanvasEdge[]
importData(data: { nodes: object[]; edges: object[] }): void
removeNode(node: CanvasNode): void
requestFrame(): Promise<void>
requestSave(): Promise<void>
selectOnly(node: CanvasNode, startEditing: boolean): void
}

export interface CreateNodeOptions {
text: string
pos?: { x: number; y: number }
position?: 'left' | 'right' | 'top' | 'bottom'
size?: { height?: number; width?: number }
focus?: boolean
}


export interface CanvasEdgeIntermediate {
fromOrTo: string
side: string
node: CanvasElement
}

interface CanvasElement {
id: string
}

export type CanvasView = ItemView & {
canvas: Canvas
}
257 changes: 257 additions & 0 deletions src/content-manager/canvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { ContentManager, Mode } from "./types";




type Item = CanvasNode & { rawText?: string } | undefined;
export default class CanvasManager implements ContentManager {
canvas: Canvas;

constructor(canvas: Canvas) {
this.canvas = canvas;
}

protected async updateNode(id: string, nodeData?: Partial<AllCanvasNodeData>) {
await this.canvas.requestFrame()
const node = this.canvas.nodes.get(id);

if (!node) return;

for (const prop in nodeData) {
if (Object.prototype.hasOwnProperty.call(nodeData, prop)) {
// @ts-ignore
node[prop] = nodeData[prop] as any;
}
}
await this.canvas.requestFrame()
await this.canvas.requestSave()
}


protected createNewNode(parentNode: CanvasNode | undefined, nodeOptions: CreateNodeOptions, nodeData?: Partial<AllCanvasNodeData>) {
if (!parentNode) throw new Error('parentNode wasn\'t detected');

const { text, size } = nodeOptions;
const width = size?.width || Math.max(MIN_WIDTH, parentNode.width);
const height = size?.height || Math.max(MIN_HEIGHT, parentNode && calculateNoteHeight({ text, width: parentNode.width, parentHeight: parentNode.height }));

const siblings = parentNode && this.canvas.getEdgesForNode(parentNode).filter((n) => n.from.node.id === parentNode.id).map((e) => e.to.node);

const farLeft = parentNode.y - parentNode.width * 5;
const siblingsRight = siblings?.reduce((right, sib) => Math.max(right, sib.x + sib.width), farLeft);
const priorSibling = siblings?.[siblings.length - 1];

const x = siblingsRight != null ? siblingsRight + NEW_NOTE_MARGIN : parentNode.x;
const y = (priorSibling ? priorSibling.y : parentNode.y + parentNode.height + NEW_NOTE_MARGIN) + height * 0.5;

const newNode = this.canvas.createTextNode({
pos: { x, y },
position: 'left',
size: { height, width },
text,
focus: false,
});

if (nodeData) newNode.setData(nodeData);

this.canvas.deselectAll();
this.canvas.addNode(newNode);

this.addConnection(generateRandomHexString(16), { fromOrTo: 'from', side: 'bottom', node: parentNode }, { fromOrTo: 'to', side: 'top', node: newNode });

return newNode;
}

protected addConnection(connectionID: string, fromConnection: CanvasEdgeIntermediate, toConnection: CanvasEdgeIntermediate) {
const data = this.canvas.getData();
if (!data) return;

this.canvas.importData({
edges: [...data.edges, { id: connectionID, fromNode: fromConnection.node.id, fromSide: fromConnection.side, toNode: toConnection.node.id, toSide: toConnection.side }],
nodes: data.nodes,
});

this.canvas.requestFrame();
}

protected async getTextSelectedItems(): Promise<Item[]> {
const extractedText = Array.from(this.canvas.selection.values())
.map(async (element: any) => {
if (element.file)
element.rawText = await app.vault.cachedRead(element.file)
else element.rawText = element.text
return element;
});

return Promise.all(extractedText);
}

protected getParentOfNode(id: string) {

}

async getSelections(): Promise<string[]> {
return (await this.getTextSelectedItems()).map(e => e?.rawText).filter(Boolean) || [];
}

async getValue(): Promise<string> {
return (await Promise.all(Array.from(this.canvas.nodes).map(async (element: any) => {
if (element.file)
element.rawText = await app.vault.cachedRead(element.file)
else element.rawText = element.text
return element;
}))).join("\n")
}

async getSelection(): Promise<string> {
return (await this.getSelections())[0]
}

async getTgSelection(tgSelectionLimiter?: string) {
return (await this.getSelections()).join("\n");
}

selectTgSelection(tgSelectionLimiter?: string) {
return;
}

getLastLetterBeforeCursor(): string {
return ""
}

async getCursor(dir?: "from" | "to" | undefined): Promise<Item> {
// get first or last item
const items = await this.getTextSelectedItems();
return items[dir == "from" ? 0 : items.length - 1];
}

setCursor(pos: Item): void {
// this.ea.viewZoomToElements([pos], [pos])
return;
}

async insertText(text: string, parent?: Item, mode?: Mode): Promise<Item> {
const items = await this.getTextSelectedItems();
let selectedItem = parent || await this.getCursor();

await this.canvas.requestFrame();

switch (mode) {
case "replace":
if (!selectedItem) throw "no item to replace";
// remove selected items(text)
for (const item of [...items.filter(i => i?.id != selectedItem?.id)]) {
if (item)
this.canvas.removeNode(item)
}

await this.canvas.requestFrame();
await selectedItem.setText(text);

break;

case "insert":
selectedItem = this.createNewNode(parent, {
text,
position: "bottom"
},
{
color: "6",
chat_role: 'assistant'
}
)

if (parent)
selectedItem.moveAndResize({
height: calculateNoteHeight({
parentHeight: parent.height,
width: selectedItem.width,
text
}),
width: selectedItem.width,
x: parent.x,
y: parent.y + parent.height + NEW_NOTE_MARGIN
})
break;

case "stream":
if (!selectedItem?.id) throw "no item to update";
await this.canvas.requestFrame();
await selectedItem.setText(selectedItem.getData().text + text);

selectedItem.moveAndResize({
height: selectedItem?.height ? calculateNoteHeight({
parentHeight: selectedItem?.height,
width: selectedItem.width,
text
}) : undefined,
width: selectedItem.width,
x: selectedItem.x,
y: selectedItem.y
})

break;
}
return selectedItem
}

async insertStream(pos: Item, mode?: "insert" | "replace"): Promise<{
insert(data: string): void,
end(): void,
replaceAllWith(newData: string): void
}> {
const items = await this.getTextSelectedItems();
let selectedItem = items[items.length - 1]
let cursor: any;

let postingContent = "";
let stillPlaying = true;
let firstTime = true;
let previewsLevel = -1;

const writerTimer: any = setInterval(async () => {
if (!stillPlaying) return clearInterval(writerTimer);
const posting = postingContent;
if (!posting) return;

const postinglines = posting.split("\n");

for (const postingLine of postinglines) {
if (firstTime) cursor = await this.insertText(postingLine, pos, mode) || cursor;
else cursor = await this.insertText(postingLine, cursor, "stream");

postingContent = postingContent.substring(postingLine.length);
firstTime = false;
}

// if (!this.plugin.settings.freeCursorOnStreaming)
// this.setCursor(cursor);
}, 200);

return {
insert(newInsertData: string) {
postingContent += newInsertData
},
end() {
stillPlaying = false;
},

replaceAllWith: async (allText) => {
await this.insertText(allText, cursor || selectedItem, "replace");
}
}
}
}

import type { Canvas, CanvasNode, CreateNodeOptions, AllCanvasNodeData, CanvasEdgeIntermediate } from "./canvas.d";

const MIN_WIDTH = 200;
const PX_PER_CHAR = 8;
const PX_PER_LINE = 100;
const TEXT_PADDING_HEIGHT = 20;
const NEW_NOTE_MARGIN = 60;
const MIN_HEIGHT = 60;

const calculateNoteHeight = ({ parentHeight, text, width }: { parentHeight: number; width?: number; text: string }) => Math.max(parentHeight, Math.round(TEXT_PADDING_HEIGHT + (PX_PER_LINE * text.length) / ((width || MIN_WIDTH) / PX_PER_CHAR)));

const generateRandomHexString = (len: number) => Array.from({ length: len }, () => ((16 * Math.random()) | 0).toString(16)).join('');
Loading

0 comments on commit 0515d84

Please sign in to comment.