humanmap-vas 1.0.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.
- package/README.md +114 -0
- package/humanmap-vas-standalone.js +238 -0
- package/package.json +17 -0
- package/src/img/head_left.svg +7 -0
- package/src/img/head_right.svg +5 -0
- package/src/img/neck_back.svg +45 -0
- package/src/img/neck_front.svg +43 -0
- package/src/img/neck_left.svg +43 -0
- package/src/img/neck_right.svg +43 -0
- package/src/img/torax_back.svg +99 -0
- package/src/img/torax_front.svg +279 -0
package/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# 🧠 HumanMap VAS — Anatomical Interactive Mapper
|
2
|
+
|
3
|
+
**HumanMap VAS** es una librería web que permite graficar el cuerpo humano con vistas anatómicas interactivas para identificar zonas según el sistema VAS.
|
4
|
+
Desarrollada como *Web Component standalone*, puede integrarse fácilmente en proyectos **HTML**, **Django**, o **Vue.js**.
|
5
|
+
|
6
|
+
---
|
7
|
+
|
8
|
+
## 📂 Estructura del proyecto
|
9
|
+
|
10
|
+
humanmap-vas/
|
11
|
+
├── src/
|
12
|
+
│ ├── img/
|
13
|
+
│ │ ├── head_right.svg
|
14
|
+
│ │ ├── head_left.svg
|
15
|
+
│ │ ├── neck_right.svg
|
16
|
+
│ │ ├── neck_left.svg
|
17
|
+
│ │ ├── torax_front.svg
|
18
|
+
│ │ └── torax_back.svg
|
19
|
+
│
|
20
|
+
├── humanmap-vas-standalone.js
|
21
|
+
└── index.html ← demo (opcional)
|
22
|
+
|
23
|
+
|
24
|
+
---
|
25
|
+
|
26
|
+
## 🚀 Uso rápido (en HTML)
|
27
|
+
|
28
|
+
Copia los archivos o enlázalos desde GitHub Pages:
|
29
|
+
|
30
|
+
```html
|
31
|
+
<!DOCTYPE html>
|
32
|
+
<html lang="es">
|
33
|
+
<head>
|
34
|
+
<meta charset="UTF-8">
|
35
|
+
<title>HumanMap VAS Demo</title>
|
36
|
+
<script src="https://luis-byt.github.io/humanmap-vas/humanmap-vas-standalone.js"></script>
|
37
|
+
</head>
|
38
|
+
<body>
|
39
|
+
<h1>Demo de HumanMap VAS</h1>
|
40
|
+
<human-map-vas></human-map-vas>
|
41
|
+
|
42
|
+
<script>
|
43
|
+
document.querySelector('human-map-vas').addEventListener('human-map-vas:select', e => {
|
44
|
+
console.log('Zonas seleccionadas:', e.detail.selected);
|
45
|
+
});
|
46
|
+
</script>
|
47
|
+
</body>
|
48
|
+
</html>
|
49
|
+
```
|
50
|
+
---
|
51
|
+
|
52
|
+
# 🧩 Integración en Django
|
53
|
+
|
54
|
+
## Copia la carpeta humanmap-vas/ dentro de tu directorio static/
|
55
|
+
|
56
|
+
myproject/
|
57
|
+
└── static/
|
58
|
+
└── humanmap-vas/
|
59
|
+
├── humanmap-vas-standalone.js
|
60
|
+
├── src/img/...
|
61
|
+
|
62
|
+
## En tu template Django:
|
63
|
+
|
64
|
+
```
|
65
|
+
{% load static %}
|
66
|
+
<script src="{% static 'humanmap-vas/humanmap-vas-standalone.js' %}"></script>
|
67
|
+
|
68
|
+
<human-map-vas></human-map-vas>
|
69
|
+
|
70
|
+
<script>
|
71
|
+
document.querySelector('human-map-vas').addEventListener('human-map-vas:select', e => {
|
72
|
+
console.log(e.detail.selected);
|
73
|
+
});
|
74
|
+
</script>
|
75
|
+
```
|
76
|
+
|
77
|
+
## Verifica que los archivos estáticos estén habilitados:
|
78
|
+
```
|
79
|
+
python manage.py collectstatic
|
80
|
+
```
|
81
|
+
---
|
82
|
+
|
83
|
+
# ⚛️ Integración en Vue.js
|
84
|
+
|
85
|
+
## Copia humanmap-vas-standalone.js dentro de public/ (o instala desde NPM cuando esté publicado).
|
86
|
+
|
87
|
+
## En tu componente Vue:
|
88
|
+
|
89
|
+
```
|
90
|
+
<template>
|
91
|
+
<div>
|
92
|
+
<human-map-vas @human-map-vas:select="onSelect"></human-map-vas>
|
93
|
+
</div>
|
94
|
+
</template>
|
95
|
+
|
96
|
+
<script>
|
97
|
+
export default {
|
98
|
+
mounted() {
|
99
|
+
const script = document.createElement('script');
|
100
|
+
script.src = '/humanmap-vas-standalone.js';
|
101
|
+
document.head.appendChild(script);
|
102
|
+
},
|
103
|
+
methods: {
|
104
|
+
onSelect(e) {
|
105
|
+
console.log('Zonas seleccionadas:', e.detail.selected);
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
</script>
|
110
|
+
```
|
111
|
+
|
112
|
+
---
|
113
|
+
|
114
|
+
## Ahora puedes disfrutar e interactar con una [demo](https://luis-byt.github.io/humanmap-vas/) de la herramienta.
|
@@ -0,0 +1,238 @@
|
|
1
|
+
// humanmap-vas-standalone.js — Cabeza + Cuello + Tórax (2 vistas: anterior/posterior)
|
2
|
+
(function(){
|
3
|
+
const STYLE = `
|
4
|
+
:host { display:block; font:14px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Noto Sans, sans-serif; color:#111827; }
|
5
|
+
.hm { position: relative; border: 1px solid #e5e7eb; border-radius: 14px; overflow: hidden; background: #fff; }
|
6
|
+
.hm-toolbar { display:grid; grid-template-columns: auto 1fr auto; align-items:center; gap:8px; padding:8px 10px; border-bottom:1px solid #eef2f7; background:#fafafa; }
|
7
|
+
.hm-center { text-align:center; font-weight:600; color:#1f2937; }
|
8
|
+
.hm-toolbar select, .hm-toolbar button { appearance:none; border:1px solid #d1d5db; border-radius:10px; padding:6px 10px; background:#fff; cursor:pointer; font-weight:500; }
|
9
|
+
.hm-canvas-wrap { position:relative; width:100%; height:512px; margin:auto; aspect-ratio: 2/3; background:#fff; }
|
10
|
+
svg.hm-svg { position:absolute; inset:0; width:100%; height:100%; }
|
11
|
+
.zone { fill: rgba(31,41,55,0); transition: fill 120ms ease; cursor: pointer; }
|
12
|
+
.zone:hover { fill: rgba(31,41,55,0.22); }
|
13
|
+
.zone.selected { fill: rgba(31,41,55,0.36); }
|
14
|
+
.label { fill:#0a0a0a; font-size:22px; pointer-events: none; user-select: none; text-anchor: middle; dominant-baseline: middle; font-weight:700; }
|
15
|
+
`;
|
16
|
+
|
17
|
+
// ───────────────────────────────────────────────────────────────────────────
|
18
|
+
// ZONAS — Cabeza, Cuello, Tórax
|
19
|
+
const ZONES = (() => {
|
20
|
+
const cfg = {
|
21
|
+
head_right: {x0:0.10, x1:0.83, y0:0.20, y1:0.78},
|
22
|
+
head_left: {x0:0.17, x1:0.90, y0:0.20, y1:0.78},
|
23
|
+
neck_right: {x0:0.25, x1:0.75, y0:0.37, y1:0.65},
|
24
|
+
neck_left: {x0:0.25, x1:0.75, y0:0.37, y1:0.65},
|
25
|
+
thorax_front: {x0:0.10, x1:0.90, y0:0.35, y1:0.75},
|
26
|
+
thorax_back: {x0:0.10, x1:0.90, y0:0.35, y1:0.75},
|
27
|
+
};
|
28
|
+
const pad = 0.015;
|
29
|
+
|
30
|
+
function build(view, x0,x1,y0,y1, codes) {
|
31
|
+
const zones=[];
|
32
|
+
const rows=codes.length, cols=codes[0].length;
|
33
|
+
const cw=(x1-x0)/cols, ch=(y1-y0)/rows;
|
34
|
+
for(let r=0;r<rows;r++){
|
35
|
+
for(let c=0;c<cols;c++){
|
36
|
+
const x=x0+c*cw+pad*cw, y=y0+r*ch+pad*ch;
|
37
|
+
const w=cw*(1-2*pad), h=ch*(1-2*pad);
|
38
|
+
zones.push({
|
39
|
+
id:`${view}-r${r+1}c${c+1}`,
|
40
|
+
code: codes[r][c],
|
41
|
+
label: codes[r][c],
|
42
|
+
view, shape:{x,y,w,h}
|
43
|
+
});
|
44
|
+
}
|
45
|
+
}
|
46
|
+
return zones;
|
47
|
+
}
|
48
|
+
|
49
|
+
// Cabeza
|
50
|
+
const headRightCodes=[
|
51
|
+
["1.1.2.1","1.1.3.1","1.1.1.1"],
|
52
|
+
["1.1.2.2","1.1.3.2","1.1.1.2"],
|
53
|
+
["1.1.2.3","1.1.3.3","1.1.1.3"]
|
54
|
+
];
|
55
|
+
const headLeftCodes=[
|
56
|
+
["1.2.1.1","1.2.3.1","1.2.2.1"],
|
57
|
+
["1.2.1.2","1.2.3.2","1.2.2.2"],
|
58
|
+
["1.2.1.3","1.2.3.3","1.2.2.3"]
|
59
|
+
];
|
60
|
+
|
61
|
+
// Cuello
|
62
|
+
const neckRightCodes=[
|
63
|
+
["2.1.1.1","2.1.2.1"],
|
64
|
+
["2.1.1.2","2.1.2.2"]
|
65
|
+
];
|
66
|
+
const neckLeftCodes=[
|
67
|
+
["2.2.1.1","2.2.2.1"],
|
68
|
+
["2.2.1.2","2.2.2.2"]
|
69
|
+
];
|
70
|
+
|
71
|
+
// Tórax — Anterior y Posterior (4x3)
|
72
|
+
const thoraxFrontCodes = [
|
73
|
+
["3.1.1.1.2","3.1.1.1.1","3.2.1.1.1","3.2.1.1.2"],
|
74
|
+
["3.1.1.2.2","3.1.1.2.1","3.2.1.2.1","3.2.1.2.2"],
|
75
|
+
["3.1.1.3.2","3.1.1.3.1","3.2.1.3.1","3.2.1.3.2"]
|
76
|
+
];
|
77
|
+
const thoraxBackCodes = [
|
78
|
+
["3.2.2.1.2","3.2.2.1.1","3.1.2.1.1","3.1.2.1.2"],
|
79
|
+
["3.2.2.2.2","3.2.2.2.1","3.1.2.2.1","3.1.2.2.2"],
|
80
|
+
["3.2.2.3.2","3.2.2.3.1","3.1.2.3.1","3.1.2.3.2"]
|
81
|
+
];
|
82
|
+
|
83
|
+
return [
|
84
|
+
...build('head_right',...Object.values(cfg.head_right),headRightCodes),
|
85
|
+
...build('head_left',...Object.values(cfg.head_left),headLeftCodes),
|
86
|
+
...build('neck_right',...Object.values(cfg.neck_right),neckRightCodes),
|
87
|
+
...build('neck_left',...Object.values(cfg.neck_left),neckLeftCodes),
|
88
|
+
...build('thorax_front',...Object.values(cfg.thorax_front),thoraxFrontCodes),
|
89
|
+
...build('thorax_back',...Object.values(cfg.thorax_back),thoraxBackCodes)
|
90
|
+
];
|
91
|
+
})();
|
92
|
+
|
93
|
+
// Vistas
|
94
|
+
const VIEWS = [
|
95
|
+
{ id: 'head_right', label:'Cabeza — Derecha (1.1)' },
|
96
|
+
{ id: 'head_left', label:'Cabeza — Izquierda (1.2)' },
|
97
|
+
{ id: 'neck_right', label:'Cuello — Derecho (2.1)' },
|
98
|
+
{ id: 'neck_left', label:'Cuello — Izquierdo (2.2)' },
|
99
|
+
{ id: 'thorax_front', label:'Tórax — Anterior (3.1/3.2)' },
|
100
|
+
{ id: 'thorax_back', label:'Tórax — Posterior (3.1/3.2)' }
|
101
|
+
];
|
102
|
+
|
103
|
+
// Layout visual
|
104
|
+
const VIEW_LAYOUTS = {
|
105
|
+
head_right:{vb:[0,0,1024,1536], y:0, h:1536, rotate:0},
|
106
|
+
head_left:{vb:[0,0,1024,1536], y:0, h:1536, rotate:0},
|
107
|
+
neck_right:{vb:[0,0,1024,1536], y:-200, h:2048, rotate:+12},
|
108
|
+
neck_left:{vb:[0,0,1024,1536], y:-200, h:2048, rotate:-12},
|
109
|
+
thorax_front:{vb:[0,0,1024,1536], y:-100, h:2048, rotate:0},
|
110
|
+
thorax_back:{vb:[0,0,1024,1536], y:-250, h:2048, rotate:0}
|
111
|
+
};
|
112
|
+
|
113
|
+
class HumanMapVAS extends HTMLElement{
|
114
|
+
constructor(){
|
115
|
+
super();
|
116
|
+
this.attachShadow({mode:'open'});
|
117
|
+
this._view=this.getAttribute('view')||'head_right';
|
118
|
+
this._zones=ZONES;
|
119
|
+
this._selected=new Set();
|
120
|
+
this._bg={
|
121
|
+
head_right:'src/img/head_right.svg',
|
122
|
+
head_left:'src/img/head_left.svg',
|
123
|
+
neck_right:'src/img/neck_right.svg',
|
124
|
+
neck_left:'src/img/neck_left.svg',
|
125
|
+
thorax_front:'src/img/torax_front.svg',
|
126
|
+
thorax_back:'src/img/torax_back.svg'
|
127
|
+
};
|
128
|
+
}
|
129
|
+
|
130
|
+
connectedCallback(){this._renderShell();this._renderCanvas();}
|
131
|
+
static get observedAttributes(){return['view'];}
|
132
|
+
attributeChangedCallback(n,o,v){if(o!==v&&n==='view'){this._view=v;if(this._root)this._renderCanvas();}}
|
133
|
+
|
134
|
+
getSelected(){
|
135
|
+
const map=new Map(this._zones.map(z=>[z.id,z]));
|
136
|
+
return Array.from(this._selected).map(id=>{
|
137
|
+
const z=map.get(id);
|
138
|
+
return{id:z.id,code:z.code,label:z.label,view:z.view};
|
139
|
+
});
|
140
|
+
}
|
141
|
+
|
142
|
+
clear(){this._selected.clear();this._renderZones();this._emit();}
|
143
|
+
|
144
|
+
_renderShell(){
|
145
|
+
const style=document.createElement('style');style.textContent=STYLE;
|
146
|
+
this._root=document.createElement('div');this._root.className='hm';
|
147
|
+
const opts=VIEWS.map(v=>`<option value="${v.id}">${v.label}</option>`).join('');
|
148
|
+
this._root.innerHTML=`
|
149
|
+
<div class="hm-toolbar">
|
150
|
+
<button id="prev">◀</button>
|
151
|
+
<div class="hm-center"><span id="cur"></span></div>
|
152
|
+
<div style="display:flex;gap:8px;align-items:center;">
|
153
|
+
<button id="next">▶</button>
|
154
|
+
<button id="reset">🔄</button>
|
155
|
+
<select id="picker">${opts}</select>
|
156
|
+
</div>
|
157
|
+
</div>
|
158
|
+
<div class="hm-canvas-wrap">
|
159
|
+
<svg class="hm-svg" xmlns="http://www.w3.org/2000/svg">
|
160
|
+
<defs id="defs"></defs>
|
161
|
+
<g id="bg"></g>
|
162
|
+
<g id="zones"></g>
|
163
|
+
</svg>
|
164
|
+
</div>`;
|
165
|
+
this.shadowRoot.append(style,this._root);
|
166
|
+
this._els={
|
167
|
+
cur:this.shadowRoot.getElementById('cur'),
|
168
|
+
picker:this.shadowRoot.getElementById('picker'),
|
169
|
+
svg:this.shadowRoot.querySelector('svg.hm-svg'),
|
170
|
+
bg:this.shadowRoot.getElementById('bg'),
|
171
|
+
zones:this.shadowRoot.getElementById('zones'),
|
172
|
+
prev:this.shadowRoot.getElementById('prev'),
|
173
|
+
next:this.shadowRoot.getElementById('next'),
|
174
|
+
reset:this.shadowRoot.getElementById('reset')
|
175
|
+
};
|
176
|
+
this._els.picker.value=this._view;
|
177
|
+
this._els.picker.addEventListener('change',()=>this.setAttribute('view',this._els.picker.value));
|
178
|
+
this._els.prev.addEventListener('click',()=>this._cycle(-1));
|
179
|
+
this._els.next.addEventListener('click',()=>this._cycle(1));
|
180
|
+
this._els.reset.addEventListener('click',()=>this.clear());
|
181
|
+
}
|
182
|
+
|
183
|
+
_cycle(dir){
|
184
|
+
const idx=VIEWS.findIndex(v=>v.id===this._view);
|
185
|
+
const next=VIEWS[(idx+dir+VIEWS.length)%VIEWS.length];
|
186
|
+
this.setAttribute('view',next.id);
|
187
|
+
}
|
188
|
+
|
189
|
+
_renderCanvas(){
|
190
|
+
const layout=VIEW_LAYOUTS[this._view];
|
191
|
+
const v=VIEWS.find(v=>v.id===this._view);
|
192
|
+
if(!layout||!v)return;
|
193
|
+
this._els.cur.textContent=v.label;
|
194
|
+
this._els.picker.value=v.id;
|
195
|
+
this._els.svg.setAttribute('viewBox',layout.vb.join(' '));
|
196
|
+
const url=this._bg[v.id];
|
197
|
+
const[vx,vy,vw,vh]=layout.vb;
|
198
|
+
this._els.bg.innerHTML=`<image href="${url}" x="0" y="${layout.y}" width="${vw}" height="${layout.h}" preserveAspectRatio="xMidYMid meet"/>`;
|
199
|
+
this._renderZones();
|
200
|
+
}
|
201
|
+
|
202
|
+
_renderZones(){
|
203
|
+
const g=this._els.zones;g.innerHTML='';
|
204
|
+
const layout=VIEW_LAYOUTS[this._view];
|
205
|
+
const[vx,vy,vw,vh]=layout.vb;
|
206
|
+
const Z=this._zones.filter(z=>z.view===this._view);
|
207
|
+
if(this._view==='neck_right')g.setAttribute('transform',`rotate(12,${vw*0.55},${vh*0.45})`);
|
208
|
+
else if(this._view==='neck_left')g.setAttribute('transform',`rotate(-12,${vw*0.45},${vh*0.45})`);
|
209
|
+
else g.removeAttribute('transform');
|
210
|
+
Z.forEach(z=>{
|
211
|
+
const{x,y,w,h}=z.shape;
|
212
|
+
const rect=document.createElementNS('http://www.w3.org/2000/svg','rect');
|
213
|
+
rect.setAttribute('x',x*vw);rect.setAttribute('y',y*vh);
|
214
|
+
rect.setAttribute('width',w*vw);rect.setAttribute('height',h*vh);
|
215
|
+
rect.setAttribute('rx',Math.min(w*vw,h*vh)*0.1);
|
216
|
+
rect.classList.add('zone');
|
217
|
+
if(this._selected.has(z.id))rect.classList.add('selected');
|
218
|
+
rect.addEventListener('click',e=>{
|
219
|
+
e.stopPropagation();
|
220
|
+
if(this._selected.has(z.id))this._selected.delete(z.id);
|
221
|
+
else this._selected.add(z.id);
|
222
|
+
this._renderZones();this._emit();
|
223
|
+
});
|
224
|
+
g.appendChild(rect);
|
225
|
+
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
|
226
|
+
t.setAttribute('x',(x+w/2)*vw);
|
227
|
+
t.setAttribute('y',(y+h/2)*vh);
|
228
|
+
t.textContent=z.code;
|
229
|
+
t.setAttribute('class','label');
|
230
|
+
g.appendChild(t);
|
231
|
+
});
|
232
|
+
}
|
233
|
+
|
234
|
+
_emit(){this.dispatchEvent(new CustomEvent('human-map-vas:select',{detail:{selected:this.getSelected()}}));}
|
235
|
+
}
|
236
|
+
|
237
|
+
customElements.define('human-map-vas',HumanMapVAS);
|
238
|
+
})();
|
package/package.json
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
{
|
2
|
+
"name": "humanmap-vas",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "**HumanMap VAS** es una librería web que permite graficar el cuerpo humano con vistas anatómicas interactivas para identificar zonas según el sistema VAS. Desarrollada como *Web Component standalone*, puede integrarse fácilmente en proyectos **HTML**, **Django**, o **Vue.js**.",
|
5
|
+
"main": "humanmap-vas-standalone.js",
|
6
|
+
"files": [
|
7
|
+
"humanmap-vas-standalone.js",
|
8
|
+
"src/img/"
|
9
|
+
],
|
10
|
+
"repository": {
|
11
|
+
"type": "git",
|
12
|
+
"url": "git+https://github.com/luis-byt/humanmap-vas.git"
|
13
|
+
},
|
14
|
+
"keywords": ["anatomy", "visualization", "vas", "medical", "webcomponent"],
|
15
|
+
"author": "Luis A. Quintana",
|
16
|
+
"license": "MIT"
|
17
|
+
}
|