diff --git a/CHANGELOG.md b/CHANGELOG.md index 5192e983..b89d55f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,65 +1,10 @@ # Changelog -## v1.2.45 (2026-03-23) -- Add project switcher to left sidebar +## v1.2.26 (2026-03-23) +- Fix 7 bugs found in review: stale camera, material leak, dead code, URL leaks -## v1.2.44 (2026-03-23) -- Fix duplicate models on tab switch; improve clash panel UX - -## v1.2.43 (2026-03-23) -- Fix hooks-after-return in SidePanel causing blackout on tab open - -## v1.2.42 (2026-03-23) -- Fix view cube 3D depth, orientation stability, IFC/2D buttons, resizable panel - -## v1.2.41 (2026-03-23) -- Replace folder icon in left rail with IFC and 2D label buttons - -## v1.2.40 (2026-03-23) -- Split upload button into IFC and 2D buttons - -## v1.2.39 (2026-03-23) -- Fix tooltips rendering behind the 3D viewer - -## v1.2.38 (2026-03-23) -- Fix viewcube fly-to and improve visual style - -## v1.2.37 (2026-03-23) -- Split icon rail into left (utility) and right (nav tabs) rails - -## v1.2.36 (2026-03-23) -- Replace text labels with animated hover tooltips on sidebar icons - -## v1.2.35 (2026-03-23) -- Add folder-with-plus SVG icon to file upload button and empty state - -## v1.2.34 (2026-03-23) -- Redesign UI: vertical icon sidebar + slide-out panel, maximized viewer - -## v1.2.33 (2026-03-23) -- Tone down LLM error banner; improve welcome message - -## v1.2.32 (2026-03-23) -- Add animated loading border + status indicator to NL panel - -## v1.2.31 (2026-03-23) -- Significantly broaden NL panel regex parsing - -## v1.2.30 (2026-03-23) -- Fix LLM model load: q4f16→q4, add error details and retry button - -## v1.2.29 (2026-03-23) -- Add in-browser LLM to NL command panel (Qwen2.5-0.5B via Transformers.js) - -## v1.2.27 (2026-03-22) -- Restore element ghost effect on viewer click - -## v1.2.26 (2026-03-22) -- Restore element ghost effect on viewer click - -## v1.2.25 (2026-03-22) -- Add natural language command panel (ClashControl AI) -- Refocus NL panel on clash setup, Q&A and workflow — not spatial interactions +## v1.2.25 (2026-03-23) +- Simplify: remove dRef, fix img cache pattern, deduplicate hasProjectData, fix unmount leak ## v1.2.24 (2026-03-22) - Fix 8 code review issues: CSP, SRI, state mutation, stale closures, dead code diff --git a/README.md b/README.md index 0d16a646..ba043d48 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ClashControl -> Version: **v1.2.45** (2026-03-23) +> Version: **v1.2.26** (2026-03-23) **Free, open-source IFC clash detection — right in your browser.** diff --git a/index.html b/index.html index ef6b47e9..0d9e113d 100644 --- a/index.html +++ b/index.html @@ -178,7 +178,7 @@ function startApp() { // ── Version (auto-updated by pre-commit hook) ── - var CC_VERSION = {v:'1.2.45',hash:'85465b6',date:'2026-03-23T10:52:36Z'}; + var CC_VERSION = {v:'1.2.26',hash:'f0e9195',date:'2026-03-23T11:10:00Z'}; // Check CDN deps loaded var need = []; @@ -307,7 +307,6 @@ case 'SET_PROJECT_LIST': return Object.assign({},s,{projectList:a.v}); case 'LOAD_PROJECT_STATE': return Object.assign({},s,a.data); case 'CLASH_GROUP_BY': return Object.assign({},s,{clashGroupBy:a.v}); - case 'TOGGLE_PROJECTION': return Object.assign({},s,{projection:s.projection==='ortho'?'perspective':'ortho'}); // 2D Underlays case 'ADD_UNDERLAY': return Object.assign({},s,{underlays:s.underlays.concat([a.v]),activeUnderlay:a.v.id}); case 'DEL_UNDERLAY': return Object.assign({},s,{underlays:s.underlays.filter(function(u){return u.id!==a.id}),activeUnderlay:s.activeUnderlay===a.id?null:s.activeUnderlay}); @@ -974,6 +973,7 @@ a.href = URL.createObjectURL(blob); a.download = 'project-'+new Date().toISOString().slice(0,10)+'.json'; a.click(); + URL.revokeObjectURL(a.href); } function loadProject(d) { @@ -1191,6 +1191,7 @@ a.href = URL.createObjectURL(blob); a.download = 'clashcontrol-'+versionId+'-'+new Date().toISOString().slice(0,10)+'.bcf'; a.click(); + URL.revokeObjectURL(a.href); }); } @@ -1283,7 +1284,6 @@ },{passive:false}); var orbit = {target:target, sync:sync, apply:apply, sph:sph, camera:camera}; // Override apply to use orbit.camera (supports projection switching) - var _origApply = apply; orbit.apply = function() { camera = orbit.camera; // keep local camera in sync sph.phi = Math.max(0.05, Math.min(Math.PI-0.05, sph.phi)); @@ -1570,6 +1570,7 @@ // Vertical ring for 3D visibility var ring3 = new THREE.Mesh(ringGeo2, ringMat2.clone()); ring3.position.set(point[0], point[1], point[2]); + ring3.rotation.y = Math.PI / 2; grp.add(ring3); scene.add(grp); _activeClashMarker = grp; @@ -1777,7 +1778,7 @@ var rect = renderer.domElement.getBoundingClientRect(); hoverMouse.x = ((e.clientX-rect.left)/rect.width)*2-1; hoverMouse.y = -((e.clientY-rect.top)/rect.height)*2+1; - hoverRaycaster.setFromCamera(hoverMouse, camera); + hoverRaycaster.setFromCamera(hoverMouse, S.camera); var hits = hoverRaycaster.intersectObjects(S.mg.children, true); var hitMesh = null; if(hits.length>0 && hits[0].object.isMesh) hitMesh = hits[0].object; @@ -1833,7 +1834,7 @@ var rect=renderer.domElement.getBoundingClientRect(); var mx=((e.clientX-rect.left)/rect.width)*2-1; var my=-((e.clientY-rect.top)/rect.height)*2+1; - raycaster.setFromCamera(new THREE.Vector2(mx,my), camera); + raycaster.setFromCamera(new THREE.Vector2(mx,my), S2.camera); // Check rotation ring first var allHandles=[]; if(S2._sboxRotRing) allHandles.push(S2._sboxRotRing); @@ -1870,7 +1871,7 @@ var rect=renderer.domElement.getBoundingClientRect(); var mx=((e.clientX-rect.left)/rect.width)*2-1; var my=-((e.clientY-rect.top)/rect.height)*2+1; - raycaster.setFromCamera(new THREE.Vector2(mx,my), camera); + raycaster.setFromCamera(new THREE.Vector2(mx,my), state3d.current.camera); if(ds.type==='rotate'){ // Rotation: horizontal mouse movement maps to Y rotation @@ -1914,7 +1915,7 @@ var rect=renderer.domElement.getBoundingClientRect(); var mx=((e.clientX-rect.left)/rect.width)*2-1; var my=-((e.clientY-rect.top)/rect.height)*2+1; - raycaster.setFromCamera(new THREE.Vector2(mx,my), camera); + raycaster.setFromCamera(new THREE.Vector2(mx,my), S2.camera); var allH=[]; if(S2._sboxRotRing) allH.push(S2._sboxRotRing); if(S2._sboxHandles) allH=allH.concat(S2._sboxHandles); @@ -1943,7 +1944,7 @@ var rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((e.clientX-rect.left)/rect.width)*2-1; mouse.y = -((e.clientY-rect.top)/rect.height)*2+1; - raycaster.setFromCamera(mouse, camera); + raycaster.setFromCamera(mouse, S.camera); if(!S.mg) return; // Filter out ghosted meshes — clicking them would corrupt shared ghost material var hits = raycaster.intersectObjects(S.mg.children, true).filter(function(h){ @@ -2101,7 +2102,7 @@ var rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((e.clientX-rect.left)/rect.width)*2-1; mouse.y = -((e.clientY-rect.top)/rect.height)*2+1; - raycaster.setFromCamera(mouse, camera); + raycaster.setFromCamera(mouse, S.camera); var hits = raycaster.intersectObjects(S.mg.children, true).filter(function(h){ return h.object.material !== _ghostMat; }); @@ -2711,6 +2712,8 @@ var orig = obj.userData._origMaterial; // Skip if currently ghosted if(obj.material === S._ghostMat) return; + // Dispose the current non-original material before replacing to prevent leaks + if(obj.material !== orig) { obj.material.dispose(); } if(style==='wireframe'){ obj.material = new THREE.MeshBasicMaterial({ color: orig.color ? orig.color.clone() : new THREE.Color(0x64748b), @@ -2730,7 +2733,6 @@ side: THREE.DoubleSide, envMapIntensity:0.5 }); } else { - // standard: restore original obj.material = orig; } }); @@ -4622,7 +4624,7 @@ var defaultTitle = hasEl ? ('Issue: '+(ep.ifcType||'Element')+' '+(ep.name||'#'+el.expressId)) : ''; var defaultDesc = hasEl ? ('Model: '+el.modelName+'\nType: '+(ep.ifcType||'Unknown')+'\nName: '+(ep.name||'N/A')+'\nStorey: '+(ep.storey||'N/A')+'\nMaterial: '+(ep.material||'N/A')+'\nExpress ID: #'+el.expressId) : ''; - var f=useState({title:defaultTitle,desc:'',priority:pr.defaultPriority||'normal',status:pr.defaultStatus||'open',category:pr.defaultCategory||'design',assignee:'',dueDate:''}), form=f[0], setForm=f[1]; + var f=useState({title:defaultTitle,desc:defaultDesc,priority:pr.defaultPriority||'normal',status:pr.defaultStatus||'open',category:pr.defaultCategory||'design',assignee:'',dueDate:''}), form=f[0], setForm=f[1]; useEffect(function(){ function onKey(e){if(e.key==='Escape')d({t:'MODAL',v:false});} window.addEventListener('keydown',onKey); diff --git a/version.json b/version.json index 03f24a00..0f736861 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "major": 1, "minor": 2, - "patch": 45, + "patch": 26, "label": "" }