[{"data":1,"prerenderedAt":814},["ShallowReactive",2],{"$fam-eVDW-6VRzsFX4uzLQJzfx4XVvRazXjLaFk6cP7k8":3},{"newThing":4,"project":5,"cycle":9,"sections":21,"threads":768,"otherDocs":769,"documents":770,"images":771,"archiveItems":811,"links":812},"Click-to-focus camera framing + isometric↔top-down angle control",{"id":6,"label":7,"liveBase":8},"qmap","Qmap",null,{"id":10,"num":11,"dir":12,"name":13,"title":13,"focus":8,"status":14,"statusNote":8,"created":15,"completed":8,"mode":8,"report":8,"recordless":16,"thumbnail":17,"imageCount":18,"artifactCount":19,"lastActivity":20,"hasPickup":16,"hasInitialPrompt":16},"c6",6,"c6.camera-focus","camera-focus","archived","2026-03-23",false,"\u002Fcycles\u002Fqmap\u002Fc6-high-arcs.png",9,0,"2026-08-15",[22,61,90,126,177,387,425,474,572,648],{"key":23,"title":24,"empty":16,"ast":25},"objective","Goal",{"type":26,"children":27},"root",[28,36,57],{"type":29,"tag":30,"props":31,"children":32},"element","p",{},[33],{"type":34,"value":35},"text","Camera control experiments:",{"type":29,"tag":37,"props":38,"children":39},"ol",{},[40,46],{"type":29,"tag":41,"props":42,"children":43},"li",{},[44],{"type":34,"value":45},"Zoom and pan to frame selected actor and its connections",{"type":29,"tag":41,"props":47,"children":48},{},[49,55],{"type":29,"tag":50,"props":51,"children":52},"strong",{},[53],{"type":34,"value":54},"Camera angle slider",{"type":34,"value":56}," - transition from isometric to top-down view",{"type":29,"tag":58,"props":59,"children":60},"hr",{},[],{"key":62,"title":63,"empty":16,"ast":64},"outcome","Outcome",{"type":26,"children":65},[66,71,80],{"type":29,"tag":30,"props":67,"children":68},{},[69],{"type":34,"value":70},"Camera framing system complete. Click actor → camera animates to frame actor and connections. Unfocus → camera returns to initial view. Integrates cleanly with grouping transitions (re-centers if selection active). Core CameraControls now properly supports arbitrary lookAt targets for future pan\u002Fnavigation features.",{"type":29,"tag":30,"props":72,"children":73},{},[74,78],{"type":29,"tag":50,"props":75,"children":76},{},[77],{"type":34,"value":54},{"type":34,"value":79}," allows exploration of different viewing perspectives - useful for understanding spatial relationships from above vs the standard isometric view.",{"type":29,"tag":30,"props":81,"children":82},{},[83,88],{"type":29,"tag":50,"props":84,"children":85},{},[86],{"type":34,"value":87},"Rotation controls",{"type":34,"value":89}," provide quick access to cardinal viewing directions without drag interaction - useful for consistent screenshots or comparing layouts from fixed angles.",{"key":91,"title":92,"empty":16,"ast":93},"extra","Focus",{"type":26,"children":94},[95,123],{"type":29,"tag":96,"props":97,"children":98},"ul",{},[99,104,109,114],{"type":29,"tag":41,"props":100,"children":101},{},[102],{"type":34,"value":103},"Automatic camera framing when clicking an actor",{"type":29,"tag":41,"props":105,"children":106},{},[107],{"type":34,"value":108},"Smooth animated transitions for camera position and zoom",{"type":29,"tag":41,"props":110,"children":111},{},[112],{"type":34,"value":113},"Reset to initial view on background click",{"type":29,"tag":41,"props":115,"children":116},{},[117,121],{"type":29,"tag":50,"props":118,"children":119},{},[120],{"type":34,"value":54},{"type":34,"value":122}," (0% isometric → 100% top-down)",{"type":29,"tag":58,"props":124,"children":125},{},[],{"key":91,"title":127,"empty":16,"ast":128},"Approach",{"type":26,"children":129},[130,174],{"type":29,"tag":37,"props":131,"children":132},{},[133,161],{"type":29,"tag":41,"props":134,"children":135},{},[136,138],{"type":34,"value":137},"When actor is clicked:",{"type":29,"tag":96,"props":139,"children":140},{},[141,146,151,156],{"type":29,"tag":41,"props":142,"children":143},{},[144],{"type":34,"value":145},"Collect positions of clicked actor + all connected actors",{"type":29,"tag":41,"props":147,"children":148},{},[149],{"type":34,"value":150},"Calculate bounding box of those positions",{"type":29,"tag":41,"props":152,"children":153},{},[154],{"type":34,"value":155},"Compute frustum size needed to frame the bounding box",{"type":29,"tag":41,"props":157,"children":158},{},[159],{"type":34,"value":160},"Animate camera to center on and zoom to fit",{"type":29,"tag":41,"props":162,"children":163},{},[164,166],{"type":34,"value":165},"When background is clicked:",{"type":29,"tag":96,"props":167,"children":168},{},[169],{"type":29,"tag":41,"props":170,"children":171},{},[172],{"type":34,"value":173},"Animate camera back to initial state (captured on scene init)",{"type":29,"tag":58,"props":175,"children":176},{},[],{"key":91,"title":178,"empty":16,"ast":179},"Key Files",{"type":26,"children":180},[181,195,242,254,300,311,384],{"type":29,"tag":182,"props":183,"children":185},"h3",{"id":184},"new-sharedcameraanimationjs",[186,188],{"type":34,"value":187},"New: ",{"type":29,"tag":189,"props":190,"children":192},"code",{"className":191},[],[193],{"type":34,"value":194},"shared\u002FcameraAnimation.js",{"type":29,"tag":96,"props":196,"children":197},{},[198,209,220,231],{"type":29,"tag":41,"props":199,"children":200},{},[201,207],{"type":29,"tag":189,"props":202,"children":204},{"className":203},[],[205],{"type":34,"value":206},"calculateFocusBounds(positions, padding)",{"type":34,"value":208}," - Bounding box from actor positions",{"type":29,"tag":41,"props":210,"children":211},{},[212,218],{"type":29,"tag":189,"props":213,"children":215},{"className":214},[],[216],{"type":34,"value":217},"frustumSizeForBounds(bounds, aspect, padding)",{"type":34,"value":219}," - Required frustum to frame bounds",{"type":29,"tag":41,"props":221,"children":222},{},[223,229],{"type":29,"tag":189,"props":224,"children":226},{"className":225},[],[227],{"type":34,"value":228},"animateCameraFocus(camera, controls, config, options)",{"type":34,"value":230}," - Animated camera transition",{"type":29,"tag":41,"props":232,"children":233},{},[234,240],{"type":29,"tag":189,"props":235,"children":237},{"className":236},[],[238],{"type":34,"value":239},"captureCameraState(camera, controls)",{"type":34,"value":241}," - Snapshot camera state for reset",{"type":29,"tag":182,"props":243,"children":245},{"id":244},"modified-srccorecameracontrolsjs",[246,248],{"type":34,"value":247},"Modified: ",{"type":29,"tag":189,"props":249,"children":251},{"className":250},[],[252],{"type":34,"value":253},"src\u002Fcore\u002FCameraControls.js",{"type":29,"tag":96,"props":255,"children":256},{},[257,270,289],{"type":29,"tag":41,"props":258,"children":259},{},[260,262,268],{"type":34,"value":261},"Added ",{"type":29,"tag":189,"props":263,"children":265},{"className":264},[],[266],{"type":34,"value":267},"target",{"type":34,"value":269}," property (was hardcoded to origin)",{"type":29,"tag":41,"props":271,"children":272},{},[273,279,281,287],{"type":29,"tag":189,"props":274,"children":276},{"className":275},[],[277],{"type":34,"value":278},"setTarget(target)",{"type":34,"value":280}," \u002F ",{"type":29,"tag":189,"props":282,"children":284},{"className":283},[],[285],{"type":34,"value":286},"getTarget()",{"type":34,"value":288}," methods",{"type":29,"tag":41,"props":290,"children":291},{},[292,298],{"type":29,"tag":189,"props":293,"children":295},{"className":294},[],[296],{"type":34,"value":297},"rotateY()",{"type":34,"value":299}," now respects target for proper rotation around focus point",{"type":29,"tag":182,"props":301,"children":303},{"id":302},"modified-experimentsappvue",[304,305],{"type":34,"value":247},{"type":29,"tag":189,"props":306,"children":308},{"className":307},[],[309],{"type":34,"value":310},"ExperimentsApp.vue",{"type":29,"tag":96,"props":312,"children":313},{},[314,325,336,347,366],{"type":29,"tag":41,"props":315,"children":316},{},[317,323],{"type":29,"tag":189,"props":318,"children":320},{"className":319},[],[321],{"type":34,"value":322},"initialCameraState",{"type":34,"value":324}," ref - Stores initial camera state",{"type":29,"tag":41,"props":326,"children":327},{},[328,334],{"type":29,"tag":189,"props":329,"children":331},{"className":330},[],[332],{"type":34,"value":333},"focusCameraOnPositions(positions)",{"type":34,"value":335}," - Focus camera on actor positions",{"type":29,"tag":41,"props":337,"children":338},{},[339,345],{"type":29,"tag":189,"props":340,"children":342},{"className":341},[],[343],{"type":34,"value":344},"resetCameraToInitial()",{"type":34,"value":346}," - Return camera to initial view",{"type":29,"tag":41,"props":348,"children":349},{},[350,356,358,364],{"type":29,"tag":189,"props":351,"children":353},{"className":352},[],[354],{"type":34,"value":355},"selectActor()",{"type":34,"value":357}," - Now calls ",{"type":29,"tag":189,"props":359,"children":361},{"className":360},[],[362],{"type":34,"value":363},"focusCameraOnPositions",{"type":34,"value":365}," after highlighting",{"type":29,"tag":41,"props":367,"children":368},{},[369,375,376,382],{"type":29,"tag":189,"props":370,"children":372},{"className":371},[],[373],{"type":34,"value":374},"clearSelection()",{"type":34,"value":357},{"type":29,"tag":189,"props":377,"children":379},{"className":378},[],[380],{"type":34,"value":381},"resetCameraToInitial",{"type":34,"value":383}," after reset",{"type":29,"tag":58,"props":385,"children":386},{},[],{"key":91,"title":388,"empty":16,"ast":389},"Animation Config",{"type":26,"children":390},[391,404,417,420],{"type":29,"tag":392,"props":393,"children":399},"pre",{"className":394,"code":395,"language":396,"meta":397,"style":398},"language-javascript","{\n  focusDuration: 500,    \u002F\u002F ms for focus animation\n  resetDuration: 500,    \u002F\u002F ms for reset animation\n  minFrustumSize: 4,     \u002F\u002F Don't zoom in more than this\n  focusPadding: 2.0,     \u002F\u002F Extra padding around framed content\n  actorPadding: 1.0,     \u002F\u002F Padding around actor bounding box\n}\n","javascript","","undefined",[400],{"type":29,"tag":189,"props":401,"children":402},{"__ignoreMap":397},[403],{"type":34,"value":395},{"type":29,"tag":30,"props":405,"children":406},{},[407,409,415],{"type":34,"value":408},"Uses ",{"type":29,"tag":189,"props":410,"children":412},{"className":411},[],[413],{"type":34,"value":414},"easeOutCubic",{"type":34,"value":416}," for quick start, gentle landing.",{"type":29,"tag":58,"props":418,"children":419},{},[],{"type":29,"tag":421,"props":422,"children":423},"style",{},[424],{"type":34,"value":397},{"key":91,"title":426,"empty":16,"ast":427},"Notes",{"type":26,"children":428},[429,471],{"type":29,"tag":96,"props":430,"children":431},{},[432,461,466],{"type":29,"tag":41,"props":433,"children":434},{},[435,437,443,445,451,453,459],{"type":34,"value":436},"Reuses animation system from ",{"type":29,"tag":189,"props":438,"children":440},{"className":439},[],[441],{"type":34,"value":442},"morphAnimation.js",{"type":34,"value":444}," (",{"type":29,"tag":189,"props":446,"children":448},{"className":447},[],[449],{"type":34,"value":450},"animateValue",{"type":34,"value":452},", ",{"type":29,"tag":189,"props":454,"children":456},{"className":455},[],[457],{"type":34,"value":458},"lerp",{"type":34,"value":460},", easing functions)",{"type":29,"tag":41,"props":462,"children":463},{},[464],{"type":34,"value":465},"Camera maintains isometric angle during pan by offsetting position relative to target",{"type":29,"tag":41,"props":467,"children":468},{},[469],{"type":34,"value":470},"Frustum size clamped to CameraControls min\u002Fmax zoom constraints",{"type":29,"tag":58,"props":472,"children":473},{},[],{"key":91,"title":475,"empty":16,"ast":476},"Key Learnings",{"type":26,"children":477},[478,488,496,534,552,569],{"type":29,"tag":30,"props":479,"children":480},{},[481,486],{"type":29,"tag":50,"props":482,"children":483},{},[484],{"type":34,"value":485},"Isometric pan without rotation:",{"type":34,"value":487}," Store camera-to-target offset once, then apply to new targets. Moving both camera position and lookAt target by the same delta preserves the viewing angle.",{"type":29,"tag":30,"props":489,"children":490},{},[491],{"type":29,"tag":50,"props":492,"children":493},{},[494],{"type":34,"value":495},"Orthographic framing math:",{"type":29,"tag":96,"props":497,"children":498},{},[499,510,521],{"type":29,"tag":41,"props":500,"children":501},{},[502,508],{"type":29,"tag":189,"props":503,"children":505},{"className":504},[],[506],{"type":34,"value":507},"frustumSize",{"type":34,"value":509}," = vertical extent (top to bottom)",{"type":29,"tag":41,"props":511,"children":512},{},[513,515],{"type":34,"value":514},"Horizontal extent = ",{"type":29,"tag":189,"props":516,"children":518},{"className":517},[],[519],{"type":34,"value":520},"frustumSize × aspect",{"type":29,"tag":41,"props":522,"children":523},{},[524,526,532],{"type":34,"value":525},"Frame bounds by taking ",{"type":29,"tag":189,"props":527,"children":529},{"className":528},[],[530],{"type":34,"value":531},"max(width\u002Faspect, depth)",{"type":34,"value":533}," to ensure both fit",{"type":29,"tag":30,"props":535,"children":536},{},[537,542,544,550],{"type":29,"tag":50,"props":538,"children":539},{},[540],{"type":34,"value":541},"State capture\u002Frestore pattern:",{"type":34,"value":543}," The same ",{"type":29,"tag":189,"props":545,"children":547},{"className":546},[],[548],{"type":34,"value":549},"animateCameraFocus()",{"type":34,"value":551}," function handles both focus-in and reset—just pass different target configurations (captured initial state vs computed bounds).",{"type":29,"tag":30,"props":553,"children":554},{},[555,560,562,567],{"type":29,"tag":50,"props":556,"children":557},{},[558],{"type":34,"value":559},"CameraControls target property:",{"type":34,"value":561}," Extending controls with a mutable ",{"type":29,"tag":189,"props":563,"children":565},{"className":564},[],[566],{"type":34,"value":267},{"type":34,"value":568}," Vector3 enables proper rotation around arbitrary points (not just origin) and panning without losing isometric angle.",{"type":29,"tag":58,"props":570,"children":571},{},[],{"key":91,"title":573,"empty":16,"ast":574},"Camera Angle Slider",{"type":26,"children":575},[576,582,587,610,615,633,641,644],{"type":29,"tag":182,"props":577,"children":579},{"id":578},"implementation",[580],{"type":34,"value":581},"Implementation",{"type":29,"tag":30,"props":583,"children":584},{},[585],{"type":34,"value":586},"Uses spherical coordinates to interpolate camera position:",{"type":29,"tag":96,"props":588,"children":589},{},[590,600],{"type":29,"tag":41,"props":591,"children":592},{},[593,598],{"type":29,"tag":50,"props":594,"children":595},{},[596],{"type":34,"value":597},"Isometric view (0%):",{"type":34,"value":599}," phi = atan(√2) ≈ 54.7° from y-axis",{"type":29,"tag":41,"props":601,"children":602},{},[603,608],{"type":29,"tag":50,"props":604,"children":605},{},[606],{"type":34,"value":607},"Top-down view (100%):",{"type":34,"value":609}," phi ≈ 0° (nearly straight down)",{"type":29,"tag":30,"props":611,"children":612},{},[613],{"type":34,"value":614},"The slider maintains:",{"type":29,"tag":96,"props":616,"children":617},{},[618,623,628],{"type":29,"tag":41,"props":619,"children":620},{},[621],{"type":34,"value":622},"Current horizontal rotation (theta) - so you can rotate then adjust angle",{"type":29,"tag":41,"props":624,"children":625},{},[626],{"type":34,"value":627},"Current distance from target (radius)",{"type":29,"tag":41,"props":629,"children":630},{},[631],{"type":34,"value":632},"LookAt target position",{"type":29,"tag":392,"props":634,"children":636},{"className":394,"code":635,"language":396,"meta":397,"style":398},"\u002F\u002F Interpolate phi based on slider (0-100)\nconst t = cameraAngle \u002F 100;\nconst phi = isometricPhi * (1 - t) + topDownPhi * t;\n",[637],{"type":29,"tag":189,"props":638,"children":639},{"__ignoreMap":397},[640],{"type":34,"value":635},{"type":29,"tag":58,"props":642,"children":643},{},[],{"type":29,"tag":421,"props":645,"children":646},{},[647],{"type":34,"value":397},{"key":91,"title":649,"empty":16,"ast":650},"Rotation Controls",{"type":26,"children":651},[652,658,668,674,707,711,716,729,737,743,761,764],{"type":29,"tag":182,"props":653,"children":655},{"id":654},"ui-layout",[656],{"type":34,"value":657},"UI Layout",{"type":29,"tag":392,"props":659,"children":663},{"className":660,"code":662,"language":34},[661],"language-text","[-] [Reset Rotation] [+]\n",[664],{"type":29,"tag":189,"props":665,"children":666},{"__ignoreMap":397},[667],{"type":34,"value":662},{"type":29,"tag":182,"props":669,"children":671},{"id":670},"functions",[672],{"type":34,"value":673},"Functions",{"type":29,"tag":96,"props":675,"children":676},{},[677,687,697],{"type":29,"tag":41,"props":678,"children":679},{},[680,685],{"type":29,"tag":50,"props":681,"children":682},{},[683],{"type":34,"value":684},"Reset Rotation",{"type":34,"value":686}," - Sets theta to 0 (camera along +Z axis)",{"type":29,"tag":41,"props":688,"children":689},{},[690,695],{"type":29,"tag":50,"props":691,"children":692},{},[693],{"type":34,"value":694},"Rotate Left (-)",{"type":34,"value":696}," - Rotates 90° counter-clockwise",{"type":29,"tag":41,"props":698,"children":699},{},[700,705],{"type":29,"tag":50,"props":701,"children":702},{},[703],{"type":34,"value":704},"Rotate Right (+)",{"type":34,"value":706}," - Rotates 90° clockwise",{"type":29,"tag":182,"props":708,"children":709},{"id":578},[710],{"type":34,"value":581},{"type":29,"tag":30,"props":712,"children":713},{},[714],{"type":34,"value":715},"All rotation controls use spherical coordinates, preserving:",{"type":29,"tag":96,"props":717,"children":718},{},[719,724],{"type":29,"tag":41,"props":720,"children":721},{},[722],{"type":34,"value":723},"Current phi (vertical angle from slider)",{"type":29,"tag":41,"props":725,"children":726},{},[727],{"type":34,"value":728},"Current radius (zoom distance)",{"type":29,"tag":392,"props":730,"children":732},{"className":394,"code":731,"language":396,"meta":397,"style":398},"function rotateBy(angle) {\n  const spherical = new THREE.Spherical(\n    currentSpherical.radius,\n    currentSpherical.phi,\n    currentSpherical.theta + angle  \u002F\u002F +\u002F- π\u002F2 for 90°\n  );\n  \u002F\u002F ... apply to camera\n}\n",[733],{"type":29,"tag":189,"props":734,"children":735},{"__ignoreMap":397},[736],{"type":34,"value":731},{"type":29,"tag":182,"props":738,"children":740},{"id":739},"design-notes",[741],{"type":34,"value":742},"Design Notes",{"type":29,"tag":96,"props":744,"children":745},{},[746,751,756],{"type":29,"tag":41,"props":747,"children":748},{},[749],{"type":34,"value":750},"Reset goes to theta=0 (not 45°) for predictable \"front view\" alignment",{"type":29,"tag":41,"props":752,"children":753},{},[754],{"type":34,"value":755},"90° increments allow quick exploration of all four cardinal directions",{"type":29,"tag":41,"props":757,"children":758},{},[759],{"type":34,"value":760},"Controls work independently of the angle slider - can rotate at any tilt",{"type":29,"tag":58,"props":762,"children":763},{},[],{"type":29,"tag":421,"props":765,"children":766},{},[767],{"type":34,"value":397},[],[],[],[772,779,783,787,791,795,798,802,807],{"src":773,"kind":774,"order":775,"caption":776,"capturedAt":777,"route":778},"\u002Fcycles\u002Fqmap\u002Fc6-isometric.png","hero",38,"View angle 0 — full isometric","2026-08-16T03:06:59.862Z","\u002Fexperiments\u002Fc6.camera-focus\u002F",{"src":780,"kind":774,"order":781,"caption":782,"capturedAt":777,"route":778},"\u002Fcycles\u002Fqmap\u002Fc6-topdown.png",41,"Angle 100 — top-down map",{"src":784,"kind":774,"order":785,"caption":786,"capturedAt":777,"route":778},"\u002Fcycles\u002Fqmap\u002Fc6-rotated.png",42,"Rotated a step, back at working angle",{"src":788,"kind":774,"order":789,"caption":790,"capturedAt":777,"route":778},"\u002Fcycles\u002Fqmap\u002Fc6-rotated-more.png",43,"Two more rotation steps",{"src":792,"kind":774,"order":793,"caption":794,"capturedAt":777,"route":778},"\u002Fcycles\u002Fqmap\u002Fc6-vendors-policy.png",44,"Vendors regrouped by security policy under the rotated camera",{"src":17,"kind":774,"order":796,"caption":797,"capturedAt":777,"route":778},45,"Curve height 8",{"src":799,"kind":800,"order":801},"\u002Fcycles\u002Fqmap\u002Fc6.outcomes.png","legacy",100,{"src":803,"kind":804,"order":805,"caption":806,"capturedAt":777,"route":778},"\u002Fcycles\u002Fqmap\u002Fc6-angle-35.png","slice",35,"Angle 35 — the working tilt",{"src":808,"kind":804,"order":809,"caption":810,"capturedAt":777,"route":778},"\u002Fcycles\u002Fqmap\u002Fc6-angle-70.png",70,"Angle 70 — flattening out",[],[813],{"route":778,"href":8},1794599370176]