@@ -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