@rig-c/review 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +242 -7
  2. package/docs/DESIGN.md +33 -5
  3. package/docs/REVIEW_PACKAGE.md +115 -0
  4. package/package.json +1 -1
  5. package/src/applied-context.ts +285 -0
  6. package/src/baseline.ts +175 -0
  7. package/src/bone-anchors.ts +123 -0
  8. package/src/bones-check.ts +105 -0
  9. package/src/build-route.ts +109 -0
  10. package/src/chest-landmarks.ts +122 -0
  11. package/src/chest-regions.ts +65 -6
  12. package/src/cli.ts +19 -8
  13. package/src/config-patch.ts +213 -0
  14. package/src/cut-edits.ts +184 -0
  15. package/src/environment.ts +226 -0
  16. package/src/feedback-export.ts +25 -1
  17. package/src/front.ts +11 -0
  18. package/src/handoff.ts +208 -0
  19. package/src/idle-tracks.ts +401 -0
  20. package/src/manifest.ts +15 -6
  21. package/src/mesh-edit.ts +317 -0
  22. package/src/mesh-entry.ts +351 -0
  23. package/src/open.ts +12 -5
  24. package/src/physics-tuning.ts +236 -0
  25. package/src/producer-files.ts +11 -0
  26. package/src/producer.ts +158 -0
  27. package/src/readings-run.ts +129 -0
  28. package/src/readings.ts +235 -0
  29. package/src/rebuild.ts +938 -0
  30. package/src/reference.ts +4 -3
  31. package/src/requirements-run.ts +160 -0
  32. package/src/requirements.ts +230 -0
  33. package/src/retarget.ts +211 -0
  34. package/src/retune-route.ts +52 -0
  35. package/src/rig-fields.ts +295 -0
  36. package/src/server.ts +59 -5
  37. package/src/session.ts +12 -0
  38. package/src/setup-mesh.ts +144 -0
  39. package/src/weights.ts +74 -0
  40. package/web/bones-panel.ts +230 -0
  41. package/web/chest-editor.ts +294 -0
  42. package/web/chest-regions-tool.ts +10 -4
  43. package/web/cuts-panel.ts +266 -0
  44. package/web/idle-panel.ts +280 -0
  45. package/web/index.html +2 -2
  46. package/web/inspector-blocks.ts +77 -0
  47. package/web/main.ts +813 -24
  48. package/web/mesh-editor.ts +303 -0
  49. package/web/mesh-panel.ts +273 -0
  50. package/web/part-selection.ts +13 -5
  51. package/web/part-thumbnails.ts +76 -0
  52. package/web/physics-panel.ts +151 -0
  53. package/web/readings-panel.ts +105 -0
  54. package/web/rebuild-client.ts +216 -0
  55. package/web/requirements-panel.ts +238 -0
  56. package/web/retarget-panel.ts +203 -0
  57. package/web/rig-fields-panel.ts +269 -0
  58. package/web/rig-overlays.ts +171 -14
  59. package/web/setup-pose.ts +30 -0
  60. package/web/styles.css +52 -5
  61. package/web/weights-view.ts +159 -0
  62. package/web/workspace-preferences.ts +17 -12
  63. package/web/workspace-ui.ts +5 -3
