mirror of
https://github.com/youfoundjk/TeXcore.git
synced 2026-07-22 07:33:31 +00:00
feat(tikz): enhance SVG parsing and element deletion; add duplication functionality
This commit is contained in:
parent
c5acdb0a36
commit
3f0ea8c86c
7 changed files with 165 additions and 19 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue