humanmap-vas 1.0.22 → 1.0.23

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.
@@ -6,12 +6,18 @@
6
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
7
  .hm-center { text-align:center; font-weight:600; color:#1f2937; }
8
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; }
9
+ .hm-canvas-wrap { position:relative; width:100%; height:600px; margin:auto; aspect-ratio: 2/3; background:#fff; }
10
10
  svg.hm-svg { position:absolute; inset:0; width:100%; height:100%; }
11
11
  .zone { fill: rgba(31,41,55,0); transition: fill 120ms ease; cursor: pointer; }
12
12
  .zone:hover { fill: rgba(31,41,55,0.22); }
13
+ .zone.readonly { cursor: default; }
14
+ .zone.readonly:not(.selected):hover { fill: rgba(31,41,55,0); }
13
15
  .zone.selected { fill: rgba(31,41,55,0.36); }
16
+ .hm-all-label { fill: #111827; font-weight: 700; font-size: 36px; text-anchor: middle; dominant-baseline: middle; }
14
17
  .label { fill:#0a0a0a; font-size:36px; pointer-events: none; user-select: none; text-anchor: middle; dominant-baseline: middle; font-weight:800; }
18
+ .hm-print-btn { position: absolute; top: 10px; right: 10px; background: rgba(17,24,39,0.85); color: #f9fafb; border: none; border-radius: 8px; cursor: pointer; font-size: 18px; padding: 6px 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); transition: opacity 0.25s ease, background 0.2s ease; opacity: 0; pointer-events: none; }
19
+ .hm-canvas-wrap:hover .hm-print-btn { opacity: 1; pointer-events: auto; }
20
+ .hm-print-btn:hover { background: rgba(37,99,235,0.9); }
15
21
  `;
16
22
 
17
23
  // ───────────────────────────────────────────────────────────────────────────
@@ -117,6 +123,7 @@
117
123
  this._view=this.getAttribute('view') || 'head_right';
118
124
  this._zones=ZONES;
119
125
  this._selected=new Set();
126
+ this._readOnly = this.hasAttribute('read-only') && this.getAttribute('read-only') !== 'false';
120
127
 
121
128
  this._upgradeProperty('selectedIds');
122
129
  this._upgradeProperty('selectedZones');
@@ -161,9 +168,16 @@
161
168
  }
162
169
  }
163
170
 
164
- connectedCallback(){this._renderShell();this._renderCanvas();this.dispatchEvent(new CustomEvent('human-map-vas:ready'));}
171
+ connectedCallback(){
172
+ this._renderShell();
173
+ this._renderCanvas();
174
+ this.dispatchEvent(new CustomEvent('human-map-vas:ready'));
175
+ this.dispatchEvent(new CustomEvent('human-map-vas:readonly-change', {
176
+ detail: { readOnly: this._readOnly }
177
+ }));
178
+ }
165
179
 
166
- static get observedAttributes() { return ['view', 'img-root']; }
180
+ static get observedAttributes() { return ['view', 'img-root', 'read-only']; }
167
181
 
168
182
  attributeChangedCallback (name, oldValue, newValue) {
169
183
  if (oldValue === newValue) return;
@@ -187,6 +201,34 @@
187
201
  if (this._root) this._renderCanvas();
188
202
  return;
189
203
  }
204
+
205
+ if (name === 'read-only') {
206
+ this._readOnly = newValue === 'true' || newValue === true;
207
+
208
+ if (this._root) {
209
+ // Refrescar la vista completamente según el modo
210
+ if (this._view === 'all') {
211
+ this._renderAllViews();
212
+ } else {
213
+ this._renderCanvas();
214
+ }
215
+
216
+ // Actualizar la visibilidad de toolbar y botón de impresión
217
+ const toolbar = this._root.querySelector('.hm-toolbar');
218
+ const printBtn = this.shadowRoot.getElementById('printBtn');
219
+
220
+ if (this._view === 'all' && this._readOnly) {
221
+ if (toolbar) toolbar.style.display = 'none';
222
+ if (printBtn) printBtn.style.display = 'block';
223
+ } else {
224
+ if (toolbar) toolbar.style.display = '';
225
+ if (printBtn) printBtn.style.display = 'none';
226
+ }
227
+ }
228
+
229
+ return;
230
+ }
231
+
190
232
  }
191
233
 
192
234
  // Devuelve solo IDs seleccionados
@@ -198,7 +240,13 @@
198
240
  set selectedIds(ids) {
199
241
  if (!Array.isArray(ids)) return;
200
242
  this._selected = new Set(ids);
201
- if (this._root) { this._renderZones(); this._emit(); }
243
+
244
+ if (this._root) {
245
+ // Si está en modo global, renderizamos todas las vistas
246
+ if (this._view === 'all') this._renderAllViews();
247
+ else this._renderZones();
248
+ this._emit();
249
+ }
202
250
  }
203
251
 
204
252
  // Devuelve objetos completos (id, code, label, view)
@@ -242,7 +290,11 @@
242
290
  this._root=document.createElement('div');
243
291
  this._root.className='hm';
244
292
 
245
- const opts=VIEWS.map(v=>`<option value="${v.id}">${v.label}</option>`).join('');
293
+ const opts = [
294
+ `<option value="all">Todas las vistas</option>`,
295
+ ...VIEWS.map(v => `<option value="${v.id}">${v.label}</option>`)
296
+ ].join('');
297
+
246
298
  this._root.innerHTML=`
247
299
  <div class="hm-toolbar">
248
300
  <button id="prev">◀</button>
@@ -259,6 +311,7 @@
259
311
  <g id="bg"></g>
260
312
  <g id="zones"></g>
261
313
  </svg>
314
+ <button id="printBtn" title="Imprimir vista" class="hm-print-btn">🖨️</button>
262
315
  </div>`;
263
316
  this.shadowRoot.append(style,this._root);
264
317
 
@@ -277,6 +330,15 @@
277
330
  this._els.prev.addEventListener('click',()=>this._cycle(-1));
278
331
  this._els.next.addEventListener('click',()=>this._cycle(1));
279
332
  this._els.reset.addEventListener('click',()=>this.clear());
333
+ this._els.printBtn = this.shadowRoot.getElementById('printBtn');
334
+ this._els.printBtn.addEventListener('click', () => {
335
+ this._els.printBtn.style.transform = 'scale(0.94)';
336
+ setTimeout(() => {
337
+ this._els.printBtn.style.transform = '';
338
+ this._printCanvasOnly(); // ⬅️ imprime solo el área actual
339
+ }, 120);
340
+ });
341
+
280
342
  }
281
343
 
282
344
  _cycle(dir){
@@ -286,8 +348,55 @@
286
348
  }
287
349
 
288
350
  _renderCanvas(){
289
- const layout=VIEW_LAYOUTS[this._view];
290
- const v=VIEWS.find(v=>v.id===this._view);
351
+ // Ocultar toolbar si está en modo global y solo lectura
352
+ if (this._els && this._root) {
353
+ const toolbar = this._root.querySelector('.hm-toolbar');
354
+ if (this._view === 'all' && this._readOnly) {
355
+ toolbar.style.display = 'none';
356
+ } else {
357
+ toolbar.style.display = '';
358
+ }
359
+ }
360
+
361
+ // Mostrar botón de impresión solo en modo global + solo lectura
362
+ const printBtn = this.shadowRoot.getElementById('printBtn');
363
+ if (printBtn) {
364
+ if (this._view === 'all' && this._readOnly) {
365
+ printBtn.style.display = 'block';
366
+ } else {
367
+ printBtn.style.display = 'none';
368
+ }
369
+ }
370
+
371
+ // Limpia cualquier render anterior (modo all o vista única)
372
+ if (this._els && this._els.svg) {
373
+ this._els.svg.innerHTML = `
374
+ <defs id="defs"></defs>
375
+ <g id="bg"></g>
376
+ <g id="zones"></g>
377
+ `;
378
+ this._els.bg = this._els.svg.querySelector('#bg');
379
+ this._els.zones = this._els.svg.querySelector('#zones');
380
+ }
381
+
382
+ // 🆕 Detección de modo global
383
+ if (this._view === 'all') {
384
+ this._renderAllViews();
385
+ this._els.cur.textContent = 'Todas las vistas';
386
+ this._els.picker.value = 'all';
387
+ this._els.prev.disabled = true;
388
+ this._els.next.disabled = true;
389
+ this._els.reset.disabled = this._readOnly;
390
+ return;
391
+ } else {
392
+ this._els.prev.disabled = false;
393
+ this._els.next.disabled = false;
394
+ this._els.reset.disabled = false;
395
+ }
396
+
397
+ // --- Render normal ---
398
+ const layout = VIEW_LAYOUTS[this._view];
399
+ const v = VIEWS.find(v => v.id === this._view);
291
400
  if(!layout||!v)return;
292
401
  this._els.cur.textContent=v.label;
293
402
  this._els.picker.value=v.id;
@@ -298,6 +407,112 @@
298
407
  this._renderZones();
299
408
  }
300
409
 
410
+ _renderAllViews() {
411
+ const svg = this._els.svg;
412
+ svg.innerHTML = ''; // limpiar
413
+
414
+ // Creamos un grupo por cada vista
415
+ const cols = 2, gap = 40;
416
+ const cellW = 480, cellH = 720;
417
+ const gridW = cols * (cellW + gap);
418
+ const gridH = Math.ceil(VIEWS.length / cols) * (cellH + gap);
419
+
420
+ svg.setAttribute('viewBox', `0 0 ${gridW} ${gridH}`);
421
+ const gRoot = document.createElementNS('http://www.w3.org/2000/svg', 'g');
422
+ svg.appendChild(gRoot);
423
+
424
+ VIEWS.forEach((v, i) => {
425
+ const layout = VIEW_LAYOUTS[v.id] || { vb: [0, 0, 1024, 1536], y: 0, h: 1536, rotate: 0 };
426
+ const row = Math.floor(i / cols);
427
+ const col = i % cols;
428
+ const gx = col * (cellW + gap);
429
+ const gy = row * (cellH + gap);
430
+
431
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
432
+ g.setAttribute('transform', `translate(${gx}, ${gy}) scale(0.45)`);
433
+
434
+ // Fondo (imagen)
435
+ const img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
436
+ img.setAttribute('href', this._bg[v.id]);
437
+ img.setAttribute('x', '0');
438
+ img.setAttribute('y', layout?.y || 0);
439
+ img.setAttribute('width', layout?.vb[2] || 1024);
440
+ img.setAttribute('height', layout?.h || 1536);
441
+ img.setAttribute('preserveAspectRatio', 'xMidYMid meet');
442
+ g.appendChild(img);
443
+
444
+ // Grupo para las zonas (permite rotar sin afectar la imagen)
445
+ const gZones = document.createElementNS('http://www.w3.org/2000/svg', 'g');
446
+
447
+ // Rotar solo las zonas del cuello
448
+ let rotation = 0;
449
+ if (v.id === 'neck_right') rotation = 12;
450
+ if (v.id === 'neck_left') rotation = -12;
451
+ if (rotation !== 0) {
452
+ gZones.setAttribute(
453
+ 'transform',
454
+ `rotate(${rotation}, ${layout.vb[2]*0.55}, ${layout.vb[3]*0.45})`
455
+ );
456
+ }
457
+
458
+ // --- Dibujar zonas ---
459
+ const zones = this._zones.filter(z => z.view === v.id);
460
+ zones.forEach(z => {
461
+ const { x, y, w, h } = z.shape;
462
+
463
+ // Zona rectangular
464
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
465
+ rect.setAttribute('x', x * layout.vb[2]);
466
+ rect.setAttribute('y', y * layout.vb[3]);
467
+ rect.setAttribute('width', w * layout.vb[2]);
468
+ rect.setAttribute('height', h * layout.vb[3]);
469
+ rect.setAttribute('rx', Math.min(w, h) * layout.vb[2] * 0.1);
470
+ rect.classList.add('zone');
471
+ if (this._selected.has(z.id)) rect.classList.add('selected');
472
+ if (this._readOnly) {
473
+ rect.classList.add('readonly');
474
+ } else {
475
+ rect.addEventListener('click', e => {
476
+ e.stopPropagation();
477
+ if (this._selected.has(z.id)) this._selected.delete(z.id);
478
+ else this._selected.add(z.id);
479
+ this._renderAllViews(); // refrescar vista global
480
+ this._emit();
481
+ });
482
+ }
483
+ gZones.appendChild(rect);
484
+
485
+ // Label centrado
486
+ const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
487
+ t.textContent = z.code;
488
+ t.setAttribute('x', (x + w / 2) * layout.vb[2]);
489
+ t.setAttribute('y', (y + h / 2) * layout.vb[3]);
490
+ t.setAttribute('fill', '#0a0a0a');
491
+ t.setAttribute('font-size', '32');
492
+ t.setAttribute('font-weight', '700');
493
+ t.setAttribute('text-anchor', 'middle');
494
+ t.setAttribute('dominant-baseline', 'middle');
495
+ t.setAttribute('pointer-events', 'none');
496
+ gZones.appendChild(t);
497
+ });
498
+
499
+ // Añadir grupo de zonas dentro del grupo principal
500
+ g.appendChild(gZones);
501
+
502
+
503
+ // Label de la vista
504
+ const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
505
+ t.textContent = v.label;
506
+ t.setAttribute('x', layout.vb[2] / 2);
507
+ t.setAttribute('y', 80);
508
+ t.setAttribute('class', 'hm-all-label');
509
+ g.appendChild(t);
510
+
511
+
512
+ gRoot.appendChild(g);
513
+ });
514
+ }
515
+
301
516
  _renderZones(){
302
517
  const g=this._els.zones;g.innerHTML='';
303
518
  const layout=VIEW_LAYOUTS[this._view];
@@ -309,19 +524,34 @@
309
524
  Z.forEach(z=>{
310
525
  const{x,y,w,h}=z.shape;
311
526
  const rect=document.createElementNS('http://www.w3.org/2000/svg','rect');
527
+
312
528
  rect.setAttribute('x',x*vw);rect.setAttribute('y',y*vh);
313
529
  rect.setAttribute('width',w*vw);rect.setAttribute('height',h*vh);
314
530
  rect.setAttribute('rx',Math.min(w*vw,h*vh)*0.1);
531
+
315
532
  rect.classList.add('zone');
316
- if(this._selected.has(z.id))rect.classList.add('selected');
317
- rect.addEventListener('click',e=>{
318
- e.stopPropagation();
319
- if(this._selected.has(z.id))this._selected.delete(z.id);
320
- else this._selected.add(z.id);
321
- this._renderZones();this._emit();
322
- });
533
+
534
+ if (this._selected.has(z.id)) rect.classList.add('selected');
535
+
536
+ // Si está en modo lectura, añadir clase visual
537
+ if (this._readOnly) {
538
+ rect.classList.add('readonly');
539
+ } else {
540
+ rect.classList.remove('readonly');
541
+ rect.addEventListener('click', e => {
542
+ e.stopPropagation();
543
+ if (this._selected.has(z.id)) this._selected.delete(z.id);
544
+ else this._selected.add(z.id);
545
+ this._renderZones();
546
+ this._emit();
547
+ });
548
+ }
549
+
550
+
323
551
  g.appendChild(rect);
552
+
324
553
  const t=document.createElementNS('http://www.w3.org/2000/svg','text');
554
+
325
555
  t.setAttribute('x',(x+w/2)*vw);
326
556
  t.setAttribute('y',(y+h/2)*vh);
327
557
  t.textContent=z.code;
@@ -330,6 +560,112 @@
330
560
  });
331
561
  }
332
562
 
563
+ // Imprime solo el área visible (hm-canvas-wrap) en una nueva ventana
564
+ _printCanvasOnly() {
565
+ if (!this._els || !this._els.svg) return;
566
+
567
+ // Clonar el SVG actual completo (vistas individuales o "all")
568
+ const clone = this._els.svg.cloneNode(true);
569
+
570
+ // Asegurar rutas absolutas en <image> (importante para que se muestren)
571
+ clone.querySelectorAll('image').forEach(img => {
572
+ const href = img.getAttribute('href') || img.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
573
+ if (href) {
574
+ const a = document.createElement('a');
575
+ a.href = href;
576
+ const abs = a.href;
577
+ img.setAttribute('href', abs);
578
+ img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', abs);
579
+ }
580
+ });
581
+
582
+ // Inyectar los estilos básicos directamente en el SVG (para conservar colores y transparencias)
583
+ const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
584
+ style.textContent = `
585
+ .zone { fill: rgba(31,41,55,0); cursor: pointer; transition: fill 120ms ease; }
586
+ .zone:hover { fill: rgba(31,41,55,0.22); }
587
+ .zone.selected { fill: rgba(31,41,55,0.36); }
588
+ .label { fill: #0a0a0a; font-size: 36px; font-weight: 800;
589
+ text-anchor: middle; dominant-baseline: middle; pointer-events: none; user-select: none; }
590
+ `;
591
+ clone.insertBefore(style, clone.firstChild);
592
+
593
+ const serializer = new XMLSerializer();
594
+ const svgMarkup = serializer.serializeToString(clone);
595
+
596
+ const isGlobal = this._view === 'all';
597
+
598
+ // HTML limpio con estilos mínimos
599
+ const html = `
600
+ <!DOCTYPE html>
601
+ <html lang="es">
602
+ <head>
603
+ <meta charset="utf-8">
604
+ <title>Impresión del mapa anatómico</title>
605
+ <style>
606
+ html, body {
607
+ margin: 0;
608
+ padding: 0mm;
609
+ background: #fff;
610
+ text-align: center;
611
+ }
612
+ svg {
613
+ width: 100%;
614
+ height: auto;
615
+ display: block;
616
+ }
617
+ .hm-all-label {
618
+ font-family: system-ui, sans-serif;
619
+ font-size: 48px;
620
+ font-weight: 800;
621
+ fill: #111827;
622
+ text-anchor: middle;
623
+ dominant-baseline: middle;
624
+ }
625
+ @page {
626
+ size: ${isGlobal ? 'A4 landscape' : 'A4 portrait'};
627
+ margin: 5mm;
628
+ }
629
+ </style>
630
+ </head>
631
+ <body>
632
+ ${svgMarkup}
633
+ </body>
634
+ </html>
635
+ `;
636
+
637
+ // Abrir ventana y escribir el HTML
638
+ const printWin = window.open('', '_blank', 'width=1024,height=768');
639
+ if (!printWin) {
640
+ alert('El navegador bloqueó la ventana de impresión. Permite popups para continuar.');
641
+ return;
642
+ }
643
+
644
+ printWin.document.open();
645
+ printWin.document.write(html);
646
+ printWin.document.close();
647
+
648
+ // Esperar hasta que las imágenes del SVG estén listas
649
+ const waitForImages = () => {
650
+ const imgs = printWin.document.querySelectorAll('image');
651
+ const promises = Array.from(imgs).map(img => {
652
+ return new Promise(resolve => {
653
+ const test = new Image();
654
+ test.onload = test.onerror = resolve;
655
+ test.src = img.getAttribute('href') || img.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
656
+ });
657
+ });
658
+ return Promise.all(promises);
659
+ };
660
+
661
+ waitForImages().then(() => {
662
+ printWin.focus();
663
+ printWin.print();
664
+ setTimeout(() => printWin.close(), 1000);
665
+ });
666
+ }
667
+
668
+
333
669
  _emit(){this.dispatchEvent(new CustomEvent('human-map-vas:select',{detail:{selected:this.getSelected()}}));}
334
670
  }
335
671
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanmap-vas",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
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
5
  "main": "humanmap-vas-standalone.js",
6
6
  "files": [