@@ -0,0 +1,77 @@
1
+ /** Inspector block row: the sticky row of buttons at the top of each Inspector section (index.html
2
+ * `.block-row`). A button scrolls its block into view inside the section (the section is the Inspector's
3
+ * scroll container) and marks it `aria-current`; the active block then follows scrolling. The active block
4
+ * per section is the only thing this adds to the inspection draft (its `ui` record, key `inspectorBlocks`);
5
+ * the scroll offset itself stays with the Inspector scroll memory. */
6
+ export const inspectorBlocksKey='inspectorBlocks';
7
+ export type ActiveBlocks=Record<string,string>;
8
+ type DraftUI=Record<string,string|number|null>;
9
+ const sectionName=/^[a-z][a-z0-9-]{0,31}$/,blockId=/^[a-z][a-z0-9-]{0,63}$/;
10
+ /** Parse a saved value; anything malformed restores as no selection rather than refusing the draft. */
11
+ export function parseActiveBlocks(value:unknown):ActiveBlocks {
12
+ if(typeof value!=='string')return {};let raw:unknown;try{raw=JSON.parse(value);}catch{return {};}
13
+ if(!raw||typeof raw!=='object'||Array.isArray(raw))return {};const result:ActiveBlocks={};
14
+ for(const [section,id]of Object.entries(raw as Record<string,unknown>))if(sectionName.test(section)&&typeof id==='string'&&blockId.test(id))result[section]=id;
15
+ return result;
16
+ }
17
+ /** The draft `ui` entry for the active blocks. */
18
+ export function activeBlocksUI(state:ActiveBlocks):DraftUI {return {[inspectorBlocksKey]:JSON.stringify(state)};}
19
+ /** The active blocks read back from a draft `ui` record. */
20
+ export function restoreActiveBlocksUI(ui:DraftUI):ActiveBlocks {return parseActiveBlocks(ui[inspectorBlocksKey]);}
21
+
22
+ export interface BlockObserver {observe(target:Element):void;disconnect():void}
23
+ export interface InspectorBlocksOptions {
24
+ /** Sections carrying `data-inspector-panel`, each with a `.block-row` first child. */
25
+ sections:HTMLElement[];
26
+ /** Creates the scroll-follow observer for one section; defaults to IntersectionObserver rooted at the section. */
27
+ observer?:(root:HTMLElement,changed:()=>void)=>BlockObserver|null;
28
+ /** Called when a click or scrolling changes a section's active block. */
29
+ changed?:()=>void;
30
+ reducedMotion?:()=>boolean;now?:()=>number;
31
+ }
32
+ export interface InspectorBlocks {state():ActiveBlocks;restore(state:ActiveBlocks):void;select(section:string,id:string):void;refresh():void;dispose():void}
33
+ /** Gap kept between the sticky row and a block scrolled under it, in CSS pixels. */
34
+ export const blockScrollGap=6;
35
+ /** How long a programmatic scroll (click or restore) holds the chosen block against scroll-follow, unless the user scrolls first. */
36
+ const holdMs=1200;
37
+
38
+ export function bindInspectorBlocks(options:InspectorBlocksOptions):InspectorBlocks {
39
+ const now=options.now??(()=>Date.now()),reduced=options.reducedMotion??(()=>typeof matchMedia==='function'&&matchMedia('(prefers-reduced-motion: reduce)').matches);
40
+ const makeObserver=options.observer??((root,changed)=>typeof IntersectionObserver==='function'?new IntersectionObserver(changed,{root,threshold:[0,.25,.5,.75,1]}):null);
41
+ let ready=false;const disposers:(()=>void)[]=[],state:ActiveBlocks={},rows=new Map<string,{section:HTMLElement;row:HTMLElement;buttons:HTMLButtonElement[];hold:number;follow:()=>void}>();
42
+ const targetOf=(section:HTMLElement,button:HTMLButtonElement)=>{const id=button.dataset.blockTarget??'';const node=id?section.ownerDocument.getElementById(id):null;return node&&section.contains(node)?node:null;};
43
+ const mark=(name:string,id:string)=>{const entry=rows.get(name);if(!entry||!entry.buttons.some(button=>button.dataset.blockTarget===id))return false;for(const button of entry.buttons){if(button.dataset.blockTarget===id)button.setAttribute('aria-current','true');else button.removeAttribute('aria-current');}const before=state[name];state[name]=id;if(before!==id&&ready)options.changed?.();return true;};
44
+ for(const section of options.sections){
45
+ const name=section.dataset.inspectorPanel??'',row=[...section.children].find(node=>(node as HTMLElement).classList.contains('block-row')) as HTMLElement|undefined;if(!name||!row)continue;
46
+ const buttons=[...row.querySelectorAll<HTMLButtonElement>('button[data-block-target]')].filter(button=>targetOf(section,button));if(!buttons.length)continue;
47
+ const entry={section,row,buttons,hold:0,follow:()=>{}};rows.set(name,entry);
48
+ // Scroll-follow: the active block is the last one whose top has reached the row; at the end of the
49
+ // section it is the last visible one, so a short closing block can still become active.
50
+ entry.follow=()=>{
51
+ if(entry.hold>now()||section.hidden)return;const top=section.getBoundingClientRect().top+row.getBoundingClientRect().height+blockScrollGap*4;
52
+ const visible=buttons.map(button=>({button,node:targetOf(section,button)})).filter((item):item is {button:HTMLButtonElement;node:HTMLElement}=>!!item.node&&!item.node.hidden);if(!visible.length)return;
53
+ let current=visible[0]!;for(const item of visible)if(item.node.getBoundingClientRect().top<=top)current=item;
54
+ if(section.scrollHeight-section.clientHeight>0&&section.scrollTop>=section.scrollHeight-section.clientHeight-2){const bottom=section.getBoundingClientRect().bottom;for(const item of visible)if(item.node.getBoundingClientRect().top<bottom)current=item;}
55
+ mark(name,current.button.dataset.blockTarget!);
56
+ };
57
+ const click=(event:Event)=>{const button=(event.target as Element|null)?.closest?.('button[data-block-target]') as HTMLButtonElement|null;if(!button||!buttons.includes(button))return;select(name,button.dataset.blockTarget!);};
58
+ const release=()=>{entry.hold=0;};const intent=(event:Event)=>{if(!row.contains(event.target as Node))release();};
59
+ row.addEventListener('click',click);for(const type of ['wheel','touchstart','keydown','pointerdown'])section.addEventListener(type,intent,{passive:true});section.addEventListener('scrollend',release);
60
+ const observer=makeObserver(section,()=>entry.follow());if(observer)for(const button of buttons)observer.observe(targetOf(section,button)!);
61
+ disposers.push(()=>{row.removeEventListener('click',click);for(const type of ['wheel','touchstart','keydown','pointerdown'])section.removeEventListener(type,intent);section.removeEventListener('scrollend',release);observer?.disconnect();});
62
+ mark(name,buttons[0]!.dataset.blockTarget!);
63
+ }
64
+ ready=true;
65
+ /** Scroll the block so its top sits just under the sticky row, then mark it. */
66
+ function select(name:string,id:string):void {
67
+ const entry=rows.get(name);if(!entry)return;const button=entry.buttons.find(b=>b.dataset.blockTarget===id);const node=button&&targetOf(entry.section,button);if(!node)return;
68
+ const {section,row}=entry,offset=node.getBoundingClientRect().top-section.getBoundingClientRect().top-row.getBoundingClientRect().height-blockScrollGap;
69
+ entry.hold=now()+holdMs;mark(name,id);section.scrollTo({top:Math.max(0,section.scrollTop+offset),behavior:reduced()?'auto':'smooth'});
70
+ }
71
+ return {
72
+ state:()=>({...state}),select,
73
+ restore(saved){for(const [name,id]of Object.entries(saved)){const entry=rows.get(name);if(entry&&mark(name,id))entry.hold=now()+holdMs;}},
74
+ refresh(){for(const entry of rows.values())entry.follow();},
75
+ dispose(){for(const dispose of disposers)dispose();disposers.length=0;rows.clear();},
76
+ };
77
+ }