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.
import { describeJaw } from './jaw-pose.js';
import type { AnatomyLayer } from './layers.js';
import type { InspectionView } from './inspection-record.js';
import { describeFingers, type FingerPose } from './finger-pose.js';
export type Pose = 'standing' | 'sitting' | 'lying' | 'walking' | 'jogging';
export type Mode = 'rotate' | 'paint' | 'pose';
export type ConcernType = 'Pain' | 'Skin change' | 'Numbness' | 'Swelling' | 'Other';
export type Point3 = [number, number, number];
export interface Mark {
part: string; point: Point3; normal: Point3; size: number; stroke: number;
surface?: { model: string; triangle: number; barycentric: Point3; uv: [number, number]; zone: string; label: string };
}
export interface Concern {
inspection?: InspectionView;
fingers?: FingerPose;
jawOpening?: number;
id: number; type: ConcernType; location: string; color: string;
layer: AnatomyLayer; marks: Mark[]; severity: number | null; onset: string; pattern: string; description: string;
triggers: string[]; pose: Pose; joints: Record<string, Point3>; localized: boolean;
timingText: string; chronic: '' | 'yes' | 'no' | 'unsure';
onsetDate: string; onsetRecordedOn: string; onsetSpeed: string; impact: string; relief: string;
}
export interface Profile { age: string; sex: string; anatomy: import('./anatomy-contract.js').AnatomyChoice; height: number; weight: number; heightConfirmed: boolean; weightConfirmed: boolean; conditions: string; medications: string; allergies: string; smoking: string; pregnancy: string; recentChanges: string }
export const colors = ['#c43c20', '#176c8e', '#784b9b', '#87601b', '#3f6855'];
export const poses: Pose[] = ['standing', 'sitting', 'lying', 'walking', 'jogging'];
export const profile: Profile = { age: '', sex: '', anatomy: 'auto', height: 172, weight: 70, heightConfirmed: false, weightConfirmed: false, conditions: '', medications: '', allergies: '', smoking: '', pregnancy: '', recentChanges: '' };
export const poseNames: Record<Pose, string> = { standing: 'Standing', sitting: 'Sitting', lying: 'Lying down', walking: 'Walking', jogging: 'Jogging' };
export function localDate(): string { const date = new Date(); return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`; }
export function makeConcern(id: number, location: string, pose: Pose, localized = true): Concern {
return { id, type: 'Pain', location, pose, localized, color: colors[(id - 1) % colors.length], layer: 'skin', marks: [], severity: null, onset: '', timingText: '', chronic: '', onsetDate: '', onsetRecordedOn: localDate(), onsetSpeed: '', impact: '', relief: '', pattern: 'Not specified', description: '', triggers: [], joints: {} };
}
export function describeRegion(part: string, point: Point3): string {
const [x, y, z] = point;
const side = part.startsWith('left') ? 'Left' : 'Right';
if (part === 'torso') {
const level = y > 0.27 ? 'chest' : y > 0.13 ? 'upper abdomen' : 'lower abdomen';
return `${Math.abs(x) < 0.055 ? 'Central' : x > 0 ? 'Left' : 'Right'} ${z < 0 ? (y > 0.25 ? 'upper back' : 'lower back') : level}`;
}
if (part === 'pelvis') return `${Math.abs(x) < 0.055 ? 'Central' : x > 0 ? 'Left' : 'Right'} ${z < 0 ? 'buttock' : 'hip / pelvis'}`;
if (part === 'head') return z < 0 ? 'Back of head' : y > 0.03 ? 'Forehead / scalp' : 'Face';
if (part === 'neck') return z < 0 ? 'Back of neck' : 'Front of neck';
if (part === 'nose') return 'Nose';
const names: Record<string, string> = { upperarm: 'upper arm', forearm: 'forearm', hand: 'hand', thigh: 'thigh', calf: 'lower leg', foot: 'foot', knee: 'knee', elbow: 'elbow', shoulder: 'shoulder' };
const name = names[part.split('-')[1]] || part;
const face = name === 'knee' ? (z >= 0 ? ' · front' : ' · back') : name === 'forearm' || name === 'thigh' ? (z >= 0 ? ' · front' : ' · back') : '';
return `${side} ${name}${face}`;
}
export function summary(c: Concern): string {
const onset = c.onsetDate ? `Started on ${c.onsetDate}. ` : c.onset ? `Started ${c.onset.toLowerCase()}. ` : 'Onset not yet provided. ';
const areas = [...new Set(c.marks.flatMap(m => m.surface ? [m.surface.label] : []))];
const mapped = areas.length > 1 ? `Approximate painted areas: ${areas.join('; ')}. ` : '';
const intensity = c.type === 'Pain' && c.severity !== null ? `Rated ${c.severity}/10. ` : '';
return `${c.type} ${c.localized ? `at ${c.location.toLowerCase()}` : `described as ${c.location.toLowerCase()}`}. ${onset}${c.timingText ? `Timing in your words: ${c.timingText}. ` : ''}${c.chronic ? `Patient-reported chronic / long-term: ${c.chronic}. ` : ''}${mapped}${c.onsetSpeed ? `${c.onsetSpeed} onset. ` : ''}${intensity}${c.pattern !== 'Not specified' ? `${c.pattern}. ` : ''}${c.triggers.length ? `Reported with ${c.triggers.join(', ').toLowerCase()}. ` : ''}${c.impact ? `Daily impact: ${c.impact}. ` : ''}${c.relief ? `What helps / tried: ${c.relief}. ` : ''}${c.localized ? `Illustrated pose: ${describePose(c.pose, c.joints, c.fingers, c.layer === 'mouth' ? c.jawOpening ?? 0 : undefined)}. Illustrated layer: ${c.layer}. ` : ''}${c.description}`;
}
export function describePose(pose: Pose, joints: Record<string, Point3>, fingers?: FingerPose, jawOpening?: number): string {
const details = [poseNames[pose].toLowerCase()];
for (const side of ['left', 'right']) {
if ((joints[`${side}-knee`]?.[0] || 0) > 0.6) details.push(`${side} knee bent`);
if (Math.abs(joints[`${side}-elbow`]?.[0] || 0) > 0.6) details.push(`${side} elbow bent`);
if (Math.abs(joints[`${side}-arm`]?.[2] || 0) > 0.5) details.push(`${side} arm away from the body`);
}
return [...details, ...describeFingers(fingers), ...(jawOpening === undefined ? [] : [describeJaw(jawOpening)])].join(', ');
}
export function escape(value: unknown): string {
return String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!));
}
