Skip to content

Commit 7637986

Browse files
chore: bump version to v3.7.5 and update changelog
1 parent be03e45 commit 7637986

17 files changed

Lines changed: 186 additions & 141 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@
33
All notable code changes to **Markdown Viewer** are documented here.
44
Non-code commits (documentation, planning, README-only updates) are excluded.
55

6+
## v3.7.5
7+
8+
- **Description:** Delivered key interactive visualization engines, export improvements, notation support, and syntax/contrast adjustments.
9+
- **Interactive Map & 3D Renderers:** Added interactive parsing and rendering engines for GeoJSON, TopoJSON, and STL (ASCII/Binary) files. Built advanced STL viewport controls (grid helper, surface angle flat shading, reset action, and modal zoom) aligned to GitHub's camera perspective.
10+
- **PNG Image Export:** Added support for exporting previews to PNG images with progress feedback, rendering on a solid white background instead of transparency to preserve contrast.
11+
- **ABC Music Notation:** Implemented client-side ABC music notation stylesheet rendering and offline desktop app support.
12+
- **Syntax Highlighting & Math:** Fixed PowerShell syntax highlighting conflicts and fenced LaTeX math block parser rendering.
13+
- **Tab-Based Export Filename:** Configured exports to automatically inherit the active tab's title instead of a generic 'document' filename (fixes #178).
14+
- **Contrast Remediation:** Enhanced highlighted search match visibility and contrast inside the editor under dark mode.
15+
- **Date:** 2026-06-18
16+
- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/commit/be03e4510d79a4ae47df51240c028e0a2fc4ba05
17+
18+
---
19+
620
## v3.7.4
721

822
- **Description:** Delivered substantial feature additions and reliability improvements, including language expansion, desktop app startup fix, export centering, re-engineered PDF page breaking, and advanced Find & Replace features.

desktop-app/neutralino.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
33
"applicationId": "com.markdownviewer.desktop",
4-
"version": "3.7.4",
4+
"version": "3.7.5",
55
"defaultMode": "window",
66
"port": 0,
77
"documentRoot": "/resources/",

