Back to Body Journal
SOURCE / PINNED RELEASE

Made of little things.

Body Journal

Release
e8049f8da4e2…
Author-recorded commit
61c04bf0e8aa…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6c4554844c7

Archive hash verified: 4369d778da046a8b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/inspection-controls.ts
import { escape as esc } from './case.js';
import type { BodyScene } from './body-scene.js';
import { anatomyLeaves, flattenAnatomy, visibilityState, type AnatomyNode } from './anatomy-tree.js';

/** One anatomy browser, two independent actions: display and camera focus. */
export class InspectionControls {
  private panel: HTMLElement;
  private toggle: HTMLButtonElement;
  private tree!: AnatomyNode;
  private signature = '';
  private selected: string | null = null;
  private focused: string | null = null;
  private expanded = new Set(['region:body']);
  constructor(private workspace: HTMLElement, private scene: BodyScene, changed: () => void) {
    this.toggle = workspace.querySelector<HTMLButtonElement>('#anatomyToggle')!;
    this.panel = document.createElement('aside');this.panel.id='anatomyPanel';this.panel.className='anatomy-panel';this.panel.setAttribute('aria-label','Anatomy focus and visibility');
    this.panel.innerHTML=`<div class="anatomy-scroll"><p class="anatomy-help">Check to show. Select a name, then Focus.</p><label class="field"><span class="sr-only">Find a body part</span><input id="anatomySearch" type="search" placeholder="Find a body part…" autocomplete="off" /></label><div class="anatomy-actions"><button id="showAllAnatomy">Show all</button><button id="hideAllAnatomy">Hide all</button><button id="wholeBody" title="Reset camera; keep visibility">Whole body</button></div><div id="visibilityTree" class="visibility-tree"></div></div><div class="anatomy-footer"><span id="anatomySelection" aria-live="polite"></span><button id="applyFocus" class="primary-button">Focus</button></div>`;
    workspace.append(this.panel);this.setOpen(true);
    this.toggle.onclick=()=>{this.setOpen(!this.open);changed();};
    this.panel.querySelector('#anatomySearch')!.addEventListener('input',()=>this.render());
    this.panel.addEventListener('click',e=>{
      const button=(e.target as HTMLElement).closest<HTMLButtonElement>('button');if(!button)return;
      if(button.dataset.expand){const id=button.dataset.expand;this.expanded.has(id)?this.expanded.delete(id):this.expanded.add(id);this.render();this.panel.querySelector<HTMLButtonElement>(`[data-expand="${id}"]`)?.focus({preventScroll:true});}
      if(button.dataset.focus){this.selected=button.dataset.focus;this.status();}
      if(button.id==='applyFocus')this.scene.focus(this.selected==='region:body'?null:this.selected);
      if(button.id==='wholeBody')this.scene.focus(null);
      if(button.id==='showAllAnatomy'||button.id==='hideAllAnatomy')this.scene.setVisible(this.tree.id,button.id==='showAllAnatomy');
    });
    this.panel.addEventListener('change',e=>{const input=e.target as HTMLInputElement;if(input.dataset.visibility)this.scene.setVisible(input.dataset.visibility,input.checked);});
    this.panel.addEventListener('keydown',e=>{if(e.key==='Escape'){e.preventDefault();this.setOpen(false);this.toggle.focus();changed();}});
    this.refresh();
  }
  get open(): boolean { return !this.panel.hidden; }
  setOpen(open: boolean): void {this.panel.hidden=!open;this.workspace.classList.toggle('anatomy-open',open);this.toggle.setAttribute('aria-expanded',String(open));this.toggle.textContent=open?'Anatomy −':'Anatomy +';}
  refresh(): void {
    const tree=this.scene.visibilityTree(),signature=JSON.stringify(tree),focus=this.scene.getInspection().target;
    if(focus!==this.focused){this.focused=focus;this.selected=focus || 'region:body';}
    if(signature!==this.signature){this.signature=signature;this.tree=tree;this.selected=focus || 'region:body';this.render();}else this.status();
  }
  private status(): void {
    const hidden=new Set(this.scene.getInspection().hidden),nodes=new Map(flattenAnatomy(this.tree).map(n=>[n.id,n]));
    this.panel.querySelectorAll<HTMLInputElement>('[data-visibility]').forEach(input=>{const state=visibilityState(nodes.get(input.dataset.visibility!)!,hidden);input.checked=state==='checked';input.indeterminate=state==='mixed';});
    this.panel.querySelectorAll<HTMLElement>('[data-focus]').forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.focus===this.selected)));
    const selected=nodes.get(this.selected || ''),unshown=selected && visibilityState(selected,hidden)==='unchecked';
    this.panel.querySelector('#anatomySelection')!.textContent=`${selected?.label || 'Select a part'}${unshown?' · hidden':''}`;
    this.panel.querySelector<HTMLButtonElement>('#applyFocus')!.disabled=!selected || selected.id==='outline';
    const leaves=anatomyLeaves(this.tree);this.toggle.title=`${leaves.filter(id=>!hidden.has(id)).length} of ${leaves.length} parts visible`;
  }
  private render(): void {
    const query=this.panel.querySelector<HTMLInputElement>('#anatomySearch')!.value.trim().toLowerCase();let serial=0;
    const matches=(n:AnatomyNode):boolean=>n.label.toLowerCase().includes(query)||!!n.children?.some(matches);
    const row=(n:AnatomyNode,ancestor=false):string=>{
      if(query&&!ancestor&&!matches(n))return '';
      const id=`anatomy-children-${serial++}`,open=this.expanded.has(n.id)||!!query,own=ancestor||!!query&&n.label.toLowerCase().includes(query);
      return `<li><div class="visibility-row">${n.children?`<button class="visibility-expand" data-expand="${esc(n.id)}" aria-expanded="${open}" aria-controls="${id}" aria-label="Expand ${esc(n.label)}">${open?'−':'+'}</button>`:'<span class="visibility-indent"></span>'}<label class="anatomy-check"><input type="checkbox" data-visibility="${esc(n.id)}" aria-label="Show ${esc(n.label)}" /></label><button class="anatomy-name" data-focus="${esc(n.id)}" aria-pressed="false">${esc(n.label)}</button></div>${n.children?`<ul id="${id}" ${open?'':'hidden'}>${n.children.map(c=>row(c,own)).join('')}</ul>`:''}</li>`;
    };
    this.panel.querySelector('#visibilityTree')!.innerHTML=`<ul>${row(this.tree)||'<li class="anatomy-help">No matching part in this layer.</li>'}</ul>`;this.status();
  }
}