feat(tikz): enhance SVG parsing and element deletion; add duplication functionality

This commit is contained in:
JK 2026-06-14 00:36:16 +02:00
parent c5acdb0a36
commit 3f0ea8c86c
7 changed files with 165 additions and 19 deletions

View file

@ -1,7 +1,7 @@
{
"id": "texcore",
"name": "TeXcore",
"version": "0.0.3",
"version": "0.0.3.1",
"minAppVersion": "1.3.5",
"description": "Automatic equation numbering, Tex Diagrams rendering, and more to transforms your vault into a rich ecosystem for scientific drafting and study.",
"author": "Jovi Koikkara",

View file

@ -186,8 +186,11 @@ export class CanvasGrid {
: elem.style.color || 'var(--text-normal)';
const parser = new DOMParser();
const doc = parser.parseFromString(elem.svgMarkup, 'image/svg+xml');
svgWrap.appendChild(activeDocument.importNode(doc.documentElement, true));
const doc = parser.parseFromString(elem.svgMarkup, 'text/html');
const svgNode = doc.querySelector('svg');
if (svgNode) {
svgWrap.appendChild(activeDocument.importNode(svgNode, true));
}
innerG.appendChild(svgWrap);
group.appendChild(innerG);
@ -314,8 +317,11 @@ export class CanvasGrid {
svgWrap.setCssStyles({ color: 'var(--text-accent)' });
const parser = new DOMParser();
const doc = parser.parseFromString(activeTemplate.svgMarkup, 'image/svg+xml');
svgWrap.appendChild(activeDocument.importNode(doc.documentElement, true));
const doc = parser.parseFromString(activeTemplate.svgMarkup, 'text/html');
const svgNode = doc.querySelector('svg');
if (svgNode) {
svgWrap.appendChild(activeDocument.importNode(svgNode, true));
}
innerG.appendChild(svgWrap);
this.wiresOverlayEl.appendChild(innerG);
} else {
@ -339,13 +345,16 @@ export class CanvasGrid {
const top = Math.min(this.lassoStart.y, this.lassoCurrent.y);
const bottom = Math.max(this.lassoStart.y, this.lassoCurrent.y);
const isEraseMode = this.context.getActiveTool() === 'erase';
const lassoColor = isEraseMode ? 'var(--text-error)' : 'var(--interactive-accent)';
rect.setAttribute('x', left.toString());
rect.setAttribute('y', top.toString());
rect.setAttribute('width', (right - left).toString());
rect.setAttribute('height', (bottom - top).toString());
rect.setAttribute('fill', 'var(--interactive-accent)');
rect.setAttribute('fill', lassoColor);
rect.setAttribute('fill-opacity', '0.2');
rect.setAttribute('stroke', 'var(--interactive-accent)');
rect.setAttribute('stroke', lassoColor);
rect.setAttribute('stroke-width', '1');
rect.setAttribute('stroke-dasharray', '4,4');
this.wiresOverlayEl.appendChild(rect);
@ -403,8 +412,11 @@ export class CanvasGrid {
}
const parser = new DOMParser();
const doc = parser.parseFromString(svg, 'image/svg+xml');
visual.appendChild(activeDocument.importNode(doc.documentElement, true));
const doc = parser.parseFromString(svg, 'text/html');
const svgNode = doc.querySelector('svg');
if (svgNode) {
visual.appendChild(activeDocument.importNode(svgNode, true));
}
el.appendChild(visual);
if (elem.label) {
@ -591,8 +603,10 @@ export class CanvasGrid {
svgMarkup: template.svgMarkup,
tikzCommand: template.tikzCommand
});
} else if (activeTool === 'select') {
this.context.handleSelectVertices([]);
} else if (activeTool === 'select' || activeTool === 'erase') {
if (activeTool === 'select') {
this.context.handleSelectVertices([]);
}
this.lassoStart = coords;
this.lassoCurrent = coords;
@ -605,7 +619,11 @@ export class CanvasGrid {
const onMouseUp = () => {
if (this.lassoStart && this.lassoCurrent) {
this.applyLassoSelection();
if (activeTool === 'erase') {
this.applyLassoErase();
} else {
this.applyLassoSelection();
}
}
this.lassoStart = null;
this.lassoCurrent = null;
@ -619,6 +637,36 @@ export class CanvasGrid {
}
}
private applyLassoErase() {
if (!this.lassoStart || !this.lassoCurrent) return;
const left = Math.min(this.lassoStart.x, this.lassoCurrent.x);
const right = Math.max(this.lassoStart.x, this.lassoCurrent.x);
const top = Math.min(this.lassoStart.y, this.lassoCurrent.y);
const bottom = Math.max(this.lassoStart.y, this.lassoCurrent.y);
const isInside = (x: number, y: number) => {
return x >= left && x <= right && y >= top && y <= bottom;
};
const toDeleteIds: string[] = [];
this.context.getElements().forEach(elem => {
if (elem.type === 'wire' && elem.x2 !== undefined && elem.y2 !== undefined) {
if (isInside(elem.x, elem.y) || isInside(elem.x2, elem.y2)) {
toDeleteIds.push(elem.id);
}
} else {
if (isInside(elem.x, elem.y)) {
toDeleteIds.push(elem.id);
}
}
});
if (toDeleteIds.length > 0) {
this.context.handleDeleteElements(toDeleteIds);
}
}
private applyLassoSelection() {
if (!this.lassoStart || !this.lassoCurrent) return;

View file

@ -288,8 +288,11 @@ export class LeftSidebar {
const svgContainer = card.createDiv({ cls: 'svg-container' });
const compSvgParser = new DOMParser();
const compSvgDoc = compSvgParser.parseFromString(comp.svgMarkup, 'image/svg+xml');
svgContainer.appendChild(activeDocument.importNode(compSvgDoc.documentElement, true));
const compSvgDoc = compSvgParser.parseFromString(comp.svgMarkup, 'text/html');
const svgNode = compSvgDoc.querySelector('svg');
if (svgNode) {
svgContainer.appendChild(activeDocument.importNode(svgNode, true));
}
card.createEl('span', { cls: 'comp-label', text: comp.name });
card.onclick = () => {

View file

@ -415,7 +415,7 @@ export class RightSidebar {
deleteBtn.appendChild(activeDocument.importNode(deleteDoc.documentElement, true));
deleteBtn.createSpan({ text: ' Delete All Selected' });
deleteBtn.onclick = () => {
selectedElements.forEach(el => this.context.handleDeleteElement(el.id));
this.context.handleDeleteElements(selectedElements.map(el => el.id));
};
} else {
const emptyState = tabContent.createDiv({ cls: 'empty-state' });

View file

@ -37,9 +37,10 @@ export class TikzEditorModal extends Modal implements TikzEditorContext {
private activeTemplate: ComponentTemplate | null = null;
private selectedVertices: SelectedVertex[] = [];
private snapToGrid = true;
private halfGrid = false;
private halfGrid = true;
private pictureOptions = '';
private zoom = 1.0;
private copiedElements: EditorElement[] = [];
// Panning state
private isSpacePressed = false;
@ -306,14 +307,23 @@ export class TikzEditorModal extends Modal implements TikzEditorContext {
}
handleDeleteElement(id: string) {
this.elements = this.elements.filter(el => el.id !== id);
this.selectedVertices = this.selectedVertices.filter(v => v.elementId !== id);
this.handleDeleteElements([id]);
}
handleDeleteElements(ids: string[]) {
if (ids.length === 0) return;
this.elements = this.elements.filter(el => !ids.includes(el.id));
this.selectedVertices = this.selectedVertices.filter(v => !ids.includes(v.elementId));
this.saveHistoryState();
this.renderCanvas();
this.renderRightSidebar();
}
handleSelectTool(tool: 'select' | 'wire' | 'text' | 'erase') {
if (tool === 'erase' && this.selectedVertices.length > 0) {
const uniqueIds = Array.from(new Set(this.selectedVertices.map(v => v.elementId)));
this.handleDeleteElements(uniqueIds);
}
this.activeTool = tool;
this.activeTemplate = null;
this.selectedVertices = [];
@ -323,6 +333,42 @@ export class TikzEditorModal extends Modal implements TikzEditorContext {
this.renderRightSidebar();
}
duplicateElements(elementsToDuplicate: EditorElement[]) {
if (elementsToDuplicate.length === 0) return;
const offset = this.PX_PER_UNIT / 2; // Offset by half grid (40px)
const copies = elementsToDuplicate.map(el => {
const copy: EditorElement = JSON.parse(JSON.stringify(el));
copy.id = this.createId();
copy.x += offset;
copy.y += offset;
if (copy.x2 !== undefined) copy.x2 += offset;
if (copy.y2 !== undefined) copy.y2 += offset;
return copy;
});
this.elements = [...this.elements, ...copies];
// Select the newly duplicated elements
const newSelection: SelectedVertex[] = [];
copies.forEach(copy => {
if (copy.type === 'wire') {
newSelection.push({ elementId: copy.id, vertex: 'start' });
newSelection.push({ elementId: copy.id, vertex: 'end' });
} else {
newSelection.push({ elementId: copy.id, vertex: 'center' });
}
});
this.selectedVertices = newSelection;
this.activeTool = 'select';
this.activeTemplate = null;
this.saveHistoryState();
this.renderCanvas();
this.renderRightSidebar();
}
handleSelectTemplate(template: ComponentTemplate) {
this.activeTemplate = template;
this.selectedVertices = [];
@ -475,6 +521,53 @@ export class TikzEditorModal extends Modal implements TikzEditorContext {
return;
}
if (e.key === 'Delete' || e.key === 'Backspace') {
if (this.selectedVertices.length > 0) {
e.preventDefault();
const uniqueIds = Array.from(new Set(this.selectedVertices.map(v => v.elementId)));
this.handleDeleteElements(uniqueIds);
}
return;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') {
e.preventDefault();
const uniqueIds = Array.from(new Set(this.selectedVertices.map(v => v.elementId)));
const selectedElements = this.elements.filter(el => uniqueIds.includes(el.id));
if (selectedElements.length > 0) {
this.copiedElements = JSON.parse(JSON.stringify(selectedElements));
showNotice(`Copied ${selectedElements.length} component(s)`);
}
return;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') {
e.preventDefault();
if (this.copiedElements && this.copiedElements.length > 0) {
this.duplicateElements(this.copiedElements);
const offset = this.PX_PER_UNIT / 2;
this.copiedElements.forEach(el => {
el.x += offset;
el.y += offset;
if (el.x2 !== undefined) el.x2 += offset;
if (el.y2 !== undefined) el.y2 += offset;
});
showNotice(`Pasted ${this.copiedElements.length} component(s)`);
}
return;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'd') {
e.preventDefault();
const uniqueIds = Array.from(new Set(this.selectedVertices.map(v => v.elementId)));
const selectedElements = this.elements.filter(el => uniqueIds.includes(el.id));
if (selectedElements.length > 0) {
this.duplicateElements(selectedElements);
showNotice(`Duplicated ${selectedElements.length} component(s)`);
}
return;
}
const key = e.key.toLowerCase();
if (key === 'v' || key === 's') {
e.preventDefault();

View file

@ -104,6 +104,7 @@ export interface TikzEditorContext {
handleUpdateElement(updated: EditorElement, saveHistory?: boolean): void;
handleUpdateElements(updatedElements: EditorElement[], saveHistory?: boolean): void;
handleDeleteElement(id: string): void;
handleDeleteElements(ids: string[]): void;
handleSelectTool(tool: 'select' | 'wire' | 'text' | 'erase'): void;
handleSelectTemplate(template: ComponentTemplate): void;
generateTikzSource(): string;

View file

@ -1,4 +1,5 @@
{
"0.0.1": "1.3.5",
"0.0.3": "1.3.5"
"0.0.3": "1.3.5",
"0.0.3.1": "1.3.5"
}