desktop-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "markdown-viewer-desktop",
3-
"version": "3.7.4",
3+
"version": "3.7.5",
44
"private": true,
55
"description": "A premium client-side GitHub-style Markdown editor and live preview tool for desktop, featuring math rendering, diagrams, syntax highlighting, and PDF/HTML exports.",
66
"scripts": {

desktop-app/resources/index.html

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -890,20 +890,30 @@ <h3 class="modal-section-title">Open-source credits</h3>
890890
<div class="stl-modal-viewer" id="stl-modal-viewer"></div>
891891
<div class="stl-modal-controls">
892892
<button class="stl-toolbar-btn" id="stl-modal-btn-solid" title="Solid Mode">
893-
<i class="bi bi-circle-fill me-1"></i>Solid
893+
<i class="bi bi-circle-fill"></i> Solid
894894
</button>
895895
<button class="stl-toolbar-btn" id="stl-modal-btn-angle" title="Surface Angle Mode">
896-
<i class="bi bi-circle-half me-1"></i>Surface Angle
896+
<i class="bi bi-circle-half"></i> Surface Angle
897897
</button>
898898
<button class="stl-toolbar-btn" id="stl-modal-btn-wireframe" title="Wireframe Mode">
899-
<i class="bi bi-grid-3x3 me-1"></i>Wireframe
899+
<i class="bi bi-grid-3x3"></i> Wireframe
900+
</button>
901+
<div class="stl-divider"></div>
902+
<button class="stl-toolbar-btn" id="stl-modal-btn-zoom-in" title="Zoom in">
903+
<i class="bi bi-zoom-in"></i> Zoom In
904+
</button>
905+
<button class="stl-toolbar-btn" id="stl-modal-btn-zoom-out" title="Zoom out">
906+
<i class="bi bi-zoom-out"></i> Zoom Out
907+
</button>
908+
<button class="stl-toolbar-btn" id="stl-modal-btn-zoom-reset" title="Reset view">
909+
<i class="bi bi-arrows-angle-contract"></i> Reset
900910
</button>
901911
<div class="stl-divider"></div>
902912
<button class="stl-toolbar-btn" id="stl-modal-btn-copy" title="Copy Image">
903-
<i class="bi bi-clipboard-image me-1"></i>Copy
913+
<i class="bi bi-clipboard-image"></i> Copy
904914
</button>
905915
<button class="stl-toolbar-btn" id="stl-modal-btn-png" title="Download PNG">
906-
<i class="bi bi-file-image me-1"></i>PNG
916+
<i class="bi bi-file-image"></i> PNG
907917
</button>
908918
</div>
909919
</div>

desktop-app/resources/js/script.js

Lines changed: 106 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ document.addEventListener("DOMContentLoaded", function () {
8989

9090
// View Mode State - Story 1.1
9191
let currentViewMode = 'split'; // 'editor', 'split', or 'preview'
92-
const APP_VERSION = '3.7.4';
92+
const APP_VERSION = '3.7.5';
9393
let activeModal = null;
9494
let lastFocusedElement = null;
9595
let isFindModalOpen = false;
@@ -2359,6 +2359,9 @@ document.addEventListener("DOMContentLoaded", function () {
23592359
const loader = new THREE.STLLoader();
23602360
const geometry = loader.parse(new TextEncoder().encode(code).buffer);
23612361

2362+
// Rotate geometry from Z-up (CAD/STL standard) to Y-up (Three.js standard)
2363+
geometry.rotateX(-Math.PI / 2);
2364+
23622365
geometry.computeBoundingBox();
23632366
geometry.computeVertexNormals();
23642367

@@ -2372,10 +2375,10 @@ document.addEventListener("DOMContentLoaded", function () {
23722375

23732376
// Add grid helper (underneath the model, matching the theme)
23742377
const currentTheme = document.documentElement.getAttribute("data-theme") || 'light';
2375-
const gridColorCenter = currentTheme === 'dark' ? 0x555555 : 0xbbbbbb;
2376-
const gridColor = currentTheme === 'dark' ? 0x2d3139 : 0xe5e5e5;
2378+
const gridColorCenter = currentTheme === 'dark' ? 0x888888 : 0xaaaaaa;
2379+
const gridColor = currentTheme === 'dark' ? 0x333742 : 0xcccccc;
23772380

2378-
const gridHelper = new THREE.GridHelper(maxDim * 3, 20, gridColorCenter, gridColor);
2381+
const gridHelper = new THREE.GridHelper(maxDim * 15, 30, gridColorCenter, gridColor);
23792382
gridHelper.position.y = -size.y / 2; // Position directly under model
23802383
scene.add(gridHelper);
23812384

@@ -2398,12 +2401,16 @@ document.addEventListener("DOMContentLoaded", function () {
23982401
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
23992402
cameraZ *= 1.4;
24002403

2401-
camera.position.set(maxDim * 0.9, maxDim * 0.9, cameraZ);
2404+
// Set initial camera position symmetrically (X = 0) with a slight top-down angle
2405+
camera.position.set(0, maxDim * 0.9, cameraZ * 1.4);
24022406
camera.lookAt(0, 0, 0);
24032407
controls.target.set(0, 0, 0);
24042408

2405-
camera.far = maxDim * 10;
2409+
camera.far = maxDim * 50;
24062410
camera.updateProjectionMatrix();
2411+
2412+
const initialPosition = camera.position.clone();
2413+
const initialTarget = controls.target.clone();
24072414

24082415
let animationFrameId;
24092416
const animate = function() {
@@ -2427,6 +2434,8 @@ document.addEventListener("DOMContentLoaded", function () {
24272434
normalMaterial,
24282435
mesh,
24292436
gridHelper,
2437+
initialPosition,
2438+
initialTarget,
24302439
animationFrameId: null
24312440
};
24322441

@@ -2436,6 +2445,52 @@ document.addEventListener("DOMContentLoaded", function () {
24362445
return view;
24372446
}
24382447

2448+
function exportStlImage(view, isDownload, button, originalText) {
2449+
if (!view || !view.renderer || !view.scene || !view.camera) return;
2450+
button.innerHTML = '<i class="bi bi-hourglass-split"></i>';
2451+
2452+
// Force a render pass to ensure the canvas buffer is loaded with the current frame
2453+
view.renderer.render(view.scene, view.camera);
2454+
2455+
const webglCanvas = view.renderer.domElement;
2456+
2457+
// Create temporary 2D canvas of the same dimensions
2458+
const tempCanvas = document.createElement('canvas');
2459+
tempCanvas.width = webglCanvas.width;
2460+
tempCanvas.height = webglCanvas.height;
2461+
2462+
const ctx = tempCanvas.getContext('2d');
2463+
// Draw solid white background
2464+
ctx.fillStyle = '#ffffff';
2465+
ctx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
2466+
// Overlay the WebGL canvas content
2467+
ctx.drawImage(webglCanvas, 0, 0);
2468+
2469+
if (isDownload) {
2470+
const dataUrl = tempCanvas.toDataURL('image/png');
2471+
const a = document.createElement('a');
2472+
a.href = dataUrl;
2473+
a.download = `model-${Date.now()}.png`;
2474+
a.click();
2475+
button.innerHTML = '<i class="bi bi-check-lg"></i>';
2476+
setTimeout(() => { button.innerHTML = originalText; }, 1500);
2477+
} else {
2478+
// Copy to clipboard
2479+
tempCanvas.toBlob(async blob => {
2480+
try {
2481+
await navigator.clipboard.write([
2482+
new ClipboardItem({ 'image/png': blob })
2483+
]);
2484+
button.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
2485+
} catch (err) {
2486+
console.error(err);
2487+
button.innerHTML = '<i class="bi bi-x-lg"></i>';
2488+
}
2489+
setTimeout(() => { button.innerHTML = originalText; }, 1500);
2490+
}, 'image/png');
2491+
}
2492+
}
2493+
24392494
function addStlToolbar(container, node, code, view) {
24402495
if (!container) return;
24412496

@@ -2450,19 +2505,19 @@ document.addEventListener("DOMContentLoaded", function () {
24502505
btnSolid.type = 'button';
24512506
btnSolid.className = 'stl-toolbar-btn active';
24522507
btnSolid.setAttribute('data-mode', 'solid');
2453-
btnSolid.textContent = 'Solid';
2508+
btnSolid.innerHTML = '<i class="bi bi-circle-fill"></i> Solid';
24542509

24552510
const btnAngle = document.createElement('button');
24562511
btnAngle.type = 'button';
24572512
btnAngle.className = 'stl-toolbar-btn';
24582513
btnAngle.setAttribute('data-mode', 'angle');
2459-
btnAngle.textContent = 'Surface Angle';
2514+
btnAngle.innerHTML = '<i class="bi bi-circle-half"></i> Surface Angle';
24602515

24612516
const btnWireframe = document.createElement('button');
24622517
btnWireframe.type = 'button';
24632518
btnWireframe.className = 'stl-toolbar-btn';
24642519
btnWireframe.setAttribute('data-mode', 'wireframe');
2465-
btnWireframe.textContent = 'Wireframe';
2520+
btnWireframe.innerHTML = '<i class="bi bi-grid-3x3"></i> Wireframe';
24662521

24672522
const btnZoom = document.createElement('button');
24682523
btnZoom.type = 'button';
@@ -2521,34 +2576,11 @@ document.addEventListener("DOMContentLoaded", function () {
25212576
});
25222577

25232578
btnCopy.addEventListener('click', () => {
2524-
const originalText = btnCopy.innerHTML;
2525-
btnCopy.innerHTML = '<i class="bi bi-hourglass-split"></i>';
2526-
view.renderer.render(view.scene, view.camera);
2527-
view.renderer.domElement.toBlob(async blob => {
2528-
try {
2529-
await navigator.clipboard.write([
2530-
new ClipboardItem({ 'image/png': blob })
2531-
]);
2532-
btnCopy.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
2533-
} catch (err) {
2534-
console.error(err);
2535-
btnCopy.innerHTML = '<i class="bi bi-x-lg"></i>';
2536-
}
2537-
setTimeout(() => { btnCopy.innerHTML = originalText; }, 1500);
2538-
}, 'image/png');
2579+
exportStlImage(view, false, btnCopy, btnCopy.innerHTML);
25392580
});
25402581

25412582
btnPng.addEventListener('click', () => {
2542-
const originalText = btnPng.innerHTML;
2543-
btnPng.innerHTML = '<i class="bi bi-hourglass-split"></i>';
2544-
view.renderer.render(view.scene, view.camera);
2545-
const dataUrl = view.renderer.domElement.toDataURL('image/png');
2546-
const a = document.createElement('a');
2547-
a.href = dataUrl;
2548-
a.download = `model-${Date.now()}.png`;
2549-
a.click();
2550-
btnPng.innerHTML = '<i class="bi bi-check-lg"></i>';
2551-
setTimeout(() => { btnPng.innerHTML = originalText; }, 1500);
2583+
exportStlImage(view, true, btnPng, btnPng.innerHTML);
25522584
});
25532585
}
25542586

@@ -10063,12 +10095,46 @@ document.addEventListener("DOMContentLoaded", function () {
1006310095
URL.revokeObjectURL(url);
1006410096
});
1006510097

10098+
function zoomStl(view, factor) {
10099+
if (!view || !view.camera || !view.controls) return;
10100+
const camera = view.camera;
10101+
const controls = view.controls;
10102+
10103+
const target = controls.target;
10104+
const position = camera.position;
10105+
const offset = new THREE.Vector3().subVectors(position, target);
10106+
10107+
offset.multiplyScalar(factor);
10108+
10109+
position.copy(target).add(offset);
10110+
controls.update();
10111+
}
10112+
10113+
function resetStlView(view) {
10114+
if (!view || !view.camera || !view.controls || !view.initialPosition || !view.initialTarget) return;
10115+
view.camera.position.copy(view.initialPosition);
10116+
view.controls.target.copy(view.initialTarget);
10117+
view.controls.update();
10118+
}
10119+
1006610120
// STL Zoom Modal Event Listeners
1006710121
document.getElementById('stl-zoom-modal-close').addEventListener('click', closeStlZoomModal);
1006810122
document.getElementById('stl-zoom-modal').addEventListener('click', function(e) {
1006910123
if (e.target === this) closeStlZoomModal();
1007010124
});
1007110125

10126+
document.getElementById('stl-modal-btn-zoom-in').addEventListener('click', () => {
10127+
if (activeModalStlView) zoomStl(activeModalStlView, 0.8);
10128+
});
10129+
10130+
document.getElementById('stl-modal-btn-zoom-out').addEventListener('click', () => {
10131+
if (activeModalStlView) zoomStl(activeModalStlView, 1.25);
10132+
});
10133+
10134+
document.getElementById('stl-modal-btn-zoom-reset').addEventListener('click', () => {
10135+
if (activeModalStlView) resetStlView(activeModalStlView);
10136+
});
10137+
1007210138
const modalBtnSolid = document.getElementById('stl-modal-btn-solid');
1007310139
const modalBtnAngle = document.getElementById('stl-modal-btn-angle');
1007410140
const modalBtnWireframe = document.getElementById('stl-modal-btn-wireframe');
@@ -10102,38 +10168,15 @@ document.addEventListener("DOMContentLoaded", function () {
1010210168
});
1010310169

1010410170
document.getElementById('stl-modal-btn-copy').addEventListener('click', function() {
10105-
if (!activeModalStlView) return;
10106-
const btn = this;
10107-
const originalText = btn.innerHTML;
10108-
btn.innerHTML = '<i class="bi bi-hourglass-split"></i>';
10109-
activeModalStlView.renderer.render(activeModalStlView.scene, activeModalStlView.camera);
10110-
activeModalStlView.renderer.domElement.toBlob(async blob => {
10111-
try {
10112-
await navigator.clipboard.write([
10113-
new ClipboardItem({ 'image/png': blob })
10114-
]);
10115-
btn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
10116-
} catch (err) {
10117-
console.error(err);
10118-
btn.innerHTML = '<i class="bi bi-x-lg"></i>';
10119-
}
10120-
setTimeout(() => { btn.innerHTML = originalText; }, 1500);
10121-
}, 'image/png');
10171+
if (activeModalStlView) {
10172+
exportStlImage(activeModalStlView, false, this, this.innerHTML);
10173+
}
1012210174
});
1012310175

1012410176
document.getElementById('stl-modal-btn-png').addEventListener('click', function() {
10125-
if (!activeModalStlView) return;
10126-
const btn = this;
10127-
const originalText = btn.innerHTML;
10128-
btn.innerHTML = '<i class="bi bi-hourglass-split"></i>';
10129-
activeModalStlView.renderer.render(activeModalStlView.scene, activeModalStlView.camera);
10130-
const dataUrl = activeModalStlView.renderer.domElement.toDataURL('image/png');
10131-
const a = document.createElement('a');
10132-
a.href = dataUrl;
10133-
a.download = `model-${Date.now()}.png`;
10134-
a.click();
10135-
btn.innerHTML = '<i class="bi bi-check-lg"></i>';
10136-
setTimeout(() => { btn.innerHTML = originalText; }, 1500);
10177+
if (activeModalStlView) {
10178+
exportStlImage(activeModalStlView, true, this, this.innerHTML);
10179+
}
1013710180
});
1013810181

1013910182
/**

0 commit comments

Comments
 (0)