diff --git a/manifest-beta.json b/manifest-beta.json index 234d5de..1f0098e 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -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", diff --git a/src/features/tikz-editor/components/canvas-grid.ts b/src/features/tikz-editor/components/canvas-grid.ts index 4f504f0..21c081e 100644 --- a/src/features/tikz-editor/components/canvas-grid.ts +++ b/src/features/tikz-editor/components/canvas-grid.ts @@ -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; diff --git a/src/features/tikz-editor/components/left-sidebar.ts b/src/features/tikz-editor/components/left-sidebar.ts index 0f94d0f..2567781 100644 --- a/src/features/tikz-editor/components/left-sidebar.ts +++ b/src/features/tikz-editor/components/left-sidebar.ts @@ -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 = () => { diff --git a/src/features/tikz-editor/components/right-sidebar.ts b/src/features/tikz-editor/components/right-sidebar.ts index d9c9601..0255ec5 100644 --- a/src/features/tikz-editor/components/right-sidebar.ts +++ b/src/features/tikz-editor/components/right-sidebar.ts @@ -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' }); diff --git a/src/features/tikz-editor/tikz-editor-modal.ts b/src/features/tikz-editor/tikz-editor-modal.ts index 3ec5a53..b2c9473 100644 --- a/src/features/tikz-editor/tikz-editor-modal.ts +++ b/src/features/tikz-editor/tikz-editor-modal.ts @@ -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(); diff --git a/src/features/tikz-editor/types.ts b/src/features/tikz-editor/types.ts index 1d40912..5523271 100644 --- a/src/features/tikz-editor/types.ts +++ b/src/features/tikz-editor/types.ts @@ -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; diff --git a/versions.json b/versions.json index 1d01c57..7a99f90 100644 --- a/versions.json +++ b/versions.json @@ -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" }