@jaak.ai/stamps 2.0.0 → 2.1.0-dev.10

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 (71) hide show
  1. package/README.md +11 -1
  2. package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
  3. package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
  4. package/dist/cjs/{index-BfhtOB0D.js → index-Ga0t6BMe.js} +176 -119
  5. package/dist/cjs/index-Ga0t6BMe.js.map +1 -0
  6. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +5 -4
  7. package/dist/cjs/jaak-stamps-webcomponent.cjs.js.map +1 -1
  8. package/dist/cjs/jaak-stamps.cjs.entry.js +1174 -655
  9. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  10. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  11. package/dist/cjs/loader.cjs.js +4 -3
  12. package/dist/cjs/loader.cjs.js.map +1 -1
  13. package/dist/collection/collection-manifest.json +1 -1
  14. package/dist/collection/components/my-component/my-component.css +224 -13
  15. package/dist/collection/components/my-component/my-component.js +950 -281
  16. package/dist/collection/components/my-component/my-component.js.map +1 -1
  17. package/dist/collection/services/CameraService.js +156 -344
  18. package/dist/collection/services/CameraService.js.map +1 -1
  19. package/dist/collection/services/DetectionService.js +100 -42
  20. package/dist/collection/services/DetectionService.js.map +1 -1
  21. package/dist/collection/services/EventBusService.js +1 -1
  22. package/dist/collection/services/EventBusService.js.map +1 -1
  23. package/dist/collection/services/ServiceContainer.js +4 -13
  24. package/dist/collection/services/ServiceContainer.js.map +1 -1
  25. package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
  26. package/dist/collection/types/component-types.js.map +1 -1
  27. package/dist/components/index.js +165 -113
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/jaak-stamps.js +1182 -658
  30. package/dist/components/jaak-stamps.js.map +1 -1
  31. package/dist/esm/app-globals-DQuL1Twl.js +6 -0
  32. package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
  33. package/dist/esm/{index-BP1Q4KOg.js → index-Drbzcuq-.js} +177 -119
  34. package/dist/esm/index-Drbzcuq-.js.map +1 -0
  35. package/dist/esm/jaak-stamps-webcomponent.js +5 -4
  36. package/dist/esm/jaak-stamps-webcomponent.js.map +1 -1
  37. package/dist/esm/jaak-stamps.entry.js +1174 -655
  38. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  39. package/dist/esm/loader.js +4 -3
  40. package/dist/esm/loader.js.map +1 -1
  41. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  42. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js.map +1 -1
  43. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  44. package/dist/jaak-stamps-webcomponent/loader.esm.js.map +1 -1
  45. package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js +2 -0
  46. package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js.map +1 -0
  47. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js +2 -0
  48. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js.map +1 -0
  49. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js +3 -0
  50. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js.map +1 -0
  51. package/dist/types/components/my-component/my-component.d.ts +87 -17
  52. package/dist/types/components.d.ts +14 -8
  53. package/dist/types/services/CameraService.d.ts +12 -14
  54. package/dist/types/services/DetectionService.d.ts +10 -3
  55. package/dist/types/services/ServiceContainer.d.ts +1 -2
  56. package/dist/types/services/interfaces/ICameraService.d.ts +3 -12
  57. package/dist/types/stencil-public-runtime.d.ts +12 -5
  58. package/dist/types/types/component-types.d.ts +3 -0
  59. package/package.json +4 -2
  60. package/dist/cjs/index-BfhtOB0D.js.map +0 -1
  61. package/dist/collection/services/LoggerService.js +0 -40
  62. package/dist/collection/services/LoggerService.js.map +0 -1
  63. package/dist/collection/services/interfaces/ILogger.js +0 -2
  64. package/dist/collection/services/interfaces/ILogger.js.map +0 -1
  65. package/dist/esm/index-BP1Q4KOg.js.map +0 -1
  66. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js +0 -2
  67. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js.map +0 -1
  68. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js +0 -3
  69. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js.map +0 -1
  70. package/dist/types/services/LoggerService.d.ts +0 -12
  71. package/dist/types/services/interfaces/ILogger.d.ts +0 -8
@@ -7,6 +7,7 @@ export class JaakStamps {
7
7
  maskSize = 90;
8
8
  cropMargin = 20;
9
9
  useDocumentClassification = false;
10
+ useDocumentDetector = true;
10
11
  preferredCamera = 'auto';
11
12
  captureDelay = 1500;
12
13
  enableBackDocumentTimer = false;
@@ -23,9 +24,16 @@ export class JaakStamps {
23
24
  showCameraSelector = false;
24
25
  isSwitchingCamera = false;
25
26
  hasDocumentDetected = false;
27
+ cameraInfoWithAutofocus = {
28
+ availableCameras: [],
29
+ selectedCameraId: null,
30
+ deviceType: 'desktop',
31
+ isMultipleCamerasAvailable: false,
32
+ preferredFacing: null
33
+ };
26
34
  currentStatus = {
27
- message: 'Inicializando componente...',
28
- description: 'Configurando servicios y cargando recursos',
35
+ message: 'Preparando capturador...',
36
+ description: 'Configurando herramientas de captura',
29
37
  type: 'initializing',
30
38
  isInitialized: false
31
39
  };
@@ -40,9 +48,13 @@ export class JaakStamps {
40
48
  detectionRate: 0
41
49
  };
42
50
  backDocumentTimerRemaining = 0;
51
+ showManualCaptureButton = false;
52
+ performanceDegradedMode = false; // Auto-switched to manual mode due to performance
53
+ showPerformanceMessage = false; // Show performance message temporarily
54
+ captureStateVersion = 0; // Triggers re-render when StateManager changes
55
+ processingButton = null; // Track which button is processing
43
56
  // Services
44
57
  serviceContainer;
45
- logger;
46
58
  eventBus;
47
59
  stateManager;
48
60
  cameraService;
@@ -59,12 +71,12 @@ export class JaakStamps {
59
71
  alignmentStartTime;
60
72
  alignmentTimer;
61
73
  backDocumentTimer;
74
+ orientationTimer;
62
75
  // Performance monitoring
63
76
  performanceMetrics = {
64
77
  fps: 0,
65
78
  inferenceTime: 0,
66
79
  memoryUsage: 0,
67
- cpuUsage: 0,
68
80
  onnxLoadTime: 0,
69
81
  frameProcessingTime: 0,
70
82
  totalDetections: 0,
@@ -73,22 +85,39 @@ export class JaakStamps {
73
85
  lastUpdateTime: 0
74
86
  };
75
87
  performanceUpdateInterval;
76
- // Performance optimization
88
+ // Performance optimization with adaptive frame skipping
77
89
  frameSkipCounter = 0;
78
- FRAME_SKIP = 2;
90
+ BASE_FRAME_SKIP = 2;
91
+ MAX_FRAME_SKIP = 8;
79
92
  consecutiveFailures = 0;
80
93
  MAX_FAILURES = 30;
81
94
  lastInferenceTime = 0;
82
95
  MIN_INFERENCE_INTERVAL = 50;
96
+ performanceHistory = [];
97
+ PERFORMANCE_HISTORY_SIZE = 10;
98
+ // Sistema simplificado de monitoreo de rendimiento
99
+ PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
100
+ SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
101
+ slowFrameCount = 0; // Contador de frames consecutivos lentos
102
+ processedFramesCount = 0; // Contador total de frames procesados
103
+ hasAutoSwitchedToManual = false;
104
+ // Canvas pool for optimized screenshot capture
105
+ canvasPool = [];
106
+ MAX_CANVAS_POOL_SIZE = 3;
83
107
  async componentDidLoad() {
84
- this.updateStatus('Iniciando servicios...', 'Configurando módulos internos', 'initializing');
85
- await this.initializeServices();
86
- this.updateStatus('Configurando eventos...', 'Preparando comunicación entre servicios', 'initializing');
87
- await this.setupEventListeners();
88
- this.updateStatus('Inicializando cámara...', 'Detectando dispositivos disponibles', 'initializing');
89
- await this.initializeComponent();
90
- if (this.debug) {
91
- this.initializePerformanceMonitor();
108
+ try {
109
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
110
+ await this.initializeServices();
111
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
112
+ await this.setupEventListeners();
113
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
114
+ await this.initializeComponent();
115
+ if (this.debug) {
116
+ this.initializePerformanceMonitor();
117
+ }
118
+ }
119
+ catch (error) {
120
+ this.updateStatus('Error de inicialización', error.message, 'error');
92
121
  }
93
122
  }
94
123
  async initializeServices() {
@@ -98,37 +127,22 @@ export class JaakStamps {
98
127
  maskSize: this.maskSize,
99
128
  cropMargin: this.cropMargin,
100
129
  useDocumentClassification: this.useDocumentClassification,
130
+ useDocumentDetector: this.useDocumentDetector,
101
131
  preferredCamera: this.preferredCamera,
102
132
  captureDelay: this.captureDelay
103
133
  };
104
134
  this.serviceContainer = new ServiceContainer(config);
105
- this.logger = this.serviceContainer.getLogger();
106
135
  this.eventBus = this.serviceContainer.getEventBus();
107
136
  this.stateManager = this.serviceContainer.getStateManager();
108
137
  this.cameraService = this.serviceContainer.getCameraService();
109
138
  this.detectionService = this.serviceContainer.getDetectionService();
110
- this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
111
139
  }
112
140
  async setupEventListeners() {
113
- this.eventBus.on('state-changed', (data) => {
114
- this.handleStateChange(data);
115
- });
116
- this.eventBus.on('camera-changed', (cameraId) => {
117
- this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
118
- });
119
- this.eventBus.on('error', (error) => {
120
- this.logger.error('Error en servicio:', error);
141
+ this.eventBus.on('state-changed', () => {
142
+ this.handleStateChange();
121
143
  });
122
144
  }
123
145
  async initializeComponent() {
124
- this.logger.state('COMPONENTE_INICIALIZANDO', {
125
- debug: this.debug,
126
- maskSize: this.maskSize,
127
- cropMargin: this.cropMargin,
128
- useDocumentClassification: this.useDocumentClassification,
129
- preferredCamera: this.preferredCamera,
130
- captureDelay: this.captureDelay
131
- });
132
146
  this.validateProps();
133
147
  if (this.debug) {
134
148
  this.stateManager.updateCaptureState({ isLoading: true });
@@ -137,58 +151,67 @@ export class JaakStamps {
137
151
  this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
138
152
  await this.cameraService.detectDeviceType();
139
153
  await this.cameraService.enumerateDevices();
140
- this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
154
+ await this.updateCameraInfoWithAutofocus();
155
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
141
156
  await this.loadOnnxRuntime();
142
157
  this.initializeResizeObserver();
143
158
  }
144
159
  validateProps() {
145
160
  if (this.maskSize < 50 || this.maskSize > 100) {
146
- this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
147
161
  this.maskSize = 90;
148
162
  }
149
163
  if (this.cropMargin < 0 || this.cropMargin > 100) {
150
- this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
151
164
  this.cropMargin = 0;
152
165
  }
153
166
  const validOptions = ['auto', 'front', 'back'];
154
167
  if (!validOptions.includes(this.preferredCamera)) {
155
- this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
156
168
  this.preferredCamera = 'auto';
157
169
  }
158
170
  if (this.captureDelay < 0 || this.captureDelay > 10000) {
159
- this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
160
171
  this.captureDelay = 1500;
161
172
  }
162
173
  }
163
174
  async loadOnnxRuntime() {
164
175
  if (!window.ort) {
165
- this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
176
+ this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
166
177
  const script = document.createElement('script');
167
178
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
168
179
  document.head.appendChild(script);
169
180
  await new Promise((resolve) => {
170
181
  script.onload = () => {
171
- setTimeout(() => {
172
- this.finalizeInitialization();
182
+ setTimeout(async () => {
183
+ await this.finalizeInitialization();
173
184
  resolve(undefined);
174
185
  }, 300);
175
186
  };
176
187
  });
177
188
  }
178
189
  else {
179
- setTimeout(() => {
180
- this.finalizeInitialization();
190
+ setTimeout(async () => {
191
+ await this.finalizeInitialization();
181
192
  }, 300);
182
193
  }
183
194
  }
184
- finalizeInitialization() {
195
+ async finalizeInitialization() {
185
196
  this.stateManager.updateCaptureState({ isLoading: false });
186
- this.currentStatus = {
187
- message: 'Listo para capturar',
188
- description: '',
189
- type: 'ready',
190
- isInitialized: true
191
- };
197
+ // Check camera permissions before showing "ready" status
198
+ const hasPermissions = await this.checkCameraPermissions();
199
+ if (hasPermissions) {
200
+ this.currentStatus = {
201
+ message: 'Listo para capturar',
202
+ description: '',
203
+ type: 'ready',
204
+ isInitialized: true
205
+ };
206
+ }
207
+ else {
208
+ this.currentStatus = {
209
+ message: 'Permisos de cámara requeridos',
210
+ description: 'Por favor, otorgue permisos de cámara para continuar',
211
+ type: 'error',
212
+ isInitialized: true
213
+ };
214
+ }
192
215
  this.emitReadyEvent();
193
216
  }
194
217
  updateStatus(message, description, type = 'loading') {
@@ -202,20 +225,30 @@ export class JaakStamps {
202
225
  emitReadyEvent() {
203
226
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
204
227
  this.isReady.emit(isDocumentReady);
205
- this.logger.state('COMPONENTE_LISTO', {
206
- ortLibraryLoaded: !!window.ort,
207
- modelPreloaded: this.detectionService.isModelLoaded(),
208
- isReady: isDocumentReady
209
- });
210
228
  }
211
- handleStateChange(data) {
212
- // React to state changes from StateManager
213
- // This is where the component updates its internal state based on service state changes
214
- if (data.changes.isLoading !== undefined) {
215
- // Force re-render when loading state changes
216
- // Note: Stencil automatically re-renders when @State changes
229
+ async checkCameraPermissions() {
230
+ try {
231
+ if (!navigator.permissions) {
232
+ // Fallback: try to access camera directly
233
+ try {
234
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
235
+ stream.getTracks().forEach(track => track.stop());
236
+ return true;
237
+ }
238
+ catch {
239
+ return false;
240
+ }
241
+ }
242
+ const permission = await navigator.permissions.query({ name: 'camera' });
243
+ return permission.state === 'granted';
244
+ }
245
+ catch (error) {
246
+ return false;
217
247
  }
218
248
  }
249
+ handleStateChange() {
250
+ // Handle state changes from StateManager if needed
251
+ }
219
252
  initializeResizeObserver() {
220
253
  if ('ResizeObserver' in window && this.detectionContainer) {
221
254
  const resizeObserver = new ResizeObserver(() => {
@@ -223,15 +256,56 @@ export class JaakStamps {
223
256
  });
224
257
  resizeObserver.observe(this.detectionContainer.parentElement);
225
258
  }
259
+ // Handle mobile device orientation changes
260
+ window.addEventListener('orientationchange', () => {
261
+ // Clear any existing orientation timer
262
+ if (this.orientationTimer) {
263
+ clearTimeout(this.orientationTimer);
264
+ }
265
+ // Add delay to ensure video dimensions are updated after rotation
266
+ this.orientationTimer = window.setTimeout(() => {
267
+ this.handleResize();
268
+ this.orientationTimer = undefined;
269
+ }, 300);
270
+ });
226
271
  }
227
272
  handleResize() {
228
273
  if (this.detectionContainer) {
229
274
  const container = this.detectionContainer.parentElement;
230
275
  const rect = container.getBoundingClientRect();
231
276
  this.updateMaskDimensions(rect);
232
- this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
233
277
  }
234
278
  }
279
+ triggerRerender() {
280
+ this.captureStateVersion = this.captureStateVersion + 1;
281
+ }
282
+ getGuideText(step) {
283
+ if (step === 'completed') {
284
+ return 'Proceso completado';
285
+ }
286
+ // Check if we should show performance message
287
+ if (this.showPerformanceMessage && step === 'front') {
288
+ return 'Modo manual activado por rendimiento. Use el botón para capturar.';
289
+ }
290
+ if (step === 'front') {
291
+ // Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
292
+ if (this.showManualCaptureButton) {
293
+ return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
294
+ }
295
+ // Instrucción básica de alineación
296
+ return 'Alinee su identificación con el marco';
297
+ }
298
+ else if (step === 'back') {
299
+ // Para el reverso, mostrar instrucciones específicas según el modo
300
+ if (this.useDocumentDetector) {
301
+ return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
302
+ }
303
+ else {
304
+ return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
305
+ }
306
+ }
307
+ return 'Alinee su identificación con el marco';
308
+ }
235
309
  updateMaskDimensions(containerRect) {
236
310
  if (!this.videoRef)
237
311
  return;
@@ -283,16 +357,38 @@ export class JaakStamps {
283
357
  this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
284
358
  this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
285
359
  this.isMaskReady = true;
286
- this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
287
- video: { width: videoWidth, height: videoHeight },
288
- displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
289
- mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
290
- center: { x: videoCenterXPercent, y: videoCenterYPercent },
291
- offset: { x: videoOffsetX, y: videoOffsetY }
292
- });
293
360
  }
294
361
  // PUBLIC METHODS
362
+ // Helper method to check if component is ready for operations
363
+ isComponentReady() {
364
+ if (!this.currentStatus.isInitialized) {
365
+ return {
366
+ ready: false,
367
+ message: 'El componente aún se está inicializando',
368
+ status: 'initializing'
369
+ };
370
+ }
371
+ if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
372
+ return {
373
+ ready: false,
374
+ message: 'El componente está cargando recursos',
375
+ status: 'loading'
376
+ };
377
+ }
378
+ if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
379
+ return {
380
+ ready: false,
381
+ message: 'Los servicios del componente no están disponibles',
382
+ status: 'error'
383
+ };
384
+ }
385
+ return { ready: true };
386
+ }
295
387
  async getCapturedImages() {
388
+ const readyCheck = this.isComponentReady();
389
+ if (!readyCheck.ready) {
390
+ throw new Error(readyCheck.message);
391
+ }
296
392
  const state = this.stateManager.getCaptureState();
297
393
  if (state.step !== 'completed') {
298
394
  throw new Error('El proceso de captura no ha sido completado');
@@ -300,146 +396,359 @@ export class JaakStamps {
300
396
  return this.stateManager.getCapturedImages();
301
397
  }
302
398
  async isProcessCompleted() {
399
+ const readyCheck = this.isComponentReady();
400
+ if (!readyCheck.ready) {
401
+ return false; // Return false instead of error for this method
402
+ }
303
403
  return this.stateManager.isProcessCompleted();
304
404
  }
305
405
  async startCapture() {
306
- await this.startDetection();
406
+ const readyCheck = this.isComponentReady();
407
+ if (!readyCheck.ready) {
408
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
409
+ return { success: false, error: readyCheck.message };
410
+ }
411
+ try {
412
+ await this.startDetection();
413
+ return { success: true };
414
+ }
415
+ catch (error) {
416
+ this.updateStatus('Error al iniciar captura', error.message, 'error');
417
+ return { success: false, error: error.message };
418
+ }
307
419
  }
308
420
  async stopCapture() {
309
- this.exitSession();
421
+ const readyCheck = this.isComponentReady();
422
+ if (!readyCheck.ready) {
423
+ return { success: false, error: readyCheck.message };
424
+ }
425
+ try {
426
+ this.exitSession();
427
+ return { success: true };
428
+ }
429
+ catch (error) {
430
+ return { success: false, error: error.message };
431
+ }
310
432
  }
311
433
  async resetCapture() {
312
- this.resetDetection();
434
+ const readyCheck = this.isComponentReady();
435
+ if (!readyCheck.ready) {
436
+ return { success: false, error: readyCheck.message };
437
+ }
438
+ try {
439
+ this.resetDetection();
440
+ return { success: true };
441
+ }
442
+ catch (error) {
443
+ return { success: false, error: error.message };
444
+ }
313
445
  }
314
446
  async skipBackCapture() {
315
- const captureState = this.stateManager.getCaptureState();
316
- const capturedImages = this.stateManager.getCapturedImages();
317
- if (captureState.step === 'back' && capturedImages.front.fullFrame) {
318
- this.completeProcess(true);
447
+ console.log('🟡 Botón clicked: Saltar reverso');
448
+ // Set processing state immediately
449
+ this.processingButton = 'skip-back';
450
+ // Add small delay to show spinner
451
+ await new Promise(resolve => setTimeout(resolve, 100));
452
+ const readyCheck = this.isComponentReady();
453
+ if (!readyCheck.ready) {
454
+ this.processingButton = null;
455
+ return { success: false, error: readyCheck.message };
456
+ }
457
+ try {
458
+ const captureState = this.stateManager.getCaptureState();
459
+ const capturedImages = this.stateManager.getCapturedImages();
460
+ if (captureState.step === 'back' && capturedImages.front.fullFrame) {
461
+ this.completeProcess(true);
462
+ return { success: true };
463
+ }
464
+ else {
465
+ return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
466
+ }
467
+ }
468
+ catch (error) {
469
+ return { success: false, error: error.message };
470
+ }
471
+ finally {
472
+ // Always clear processing state when done
473
+ this.processingButton = null;
319
474
  }
320
475
  }
321
476
  async getStatus() {
322
- const captureState = this.stateManager.getCaptureState();
323
- const capturedImages = this.stateManager.getCapturedImages();
324
- return {
325
- isVideoActive: captureState.isVideoActive,
326
- captureStep: captureState.step,
327
- hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
328
- isProcessCompleted: this.stateManager.isProcessCompleted(),
329
- isModelPreloaded: this.detectionService.isModelLoaded()
330
- };
477
+ const readyCheck = this.isComponentReady();
478
+ if (!readyCheck.ready) {
479
+ return {
480
+ isVideoActive: false,
481
+ captureStep: 'front',
482
+ hasImages: false,
483
+ isProcessCompleted: false,
484
+ isModelPreloaded: false,
485
+ componentStatus: readyCheck.status,
486
+ componentMessage: readyCheck.message
487
+ };
488
+ }
489
+ try {
490
+ const captureState = this.stateManager.getCaptureState();
491
+ const capturedImages = this.stateManager.getCapturedImages();
492
+ return {
493
+ isVideoActive: captureState.isVideoActive,
494
+ captureStep: captureState.step,
495
+ hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
496
+ isProcessCompleted: this.stateManager.isProcessCompleted(),
497
+ isModelPreloaded: this.detectionService.isModelLoaded(),
498
+ componentStatus: 'ready'
499
+ };
500
+ }
501
+ catch (error) {
502
+ return {
503
+ isVideoActive: false,
504
+ captureStep: 'front',
505
+ hasImages: false,
506
+ isProcessCompleted: false,
507
+ isModelPreloaded: false,
508
+ componentStatus: 'error',
509
+ componentMessage: error.message
510
+ };
511
+ }
331
512
  }
332
513
  async preloadModel() {
514
+ const readyCheck = this.isComponentReady();
515
+ if (!readyCheck.ready) {
516
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
517
+ return { success: false, error: readyCheck.message };
518
+ }
333
519
  if (this.detectionService.isModelLoaded()) {
334
- this.logger.state('MODELO_YA_PRECARGADO');
335
- this.updateStatus('Modelos ya cargados', '', 'ready');
520
+ this.updateStatus('Reconocimiento listo', '', 'ready');
336
521
  return { success: true, message: 'Model already loaded' };
337
522
  }
338
523
  try {
339
524
  const loadStartTime = performance.now();
340
- this.updateStatus('Descargando modelo de detección...', 'Obteniendo red neuronal para reconocimiento de documentos', 'loading');
341
- this.stateManager.updateCaptureState({ isLoading: true });
342
- await this.detectionService.loadModel();
343
- this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de tipos de documento', 'loading');
344
- await this.detectionService.loadClassificationModel();
525
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
526
+ if (this.stateManager) {
527
+ this.stateManager.updateCaptureState({ isLoading: true });
528
+ }
529
+ try {
530
+ await this.detectionService.loadModel();
531
+ }
532
+ catch (modelError) {
533
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
534
+ throw modelError;
535
+ }
536
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
537
+ try {
538
+ await this.detectionService.loadClassificationModel();
539
+ }
540
+ catch (classificationError) {
541
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
542
+ throw classificationError;
543
+ }
345
544
  const loadEndTime = performance.now();
346
545
  const loadTime = loadEndTime - loadStartTime;
347
546
  // Record ONNX load time
348
547
  if (this.debug) {
349
548
  this.recordOnnxPerformance(loadTime, 0);
350
549
  }
351
- this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
550
+ this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
352
551
  await new Promise(resolve => setTimeout(resolve, 300));
353
- this.updateStatus('Modelos precargados', '', 'ready');
354
- this.stateManager.updateCaptureState({ isLoading: false });
552
+ this.updateStatus('Reconocimiento preparado', '', 'ready');
553
+ if (this.stateManager) {
554
+ this.stateManager.updateCaptureState({ isLoading: false });
555
+ }
355
556
  this.emitReadyEvent();
356
- this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
357
557
  return { success: true, message: 'Models preloaded successfully' };
358
558
  }
359
559
  catch (error) {
360
- this.logger.error('Error al precargar modelos:', error);
361
- this.updateStatus('Error al cargar modelos', 'No se pudieron descargar los recursos necesarios', 'error');
362
- this.stateManager.updateCaptureState({ isLoading: false });
560
+ this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
561
+ if (this.stateManager) {
562
+ this.stateManager.updateCaptureState({ isLoading: false });
563
+ }
363
564
  return { success: false, error: error.message };
364
565
  }
365
566
  }
366
567
  async getCameraInfo() {
367
- return this.cameraService.getCameraInfo();
568
+ const readyCheck = this.isComponentReady();
569
+ if (!readyCheck.ready) {
570
+ return {
571
+ availableCameras: [],
572
+ selectedCameraId: null,
573
+ deviceType: 'desktop',
574
+ isMultipleCamerasAvailable: false,
575
+ preferredFacing: null,
576
+ error: readyCheck.message
577
+ };
578
+ }
579
+ try {
580
+ return await this.cameraService.getCameraInfo();
581
+ }
582
+ catch (error) {
583
+ return {
584
+ availableCameras: [],
585
+ selectedCameraId: null,
586
+ deviceType: 'desktop',
587
+ isMultipleCamerasAvailable: false,
588
+ preferredFacing: null,
589
+ error: error.message
590
+ };
591
+ }
368
592
  }
369
593
  async setPreferredCamera(camera) {
370
- this.preferredCamera = camera;
371
- this.serviceContainer.updateConfig({ preferredCamera: camera });
372
- await this.cameraService.enumerateDevices();
373
- const captureState = this.stateManager.getCaptureState();
374
- if (captureState.isVideoActive) {
375
- const selectedCameraId = this.cameraService.getSelectedCameraId();
376
- if (selectedCameraId) {
377
- await this.cameraService.switchCamera(selectedCameraId);
594
+ const readyCheck = this.isComponentReady();
595
+ if (!readyCheck.ready) {
596
+ return {
597
+ success: false,
598
+ error: readyCheck.message,
599
+ selectedCamera: null,
600
+ availableCameras: 0
601
+ };
602
+ }
603
+ try {
604
+ this.preferredCamera = camera;
605
+ this.serviceContainer.updateConfig({ preferredCamera: camera });
606
+ await this.cameraService.enumerateDevices();
607
+ await this.updateCameraInfoWithAutofocus();
608
+ const captureState = this.stateManager.getCaptureState();
609
+ if (captureState.isVideoActive) {
610
+ const selectedCameraId = this.cameraService.getSelectedCameraId();
611
+ if (selectedCameraId) {
612
+ await this.cameraService.switchCamera(selectedCameraId);
613
+ }
378
614
  }
615
+ return {
616
+ success: true,
617
+ selectedCamera: this.cameraService.getSelectedCameraId(),
618
+ availableCameras: this.cameraService.getAvailableCameras().length
619
+ };
620
+ }
621
+ catch (error) {
622
+ return {
623
+ success: false,
624
+ error: error.message,
625
+ selectedCamera: null,
626
+ availableCameras: 0
627
+ };
379
628
  }
380
- return {
381
- success: true,
382
- selectedCamera: this.cameraService.getSelectedCameraId(),
383
- availableCameras: this.cameraService.getAvailableCameras().length
384
- };
385
629
  }
386
630
  async setCaptureDelay(delay) {
387
631
  if (delay < 0 || delay > 10000) {
388
- throw new Error('Capture delay must be between 0 and 10000 milliseconds');
632
+ return {
633
+ success: false,
634
+ error: 'Capture delay must be between 0 and 10000 milliseconds',
635
+ captureDelay: this.captureDelay
636
+ };
389
637
  }
638
+ // Allow setting capture delay even during initialization
390
639
  this.captureDelay = delay;
391
- this.serviceContainer.updateConfig({ captureDelay: delay });
640
+ if (this.serviceContainer) {
641
+ this.serviceContainer.updateConfig({ captureDelay: delay });
642
+ }
392
643
  return {
393
644
  success: true,
394
645
  captureDelay: this.captureDelay
395
646
  };
396
647
  }
397
648
  async getCaptureDelay() {
649
+ // Always allow getting capture delay, even during initialization
398
650
  return this.captureDelay;
399
651
  }
400
- async setTorchEnabled(enabled) {
401
- const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
402
- return {
403
- success: torchEnabled,
404
- enabled: torchEnabled ? enabled : false
405
- };
406
- }
407
- async focusAtPoint(x, y) {
408
- const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
652
+ // Memory and device capability detection
653
+ checkDeviceCapabilities() {
654
+ const deviceMemory = navigator.deviceMemory;
655
+ // Siempre intentar usar ML, independientemente del dispositivo
656
+ // Las restricciones anteriores no eran confiables
409
657
  return {
410
- success: focused,
411
- coordinates: { x, y }
658
+ canUseML: true,
659
+ reason: 'Dispositivo compatible con detección automática.',
660
+ memoryMB: deviceMemory ? deviceMemory * 1024 : null
412
661
  };
413
662
  }
414
663
  // DETECTION METHODS
415
664
  async startDetection() {
416
- this.logger.state('INICIANDO_DETECCION');
417
665
  try {
666
+ // Verificar capacidades del dispositivo antes de cargar modelos
667
+ const capabilities = this.checkDeviceCapabilities();
668
+ // Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
669
+ if (!capabilities.canUseML && this.useDocumentDetector) {
670
+ this.switchToManualModeWithWarning(capabilities.reason);
671
+ return;
672
+ }
418
673
  // Paso 1: Verificar y cargar modelos si es necesario
419
674
  if (!this.detectionService.isModelLoaded()) {
420
675
  const loadStartTime = performance.now();
421
- this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
676
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
422
677
  this.stateManager.updateCaptureState({ isLoading: true });
423
- await this.detectionService.loadModel();
424
- this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de documentos', 'loading');
425
- await this.detectionService.loadClassificationModel();
678
+ try {
679
+ await this.detectionService.loadModel();
680
+ }
681
+ catch (modelError) {
682
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
683
+ if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
684
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
685
+ this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
686
+ return;
687
+ }
688
+ else {
689
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
690
+ throw modelError;
691
+ }
692
+ }
693
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
694
+ try {
695
+ await this.detectionService.loadClassificationModel();
696
+ }
697
+ catch (classificationError) {
698
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
699
+ if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
700
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
701
+ this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
702
+ return;
703
+ }
704
+ else {
705
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
706
+ throw classificationError;
707
+ }
708
+ }
426
709
  const loadEndTime = performance.now();
427
710
  const loadTime = loadEndTime - loadStartTime;
428
711
  // Record ONNX load time
429
712
  if (this.debug) {
430
713
  this.recordOnnxPerformance(loadTime, 0);
431
714
  }
432
- this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
433
715
  }
434
716
  // Paso 2: Detectar y configurar dispositivos
435
- this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
436
- await this.cameraService.enumerateDevices();
717
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
718
+ try {
719
+ await this.cameraService.enumerateDevices();
720
+ }
721
+ catch (enumerateError) {
722
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
723
+ }
724
+ try {
725
+ await this.updateCameraInfoWithAutofocus();
726
+ }
727
+ catch (cameraInfoError) {
728
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
729
+ }
437
730
  // Paso 3: Configurar cámara seleccionada
438
- this.updateStatus('Configurando cámara...', 'Estableciendo resolución y parámetros óptimos', 'loading');
439
- const stream = await this.cameraService.setupCamera();
440
- // Paso 4: Inicializar video y ocultar estado inmediatamente
441
- this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
442
- await this.initializeVideoStream(stream);
731
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
732
+ let stream;
733
+ try {
734
+ stream = await this.cameraService.setupCamera();
735
+ }
736
+ catch (setupError) {
737
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
738
+ }
739
+ // Paso 4: Inicializar video y mostrar estado de análisis inicial
740
+ if (this.useDocumentDetector) {
741
+ this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
742
+ }
743
+ else {
744
+ this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
745
+ }
746
+ try {
747
+ await this.initializeVideoStream(stream);
748
+ }
749
+ catch (videoError) {
750
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
751
+ }
443
752
  // Paso 5: Calibrar máscara
444
753
  if (this.detectionContainer) {
445
754
  const container = this.detectionContainer.parentElement;
@@ -449,11 +758,14 @@ export class JaakStamps {
449
758
  // Finalizar e iniciar captura
450
759
  this.startTime = Date.now();
451
760
  this.stateManager.updateCaptureState({ isLoading: false });
761
+ // Mostrar botón manual si el detector está deshabilitado
762
+ if (!this.useDocumentDetector) {
763
+ this.showManualCaptureButton = true;
764
+ }
452
765
  this.detectFrame();
453
766
  }
454
767
  catch (err) {
455
- this.logger.error('Error al inicializar detección:', err);
456
- this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
768
+ this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
457
769
  this.stateManager.updateCaptureState({ isLoading: false });
458
770
  }
459
771
  }
@@ -463,14 +775,16 @@ export class JaakStamps {
463
775
  this.videoStream = stream;
464
776
  const isRear = this.cameraService.isRearCamera(stream);
465
777
  this.shouldMirrorVideo = !isRear;
466
- this.logger.state('CAMARA_CONFIGURADA', {
467
- isRearCamera: isRear,
468
- shouldMirrorVideo: this.shouldMirrorVideo
469
- });
470
778
  return new Promise((resolve) => {
471
779
  this.videoRef.onloadedmetadata = async () => {
472
780
  await this.videoRef.play();
473
781
  this.stateManager.updateCaptureState({ isVideoActive: true });
782
+ // Recalculate mask dimensions when new camera stream loads
783
+ if (this.detectionContainer) {
784
+ const container = this.detectionContainer.parentElement;
785
+ const rect = container.getBoundingClientRect();
786
+ this.updateMaskDimensions(rect);
787
+ }
474
788
  resolve();
475
789
  };
476
790
  });
@@ -485,15 +799,28 @@ export class JaakStamps {
485
799
  const captureState = this.stateManager.getCaptureState();
486
800
  if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
487
801
  return;
802
+ // Show manual capture button when document detector is disabled or performance is degraded
803
+ if (!this.useDocumentDetector || this.performanceDegradedMode) {
804
+ this.showManualCaptureButton = true;
805
+ // Continue the loop for UI updates but skip detection logic
806
+ if (captureState.step !== 'completed') {
807
+ this.animationId = requestAnimationFrame(() => this.detectFrame());
808
+ }
809
+ return;
810
+ }
811
+ else if (!this.performanceDegradedMode) {
812
+ this.showManualCaptureButton = false;
813
+ }
488
814
  if (captureState.isDetectionPaused) {
489
815
  if (captureState.step !== 'completed') {
490
816
  this.animationId = requestAnimationFrame(() => this.detectFrame());
491
817
  }
492
818
  return;
493
819
  }
494
- // Frame skipping for performance
820
+ // Adaptive frame skipping for performance
495
821
  this.frameSkipCounter++;
496
- if (this.frameSkipCounter <= this.FRAME_SKIP) {
822
+ const currentFrameSkip = this.getAdaptiveFrameSkip();
823
+ if (this.frameSkipCounter <= currentFrameSkip) {
497
824
  if (captureState.step !== 'completed') {
498
825
  this.animationId = requestAnimationFrame(() => this.detectFrame());
499
826
  }
@@ -511,10 +838,27 @@ export class JaakStamps {
511
838
  this.lastInferenceTime = currentTime;
512
839
  // Measure preprocessing and inference time
513
840
  const inferenceStartTime = performance.now();
514
- const inputTensor = this.detectionService.preprocess(this.videoRef);
515
- // Standard detection without quality validation
516
- const detections = await this.detectionService.runInference(inputTensor);
517
- const inferenceTime = performance.now() - inferenceStartTime;
841
+ let inputTensor;
842
+ let detections;
843
+ let inferenceTime;
844
+ try {
845
+ inputTensor = this.detectionService.preprocess(this.videoRef);
846
+ }
847
+ catch (preprocessError) {
848
+ this.switchToManualModeWithError('Error al procesar imagen');
849
+ return;
850
+ }
851
+ try {
852
+ // Standard detection without timeout (timeout is handled by frame counting)
853
+ detections = await this.detectionService.runInference(inputTensor);
854
+ inferenceTime = performance.now() - inferenceStartTime;
855
+ }
856
+ catch (inferenceError) {
857
+ this.switchToManualModeWithError('Error en detección automática');
858
+ return;
859
+ }
860
+ // Increment frame count
861
+ this.processedFramesCount++;
518
862
  // Record ONNX performance metrics
519
863
  if (this.debug) {
520
864
  this.recordOnnxPerformance(0, inferenceTime);
@@ -553,9 +897,30 @@ export class JaakStamps {
553
897
  }
554
898
  this.updateMaskColor(detections);
555
899
  // Record frame processing metrics
900
+ const frameEndTime = performance.now();
901
+ const totalFrameTime = frameEndTime - frameStartTime;
902
+ // Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
903
+ if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
904
+ // Verificar si el frame es lento
905
+ if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
906
+ this.slowFrameCount++;
907
+ // Si tenemos suficientes frames lentos consecutivos, cambiar a manual
908
+ if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
909
+ this.switchToManualMode();
910
+ }
911
+ }
912
+ else {
913
+ // Frame rápido, reiniciar contador
914
+ if (this.slowFrameCount > 0) {
915
+ this.slowFrameCount = 0;
916
+ }
917
+ }
918
+ // Cambiar mensaje de estado después de analizar los primeros 2 frames
919
+ if (this.processedFramesCount === 2) {
920
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
921
+ }
922
+ }
556
923
  if (this.debug) {
557
- const frameEndTime = performance.now();
558
- const totalFrameTime = frameEndTime - frameStartTime;
559
924
  this.recordFrameProcessing(totalFrameTime, detections.length);
560
925
  }
561
926
  // Continue detection loop
@@ -569,7 +934,6 @@ export class JaakStamps {
569
934
  }
570
935
  }
571
936
  catch (e) {
572
- this.logger.error('Error en inferencia de modelo:', e);
573
937
  const captureState = this.stateManager.getCaptureState();
574
938
  if (captureState.step !== 'completed') {
575
939
  setTimeout(() => this.detectFrame(), 100);
@@ -592,10 +956,18 @@ export class JaakStamps {
592
956
  clearTimeout(this.alignmentTimer);
593
957
  this.alignmentTimer = undefined;
594
958
  }
959
+ if (this.orientationTimer) {
960
+ clearTimeout(this.orientationTimer);
961
+ this.orientationTimer = undefined;
962
+ }
963
+ // Liberar recursos de ONNX al limpiar el componente
964
+ this.releaseOnnxResources();
595
965
  if (this.performanceUpdateInterval) {
596
966
  clearInterval(this.performanceUpdateInterval);
597
967
  this.performanceUpdateInterval = undefined;
598
968
  }
969
+ // Clear canvas pool
970
+ this.canvasPool = [];
599
971
  this.detectionBoxes = [];
600
972
  this.alignmentStartTime = undefined;
601
973
  this.hasDocumentDetected = false;
@@ -610,14 +982,8 @@ export class JaakStamps {
610
982
  step: 'front',
611
983
  isCapturing: false
612
984
  };
613
- const cameraInfo = this.cameraService?.getCameraInfo() || {
614
- availableCameras: [],
615
- isMultipleCamerasAvailable: false,
616
- selectedCameraId: null,
617
- deviceType: 'desktop',
618
- preferredFacing: null
619
- };
620
- return (h("div", { key: '9d4d77042218ab3d0bc4fb1ce879db09dc3e78b4', class: "detector-container" }, h("div", { key: '51ac8ccb1275637282b038ad8364d19f14ef61b1', class: "video-container" }, h("video", { key: 'a34f0481da20ec5b7b62b21aa275ec5f6dfec5d1', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '43b71dfe3eaf20ffa66a4139e24b1af6a2074631', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
985
+ const cameraInfo = this.cameraInfoWithAutofocus;
986
+ return (h("div", { key: 'dda3e3054a18dac59fd6dab4bf722f405d1c1b0f', class: "detector-container" }, h("div", { key: 'c260bf52307e072a634cddcc273c1f55037d01ef', class: "video-container" }, h("video", { key: '5f20ce9d95c5ece7ab90a2da30f28034b86a57a0', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '6afd8a46c16592c67e6bb93f1655de1660338f30', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
621
987
  position: 'absolute',
622
988
  left: `${box.x}px`,
623
989
  top: `${box.y}px`,
@@ -626,9 +992,9 @@ export class JaakStamps {
626
992
  border: '2px solid #32406C',
627
993
  pointerEvents: 'none',
628
994
  boxSizing: 'border-box'
629
- } })))), this.isMaskReady && (h("div", { key: 'c2596f8cabb227e3ea3b082495f52b141cb1690f', class: "overlay-mask" }, h("div", { key: 'ba1ea39b5df1cd312a9bd9410b7225a29cfb8a29', class: "card-outline" }, h("div", { key: '6ce4d4d969d5f3fbcf75a4eb285f61d434c5633b', class: "side side-top" }), h("div", { key: '36653ecc4fcda25dfc84bc4d1f4c4c5dfb71ced7', class: "side side-right" }), h("div", { key: '8b108fa8fb370b229a0d1a77cb1ff044319a8f75', class: "side side-bottom" }), h("div", { key: 'a7aa3b45c065a50445aafd5c3ccefb57b0c9c769', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'b94b92273e12a019238e11ae57f21e140f13c3f1', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '144c3741003ec0cc148bb6ce92012d7b3cd110c8', class: "skip-section" }, h("div", { key: 'f0a984a6b75afa374c5d47da9d53e39f98071da0', class: "skip-explanation" }, "Si tu documento no tiene lado trasero, da clic en el bot\u00F3n para continuar con el proceso"), h("button", { key: '57ac30d776e30709aab6e0a621cef889da3cc677', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
995
+ } })))), this.isMaskReady && (h("div", { key: '178f2022bed334f8c9c8c866a92b44c8cb0369b1', class: "overlay-mask" }, h("div", { key: 'acb1bc996409095afd29ffa5a038c3eeae6c7ed9', class: "card-outline" }, h("div", { key: 'dcc952d9011c5fd13b229f3a759cc74721a1ee11', class: "side side-top" }), h("div", { key: 'addb25f6c41ad10a09b6918e6357052294d14adf', class: "side side-right" }), h("div", { key: 'a1ec08f3bf1b16731a224c034c76dd8c6412597b', class: "side side-bottom" }), h("div", { key: '22f3c158f2c2bb1dc582f796c39ea60bc11505dd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '5b150f7df5b0c5402e42bae4601936d4e5d1f5d4', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '17b0a81437eb8691cb4dc5042c72bda01ac92be0', class: "back-capture-section" }, h("div", { key: 'a65ccbd5f53b06ec340b36a0a2348cf04920b3df', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode) && (h("button", { key: '383d7398079dbb8b1adfe695407cc598059b2a94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (h("span", { key: '86bddcfeb6f90da9b18926f4c6e1208296502dcf', class: "button-spinner" })), "Capturar Reverso")), h("button", { key: '5c9d116cbb9b5bc16481b2b90c8451182ad436c5', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (h("span", { key: '881f00cfc3a24167bc6b8da5849cb404101b692d', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
630
996
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
631
- : 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: 'bd45628707e0169317ed3dd4247e1056d7046104', class: "camera-controls" }, h("button", { key: 'e750fbf4d5d76d9e9596823bb2b86801ef19e485', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: '40ada780a8ea13aeeaee9cb39bc6496f69f2679d', class: "camera-selector-dropdown" }, h("div", { key: '3a920bb02ba024f0359513f10544bf0528ecee6d', class: "camera-selector-header" }, h("span", { key: 'e5efd478a602b13821ba53cb9a7a59d1b4a71178' }, "Seleccionar C\u00E1mara"), h("button", { key: '2df6cc11e162be74862029e33815927e3fa8f2f8', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '06934642dff134c9e1a6872f935a5892a9c7b835', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '8d5ede287c2efb0ebe418bee8dc7fb310443c991', class: "device-info" }, h("small", { key: 'b7dd10565113694b09f01ab28ffa29262ae1d2bc' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '2f9e549e6c0c56d4db5835e58361fb17bba6cf90', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'a6ef0ed127f23e9d764896ed743ca8d8454851a6', class: "flip-animation" }, h("div", { key: '0b04411f88a634bb3243b00ca2d5a45fd6962dea', class: "id-card-icon" }), h("div", { key: '9327be58879b407cea87f455989406e58302e9d1', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: 'bd268578b5003e99256e3340b4a9a6fb7656be64', class: "success-animation" }, h("div", { key: '064e0a5b9851de14bc6829f028e7ea7161fd1633', class: "check-icon" }), h("div", { key: 'bdcd94a8806399e396e218a359c10b11d05f38d6', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '3d9af617376c16e44e065873919c3d90ff46110a', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: 'be7b56decfd6f12dcca46b2a9456480e3d5ac00e', class: "status-spinner" })), h("div", { key: '5fe9850883cb749cebaa18c3e9c13ca21325e373', class: "status-content" }, h("div", { key: '43fdb50691e4b98e56126af1a129f625061128bf', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'b76cbfa4a0349421fcd535d3fd8bf40c89ca3aef', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'eddefb464af0487ab9ab2a0f76dccbb6a2092e51', class: "performance-monitor" }, h("div", { key: '7b7d93b56cb4cd4fbb1bdde89c6150b282fa805e', class: "performance-expanded" }, h("div", { key: 'f302e8b9d985fb8cad53c4c4e0d37a3bf5a63261', class: "metrics-row" }, h("div", { key: 'e8d8f2420ee8643a7f454d38065c75b5be598f77', class: "metric-compact" }, h("span", { key: '6e52eb5bb06e0df57e61774de38c31349f45e22b', class: "metric-label" }, "FPS"), h("span", { key: '6636f9cea20c49b49c17db86188e2f6b214c45c3', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'afa1d9dec05b14256a1b1fcec8638b8c21a56e37', class: "metric-compact" }, h("span", { key: '9c55e30f5d01582648bacc1fb38ca1990485a591', class: "metric-label" }, "MEM"), h("span", { key: 'ca263601e8715027f95ffb2d5413a643dc570cf2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '03caf19961fd01b7781484d49d8121b1e78c161f', class: "metrics-row" }, h("div", { key: 'bffb08f01f703411f7c8605e4f8d95b1c37b5c0f', class: "metric-compact" }, h("span", { key: '9b92279b3253cc669412733facb3751f4344d69f', class: "metric-label" }, "INF"), h("span", { key: '3234d4150e2e1479e3b552767af8123e69911d6c', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '77082d2669db53f19fb50b3bbfa1ebefb0b86ff4', class: "metric-compact" }, h("span", { key: 'cc6ef96c72c800ba36dab2a388f1de863b2dacd5', class: "metric-label" }, "FRAME"), h("span", { key: 'e2bb4b7351869a76e810f8bbf20d805154495e35', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '5700d7ef839fa77686299d79445203090dc8079f', class: "metrics-row" }, h("div", { key: 'a2999a035fccb7f250c01b084075490678513c87', class: "metric-compact" }, h("span", { key: '3677d58665996fa5969507dbaff9e956b2ea54ee', class: "metric-label" }, "DET"), h("span", { key: 'c584312438b339c82a3bdc33f00fb985ff2a6f55', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '58866219f213bf8607083f30be2d5ca5d0515cea', class: "metric-compact" }, h("span", { key: 'd90b450051331347f219b6c0f197536d0dcee9b7', class: "metric-label" }, "RATE"), h("span", { key: 'f6e6cd7948e06014e5a0416e4e81394fa36ec61d', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: 'c1d0194f181a0082cc1aa5452de382908da61b65', class: "watermark" }, h("img", { key: 'e04d35f3c79a2b4a3922ab5b0f102220171fa7c0', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
997
+ : 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: 'd929fc0eeeeee944f3d722fde98c52642443e9a5', class: "camera-controls" }, h("button", { key: '1afcfae9d4a23aaede153f27ba6895a33182ada1', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: 'f37feefd9def935417467dffb52b9fe8d8acea79', class: "camera-selector-dropdown" }, h("div", { key: 'a8479ec6a6c4076b5dc40677e1988295100e4d0d', class: "camera-selector-header" }, h("span", { key: '074f02967354293a1e5df4e3ac8a126faee33d04' }, "Seleccionar C\u00E1mara"), h("button", { key: 'd6ba0252fb269c3a04967fb0e5c075166f202d71', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '63f5f9be2b05da71f4fe5a4e38b11ab3e39fb1e0', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '4ca46fdebe0eaa078f1dae4d2f1af03321577c7f', class: "device-info" }, h("small", { key: '10c54ddc5a196b11a69077b045119187ed09f4be' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '25393018948798f4d9d5f44738e61c7a0a6be990', class: "manual-capture-section" }, h("button", { key: '7a6c8686c0db3e352f9689bc41f7fd3f612c800c', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (h("span", { key: '1916b97da3f8dce7d06b86b21f0361dd7ce10b95', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (h("div", { key: 'aed05a3280dc96121564ffea87945f441d4286f6', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '63d262e5e10325d145fada8e91a878c47305f2b9', class: "flip-animation" }, h("div", { key: '2a3699f47e595c5e4ab00dfd11e1e66ab2e13039', class: "id-card-icon" }), h("div", { key: 'df195fe9a6db4ec3e14b04b4e932d55adb7c3f3c', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '670341c302a31c8017a9083ebf35e2988122ac3f', class: "success-animation" }, h("div", { key: '27b0b0d91de6a882182b6ccdebecc7c440997e36', class: "check-icon" }), h("div", { key: '71ee5327faf48ea26c2ae36a8a6906c448c3a389', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '39c92aa0b82f6169058b1092894845fcba3f27b4', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '74b02ba81756f0e8bfb6e2feafdb2c44fb80afad', class: "status-spinner" })), h("div", { key: 'c7419efb2c6e2760019ecce619534e24ae3d7351', class: "status-content" }, h("div", { key: '57f315384630f17f6332eddfea628b333299e831', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'ac8632fdd68c24ed00192cb04a13026ada1f953d', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'e51286a839795b18cfac2874e7e1f6e9f8e3bb4b', class: "performance-monitor" }, h("div", { key: 'f8cd289d260af9df2daf23bdfb8db70fe8bf6f45', class: "performance-expanded" }, h("div", { key: 'bf2becb2ee3ad3bffb82b0e90246a4385a19f567', class: "metrics-row" }, h("div", { key: 'f334c2b5de45df73aec6e06cd371151110789a45', class: "metric-compact" }, h("span", { key: 'cbf32f72eb3aaf1bf770d651c32123a43b81035c', class: "metric-label" }, "FPS"), h("span", { key: '9d2d2f46ba3fdb23c010f781044f00b1dd1250e6', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'f29406e8a5b00f5838a144de9fda77ce53177d43', class: "metric-compact" }, h("span", { key: '8f44e29f6134c340f0a58f8aa321612c8a9bbfb8', class: "metric-label" }, "MEM"), h("span", { key: '7a58e164d292329d95054b2679a90e031f6fe4c2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'd0284a0e65fc2879557d73fc2e03295c29f512ae', class: "metrics-row" }, h("div", { key: '78dcbce552e65c95fcd01158f3aa8916ef0e7175', class: "metric-compact" }, h("span", { key: '673cfbc8c9dabb020a3d3c18ee07558a61da9b51', class: "metric-label" }, "INF"), h("span", { key: '833761b7a875c931a8b31913930c3b132f71f618', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '324801e3e69a8de181755367201d8e8c643e8d72', class: "metric-compact" }, h("span", { key: 'f928d9b2dfbf81f5738ab08a42ebb76ff4a483de', class: "metric-label" }, "FRAME"), h("span", { key: 'dc9f2e4a1e848be4d240d89c89126203555ae203', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '7ca0a7b44719b1884334edab586267f021cf86c1', class: "metrics-row" }, h("div", { key: '262667c4320d337dae2220b77f142cf293542562', class: "metric-compact" }, h("span", { key: '578b53d251e58933f654a5afe558931e4333f022', class: "metric-label" }, "DET"), h("span", { key: '4fb36eaebebd73e5cb06d582fa3b4ece61463761', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'c17192e3964164ad603427de3befa1615b027022', class: "metric-compact" }, h("span", { key: 'd004e74797b9468e94c2af996bb62c1a3ca8d58e', class: "metric-label" }, "RATE"), h("span", { key: '0960e8760fd0ac2456a9e76598e5392c24e42d3b', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '3588fba1fe51af1335d643107ba0355aca6f4f8c', class: "watermark" }, h("img", { key: 'd53ec6b0ee9f97f602f4f8857ec605427b2ee478', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
632
998
  }
633
999
  // Utility methods
634
1000
  updateDetectionBoxes(boxes) {
@@ -709,35 +1075,18 @@ export class JaakStamps {
709
1075
  if (allSidesAligned && bestBox) {
710
1076
  cardOutline?.classList.add('perfect-match');
711
1077
  corners?.forEach(corner => corner.classList.add('perfect-match'));
712
- // Debug logging
713
- this.logger.state('CAPTURE_EVALUATION', {
714
- allSidesAligned: true,
715
- hasScreenshotTaken: this.hasScreenshotTaken,
716
- captureDelay: this.captureDelay,
717
- alignmentStartTime: this.alignmentStartTime
718
- });
719
1078
  if (!this.hasScreenshotTaken) {
720
1079
  const currentTime = Date.now();
721
1080
  // Initialize alignment start time if not set
722
1081
  if (!this.alignmentStartTime) {
723
1082
  this.alignmentStartTime = currentTime;
724
- this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
725
1083
  }
726
1084
  // Check if document has been aligned for the configured delay
727
1085
  const alignmentDuration = currentTime - this.alignmentStartTime;
728
- this.logger.state('ALIGNMENT_DURATION_CHECK', {
729
- alignmentDuration,
730
- captureDelay: this.captureDelay,
731
- readyToCapture: alignmentDuration >= this.captureDelay
732
- });
733
1086
  if (alignmentDuration >= this.captureDelay) {
734
- this.logger.state('TRIGGERING_CAPTURE', {
735
- alignmentDuration,
736
- captureDelay: this.captureDelay
737
- });
738
1087
  this.lastDetectedBox = bestBox;
739
- this.takeScreenshot().catch(error => {
740
- this.logger.error('Error al tomar captura de pantalla:', error);
1088
+ this.takeScreenshot().catch(() => {
1089
+ // Handle screenshot error silently
741
1090
  });
742
1091
  this.hasScreenshotTaken = true;
743
1092
  this.alignmentStartTime = undefined;
@@ -760,24 +1109,150 @@ export class JaakStamps {
760
1109
  }
761
1110
  }
762
1111
  }
1112
+ async takeManualScreenshot() {
1113
+ console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
1114
+ // Set processing state immediately
1115
+ const captureState = this.stateManager?.getCaptureState();
1116
+ if (captureState?.step === 'front') {
1117
+ this.processingButton = 'capture-front';
1118
+ }
1119
+ else if (captureState?.step === 'back') {
1120
+ this.processingButton = 'capture-back';
1121
+ }
1122
+ console.log('🔵 processingButton set to:', this.processingButton);
1123
+ // Add small delay to show spinner
1124
+ await new Promise(resolve => setTimeout(resolve, 100));
1125
+ if (!this.videoRef) {
1126
+ this.processingButton = null;
1127
+ return;
1128
+ }
1129
+ // When using manual capture, use mask coordinates for cropping
1130
+ // Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
1131
+ await this.takeScreenshotWithMaskCoordinates();
1132
+ }
1133
+ async takeScreenshotWithMaskCoordinates() {
1134
+ if (!this.videoRef || !this.detectionContainer)
1135
+ return;
1136
+ this.stateManager.updateCaptureState({ isCapturing: true });
1137
+ this.triggerCaptureAnimation();
1138
+ // Use pooled canvas for optimization
1139
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
1140
+ const captureCtx = captureCanvas.getContext('2d', { alpha: false });
1141
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
1142
+ captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
1143
+ // Calculate mask coordinates for cropping
1144
+ const container = this.detectionContainer.parentElement;
1145
+ const containerRect = container.getBoundingClientRect();
1146
+ // Get mask dimensions from CSS properties
1147
+ const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
1148
+ const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
1149
+ const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
1150
+ const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
1151
+ // Convert percentages to actual video coordinates
1152
+ const videoWidth = this.videoRef.videoWidth;
1153
+ const videoHeight = this.videoRef.videoHeight;
1154
+ const videoAspectRatio = videoWidth / videoHeight;
1155
+ const containerAspectRatio = containerRect.width / containerRect.height;
1156
+ let displayedVideoWidth, displayedVideoHeight;
1157
+ let videoOffsetX = 0, videoOffsetY = 0;
1158
+ if (videoAspectRatio > containerAspectRatio) {
1159
+ displayedVideoWidth = containerRect.width;
1160
+ displayedVideoHeight = containerRect.width / videoAspectRatio;
1161
+ videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
1162
+ }
1163
+ else {
1164
+ displayedVideoHeight = containerRect.height;
1165
+ displayedVideoWidth = containerRect.height * videoAspectRatio;
1166
+ videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
1167
+ }
1168
+ // Calculate mask coordinates in video space
1169
+ const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
1170
+ const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
1171
+ const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
1172
+ const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
1173
+ // Convert to video coordinates
1174
+ const scaleX = videoWidth / displayedVideoWidth;
1175
+ const scaleY = videoHeight / displayedVideoHeight;
1176
+ const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
1177
+ const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
1178
+ const maskWidthInVideo = maskWidthInContainer * scaleX;
1179
+ const maskHeightInVideo = maskHeightInContainer * scaleY;
1180
+ // Calculate crop coordinates
1181
+ const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
1182
+ const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
1183
+ const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
1184
+ const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
1185
+ // Use pooled canvas for cropped version
1186
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
1187
+ const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
1188
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
1189
+ croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
1190
+ const captureState = this.stateManager.getCaptureState();
1191
+ if (captureState.step === 'front') {
1192
+ this.stateManager.setCapturedImages({
1193
+ front: {
1194
+ fullFrame: captureCanvas.toDataURL('image/png'),
1195
+ cropped: croppedCanvas.toDataURL('image/png')
1196
+ }
1197
+ });
1198
+ // Check if document classification is enabled (independent of detector)
1199
+ if (this.useDocumentClassification) {
1200
+ try {
1201
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
1202
+ if (classification && classification.class === 'passport') {
1203
+ this.completeProcess(true);
1204
+ this.returnCanvasToPool(captureCanvas);
1205
+ this.returnCanvasToPool(croppedCanvas);
1206
+ // Clear processing button when passport is detected
1207
+ this.processingButton = null;
1208
+ return;
1209
+ }
1210
+ }
1211
+ catch (classifyError) {
1212
+ // No cambiar a manual aquí, solo registrar el error y continuar
1213
+ }
1214
+ }
1215
+ this.stateManager.updateCaptureState({
1216
+ step: 'back',
1217
+ isDetectionPaused: true,
1218
+ showFlipAnimation: true
1219
+ });
1220
+ this.triggerRerender();
1221
+ setTimeout(() => {
1222
+ this.stateManager.updateCaptureState({
1223
+ showFlipAnimation: false,
1224
+ isDetectionPaused: false
1225
+ });
1226
+ this.triggerRerender();
1227
+ this.startBackDocumentTimer();
1228
+ // Clear processing button after animation completes
1229
+ this.processingButton = null;
1230
+ }, 3000);
1231
+ }
1232
+ else if (captureState.step === 'back') {
1233
+ this.stateManager.setCapturedImages({
1234
+ back: {
1235
+ fullFrame: captureCanvas.toDataURL('image/png'),
1236
+ cropped: croppedCanvas.toDataURL('image/png')
1237
+ }
1238
+ });
1239
+ this.completeProcess(false);
1240
+ // Clear processing button after back capture completes
1241
+ this.processingButton = null;
1242
+ }
1243
+ // Return canvases to pool after use
1244
+ this.returnCanvasToPool(captureCanvas);
1245
+ this.returnCanvasToPool(croppedCanvas);
1246
+ }
763
1247
  async takeScreenshot() {
764
1248
  if (!this.videoRef || !this.lastDetectedBox)
765
1249
  return;
766
- this.logger.state('INICIANDO_CAPTURA', {
767
- captureStep: this.stateManager.getCaptureState().step,
768
- detectedBox: this.lastDetectedBox,
769
- videoResolution: {
770
- width: this.videoRef.videoWidth,
771
- height: this.videoRef.videoHeight
772
- }
773
- });
774
1250
  this.stateManager.updateCaptureState({ isCapturing: true });
775
1251
  this.triggerCaptureAnimation();
776
- // Create capture canvas
777
- const captureCanvas = document.createElement('canvas');
778
- captureCanvas.width = this.videoRef.videoWidth;
779
- captureCanvas.height = this.videoRef.videoHeight;
1252
+ // Use pooled canvas for optimization
1253
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
780
1254
  const captureCtx = captureCanvas.getContext('2d', { alpha: false });
1255
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
781
1256
  captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
782
1257
  // Calculate crop coordinates
783
1258
  const INPUT_SIZE = 320;
@@ -787,11 +1262,10 @@ export class JaakStamps {
787
1262
  const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
788
1263
  const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
789
1264
  const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
790
- // Create cropped version
791
- const croppedCanvas = document.createElement('canvas');
792
- croppedCanvas.width = cropWidth;
793
- croppedCanvas.height = cropHeight;
1265
+ // Use pooled canvas for cropped version
1266
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
794
1267
  const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
1268
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
795
1269
  croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
796
1270
  const captureState = this.stateManager.getCaptureState();
797
1271
  if (captureState.step === 'front') {
@@ -803,11 +1277,15 @@ export class JaakStamps {
803
1277
  });
804
1278
  // Check if document classification is enabled
805
1279
  if (this.useDocumentClassification) {
806
- const classification = await this.detectionService.classifyDocument(croppedCanvas);
807
- if (classification && classification.class === 'passport') {
808
- this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
809
- this.completeProcess(true);
810
- return;
1280
+ try {
1281
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
1282
+ if (classification && classification.class === 'passport') {
1283
+ this.completeProcess(true);
1284
+ return;
1285
+ }
1286
+ }
1287
+ catch (classifyError) {
1288
+ // No cambiar a manual aquí, solo registrar el error y continuar
811
1289
  }
812
1290
  }
813
1291
  this.stateManager.updateCaptureState({
@@ -832,6 +1310,9 @@ export class JaakStamps {
832
1310
  });
833
1311
  this.completeProcess(false);
834
1312
  }
1313
+ // Return canvases to pool after use
1314
+ this.returnCanvasToPool(captureCanvas);
1315
+ this.returnCanvasToPool(croppedCanvas);
835
1316
  }
836
1317
  triggerCaptureAnimation() {
837
1318
  const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
@@ -841,6 +1322,135 @@ export class JaakStamps {
841
1322
  cardOutline?.classList.remove('capturing');
842
1323
  }, 600);
843
1324
  }
1325
+ // Método simplificado para cambio a modo manual
1326
+ switchToManualMode() {
1327
+ // Only switch if detector is enabled and not already switched
1328
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
1329
+ return;
1330
+ }
1331
+ // Detener todo el monitoreo de performance
1332
+ this.stopPerformanceMonitoring();
1333
+ // Liberar recursos de ONNX
1334
+ this.releaseOnnxResources();
1335
+ // Resetear máscara a blanco y limpiar detecciones
1336
+ this.resetMaskToWhite();
1337
+ this.detectionBoxes = [];
1338
+ this.hasAutoSwitchedToManual = true;
1339
+ this.performanceDegradedMode = true;
1340
+ this.showManualCaptureButton = true;
1341
+ this.showPerformanceMessage = true;
1342
+ // Hide performance message after 5 seconds
1343
+ setTimeout(() => {
1344
+ this.showPerformanceMessage = false;
1345
+ }, 5000);
1346
+ }
1347
+ // Método para cambiar a modo manual por errores de ONNX
1348
+ switchToManualModeWithWarning(warningMessage) {
1349
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
1350
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
1351
+ return;
1352
+ }
1353
+ // No necesitamos liberar recursos ONNX porque no se han cargado aún
1354
+ // Resetear máscara a blanco y limpiar detecciones
1355
+ this.resetMaskToWhite();
1356
+ this.detectionBoxes = [];
1357
+ // Cambiar a modo manual
1358
+ this.hasAutoSwitchedToManual = true;
1359
+ this.performanceDegradedMode = true;
1360
+ this.showManualCaptureButton = true;
1361
+ this.useDocumentDetector = false; // Desactivar detector automático
1362
+ // Actualizar el estado con mensaje informativo (no error)
1363
+ this.updateStatus('Modo manual activado', warningMessage, 'active');
1364
+ // Continuar con la configuración de cámara
1365
+ this.continueWithCameraSetup();
1366
+ }
1367
+ async continueWithCameraSetup() {
1368
+ try {
1369
+ // Paso 2: Detectar y configurar dispositivos
1370
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1371
+ try {
1372
+ await this.cameraService.enumerateDevices();
1373
+ }
1374
+ catch (enumerateError) {
1375
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
1376
+ }
1377
+ try {
1378
+ await this.updateCameraInfoWithAutofocus();
1379
+ }
1380
+ catch (cameraInfoError) {
1381
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
1382
+ }
1383
+ // Paso 3: Configurar cámara seleccionada
1384
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1385
+ let stream;
1386
+ try {
1387
+ stream = await this.cameraService.setupCamera();
1388
+ }
1389
+ catch (setupError) {
1390
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
1391
+ }
1392
+ // Paso 4: Inicializar video
1393
+ this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
1394
+ try {
1395
+ await this.initializeVideoStream(stream);
1396
+ }
1397
+ catch (videoError) {
1398
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
1399
+ }
1400
+ // Paso 5: Calibrar máscara
1401
+ if (this.detectionContainer) {
1402
+ const container = this.detectionContainer.parentElement;
1403
+ const rect = container.getBoundingClientRect();
1404
+ this.updateMaskDimensions(rect);
1405
+ }
1406
+ // Finalizar
1407
+ this.startTime = Date.now();
1408
+ this.stateManager.updateCaptureState({ isLoading: false });
1409
+ // Mostrar botón manual (ya debe estar habilitado)
1410
+ this.showManualCaptureButton = true;
1411
+ }
1412
+ catch (error) {
1413
+ this.updateStatus('Error de cámara', error.message, 'error');
1414
+ throw error;
1415
+ }
1416
+ }
1417
+ switchToManualModeWithError(errorMessage) {
1418
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
1419
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
1420
+ return;
1421
+ }
1422
+ // Liberar recursos de ONNX inmediatamente
1423
+ this.releaseOnnxResources();
1424
+ // Resetear máscara a blanco y limpiar detecciones
1425
+ this.resetMaskToWhite();
1426
+ this.detectionBoxes = [];
1427
+ // Cambiar a modo manual
1428
+ this.hasAutoSwitchedToManual = true;
1429
+ this.performanceDegradedMode = true;
1430
+ this.showManualCaptureButton = true;
1431
+ this.useDocumentDetector = false; // Desactivar detector automático
1432
+ // Actualizar el estado con mensaje de error
1433
+ this.updateStatus('Modo manual activado', errorMessage, 'error');
1434
+ // Ocultar mensaje después de 5 segundos
1435
+ setTimeout(() => {
1436
+ this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
1437
+ }, 5000);
1438
+ }
1439
+ stopPerformanceMonitoring() {
1440
+ // Nota: No necesitamos limpiar más variables porque ya no se usarán
1441
+ // El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
1442
+ }
1443
+ // Método para liberar recursos de ONNX cuando se cambia a modo manual
1444
+ releaseOnnxResources() {
1445
+ try {
1446
+ // Liberar recursos del servicio de detección
1447
+ if (this.detectionService) {
1448
+ this.detectionService.cleanup();
1449
+ }
1450
+ }
1451
+ catch (error) {
1452
+ }
1453
+ }
844
1454
  completeProcess(skippedBack = false) {
845
1455
  this.stateManager.updateCaptureState({
846
1456
  step: 'completed',
@@ -860,11 +1470,6 @@ export class JaakStamps {
860
1470
  setTimeout(() => {
861
1471
  this.stateManager.updateCaptureState({ showSuccessAnimation: false });
862
1472
  }, 3000);
863
- this.logger.state('PROCESO_COMPLETADO', {
864
- skippedBack,
865
- totalImages: capturedImages.metadata.totalImages,
866
- timestamp: new Date().toISOString()
867
- });
868
1473
  }
869
1474
  stopDetection() {
870
1475
  if (this.animationId) {
@@ -873,7 +1478,6 @@ export class JaakStamps {
873
1478
  }
874
1479
  this.clearBackDocumentTimer();
875
1480
  this.detectionBoxes = [];
876
- this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
877
1481
  }
878
1482
  startBackDocumentTimer() {
879
1483
  if (!this.enableBackDocumentTimer)
@@ -902,6 +1506,15 @@ export class JaakStamps {
902
1506
  return; // Don't toggle if switching camera
903
1507
  this.showCameraSelector = !this.showCameraSelector;
904
1508
  }
1509
+ async updateCameraInfoWithAutofocus() {
1510
+ try {
1511
+ const cameraInfo = await this.cameraService.getCameraInfo();
1512
+ this.cameraInfoWithAutofocus = cameraInfo;
1513
+ }
1514
+ catch (error) {
1515
+ // Keep existing state if update fails
1516
+ }
1517
+ }
905
1518
  async handleCameraSwitch(cameraId) {
906
1519
  if (this.isSwitchingCamera)
907
1520
  return; // Prevent multiple simultaneous switches
@@ -909,10 +1522,6 @@ export class JaakStamps {
909
1522
  // Close the selector immediately when user selects a camera
910
1523
  this.showCameraSelector = false;
911
1524
  this.isSwitchingCamera = true;
912
- this.logger.state('INICIANDO_CAMBIO_CAMARA', {
913
- from: this.cameraService.getSelectedCameraId(),
914
- to: cameraId
915
- });
916
1525
  // Stop current video stream
917
1526
  if (this.videoStream) {
918
1527
  this.videoStream.getTracks().forEach(track => track.stop());
@@ -923,21 +1532,17 @@ export class JaakStamps {
923
1532
  const newStream = await this.cameraService.setupCamera();
924
1533
  // Update video element
925
1534
  await this.initializeVideoStream(newStream);
926
- this.logger.state('CAMBIO_CAMARA_EXITOSO', {
927
- newCameraId: cameraId,
928
- isRearCamera: this.cameraService.isRearCamera(newStream)
929
- });
1535
+ // Update camera info with autofocus data
1536
+ await this.updateCameraInfoWithAutofocus();
930
1537
  }
931
1538
  catch (error) {
932
- this.logger.error('Error al cambiar cámara:', error);
933
1539
  // Try to restore previous camera if switch failed
934
1540
  try {
935
1541
  const fallbackStream = await this.cameraService.setupCamera();
936
1542
  await this.initializeVideoStream(fallbackStream);
937
1543
  }
938
1544
  catch (fallbackError) {
939
- this.logger.error('Error al restaurar cámara anterior:', fallbackError);
940
- this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
1545
+ this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
941
1546
  }
942
1547
  }
943
1548
  finally {
@@ -959,6 +1564,13 @@ export class JaakStamps {
959
1564
  this.detectionBoxes = [];
960
1565
  this.alignmentStartTime = undefined;
961
1566
  this.hasDocumentDetected = false;
1567
+ // Reset sistema simplificado
1568
+ this.processedFramesCount = 0;
1569
+ this.slowFrameCount = 0;
1570
+ // Reset performance degradation state
1571
+ this.hasAutoSwitchedToManual = false;
1572
+ this.performanceDegradedMode = false;
1573
+ this.showPerformanceMessage = false;
962
1574
  if (this.alignmentTimer) {
963
1575
  clearTimeout(this.alignmentTimer);
964
1576
  this.alignmentTimer = undefined;
@@ -978,7 +1590,7 @@ export class JaakStamps {
978
1590
  this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
979
1591
  }
980
1592
  this.isMaskReady = false;
981
- this.updateStatus('Sesión finalizada', '', 'ready');
1593
+ this.updateStatus('Captura finalizada', '', 'ready');
982
1594
  this.detectionBoxes = [];
983
1595
  this.cleanup();
984
1596
  }
@@ -989,7 +1601,6 @@ export class JaakStamps {
989
1601
  this.performanceUpdateInterval = window.setInterval(() => {
990
1602
  this.updatePerformanceMetrics();
991
1603
  }, 500);
992
- this.logger.debug('Monitor de performance inicializado');
993
1604
  }
994
1605
  updatePerformanceMetrics() {
995
1606
  const currentTime = performance.now();
@@ -1032,6 +1643,84 @@ export class JaakStamps {
1032
1643
  if (detectionsFound > 0) {
1033
1644
  this.performanceMetrics.successfulDetections++;
1034
1645
  }
1646
+ // Update performance history for adaptive frame skipping
1647
+ this.performanceHistory.push(processingTime);
1648
+ if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
1649
+ this.performanceHistory.shift();
1650
+ }
1651
+ }
1652
+ // Adaptive frame skipping based on performance
1653
+ getAdaptiveFrameSkip() {
1654
+ // Base conditions for high frame skip
1655
+ if (this.consecutiveFailures > 20)
1656
+ return this.MAX_FRAME_SKIP;
1657
+ if (this.performanceMetrics.inferenceTime > 200)
1658
+ return 6;
1659
+ if (this.performanceMetrics.memoryUsage > 200)
1660
+ return 5;
1661
+ // Calculate average processing time
1662
+ if (this.performanceHistory.length > 0) {
1663
+ const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
1664
+ if (avgProcessingTime > 100)
1665
+ return 4;
1666
+ if (avgProcessingTime > 80)
1667
+ return 3;
1668
+ if (avgProcessingTime > 60)
1669
+ return this.BASE_FRAME_SKIP + 1;
1670
+ }
1671
+ // Low memory devices get higher frame skip
1672
+ if (this.performanceMetrics.memoryUsage > 150)
1673
+ return 4;
1674
+ // Performance is good, use base frame skip
1675
+ return this.BASE_FRAME_SKIP;
1676
+ }
1677
+ // Método para resetear la máscara a color blanco
1678
+ resetMaskToWhite() {
1679
+ const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
1680
+ const corners = this.el.shadowRoot?.querySelectorAll('.corner');
1681
+ const topSide = this.el.shadowRoot?.querySelector('.side-top');
1682
+ const rightSide = this.el.shadowRoot?.querySelector('.side-right');
1683
+ const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
1684
+ const leftSide = this.el.shadowRoot?.querySelector('.side-left');
1685
+ // Remover todas las clases de alineación y estado perfecto
1686
+ cardOutline?.classList.remove('perfect-match');
1687
+ corners?.forEach(corner => corner.classList.remove('perfect-match'));
1688
+ topSide?.classList.remove('aligned');
1689
+ rightSide?.classList.remove('aligned');
1690
+ bottomSide?.classList.remove('aligned');
1691
+ leftSide?.classList.remove('aligned');
1692
+ // Resetear estado de alineación
1693
+ this.sideAlignment = { top: false, right: false, bottom: false, left: false };
1694
+ }
1695
+ // Canvas pool management for screenshots
1696
+ getPooledCanvas(width, height) {
1697
+ // Try to find a canvas with matching dimensions
1698
+ const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
1699
+ if (matchingIndex !== -1) {
1700
+ return this.canvasPool.splice(matchingIndex, 1)[0];
1701
+ }
1702
+ // If no matching canvas, try to reuse any canvas and resize
1703
+ if (this.canvasPool.length > 0) {
1704
+ const canvas = this.canvasPool.pop();
1705
+ canvas.width = width;
1706
+ canvas.height = height;
1707
+ return canvas;
1708
+ }
1709
+ // Create new canvas if pool is empty
1710
+ const canvas = document.createElement('canvas');
1711
+ canvas.width = width;
1712
+ canvas.height = height;
1713
+ return canvas;
1714
+ }
1715
+ returnCanvasToPool(canvas) {
1716
+ // Only return to pool if not at max capacity
1717
+ if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
1718
+ // Clear the canvas before returning to pool
1719
+ const ctx = canvas.getContext('2d');
1720
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1721
+ this.canvasPool.push(canvas);
1722
+ }
1723
+ // If pool is full, let the canvas be garbage collected
1035
1724
  }
1036
1725
  static get is() { return "jaak-stamps"; }
1037
1726
  static get encapsulation() { return "shadow"; }
@@ -1147,6 +1836,26 @@ export class JaakStamps {
1147
1836
  "reflect": false,
1148
1837
  "defaultValue": "false"
1149
1838
  },
1839
+ "useDocumentDetector": {
1840
+ "type": "boolean",
1841
+ "attribute": "use-document-detector",
1842
+ "mutable": false,
1843
+ "complexType": {
1844
+ "original": "boolean",
1845
+ "resolved": "boolean",
1846
+ "references": {}
1847
+ },
1848
+ "required": false,
1849
+ "optional": false,
1850
+ "docs": {
1851
+ "tags": [],
1852
+ "text": ""
1853
+ },
1854
+ "getter": false,
1855
+ "setter": false,
1856
+ "reflect": false,
1857
+ "defaultValue": "true"
1858
+ },
1150
1859
  "preferredCamera": {
1151
1860
  "type": "string",
1152
1861
  "attribute": "preferred-camera",
@@ -1238,9 +1947,15 @@ export class JaakStamps {
1238
1947
  "showCameraSelector": {},
1239
1948
  "isSwitchingCamera": {},
1240
1949
  "hasDocumentDetected": {},
1950
+ "cameraInfoWithAutofocus": {},
1241
1951
  "currentStatus": {},
1242
1952
  "performanceData": {},
1243
- "backDocumentTimerRemaining": {}
1953
+ "backDocumentTimerRemaining": {},
1954
+ "showManualCaptureButton": {},
1955
+ "performanceDegradedMode": {},
1956
+ "showPerformanceMessage": {},
1957
+ "captureStateVersion": {},
1958
+ "processingButton": {}
1244
1959
  };
1245
1960
  }
1246
1961
  static get events() {
@@ -1319,7 +2034,7 @@ export class JaakStamps {
1319
2034
  },
1320
2035
  "startCapture": {
1321
2036
  "complexType": {
1322
- "signature": "() => Promise<void>",
2037
+ "signature": "() => Promise<{ success: boolean; error?: undefined; } | { success: boolean; error: any; }>",
1323
2038
  "parameters": [],
1324
2039
  "references": {
1325
2040
  "Promise": {
@@ -1327,7 +2042,7 @@ export class JaakStamps {
1327
2042
  "id": "global::Promise"
1328
2043
  }
1329
2044
  },
1330
- "return": "Promise<void>"
2045
+ "return": "Promise<{ success: boolean; error?: undefined; } | { success: boolean; error: any; }>"
1331
2046
  },
1332
2047
  "docs": {
1333
2048
  "text": "",
@@ -1336,7 +2051,7 @@ export class JaakStamps {
1336
2051
  },
1337
2052
  "stopCapture": {
1338
2053
  "complexType": {
1339
- "signature": "() => Promise<void>",
2054
+ "signature": "() => Promise<{ success: boolean; error?: undefined; } | { success: boolean; error: any; }>",
1340
2055
  "parameters": [],
1341
2056
  "references": {
1342
2057
  "Promise": {
@@ -1344,7 +2059,7 @@ export class JaakStamps {
1344
2059
  "id": "global::Promise"
1345
2060
  }
1346
2061
  },
1347
- "return": "Promise<void>"
2062
+ "return": "Promise<{ success: boolean; error?: undefined; } | { success: boolean; error: any; }>"
1348
2063
  },
1349
2064
  "docs": {
1350
2065
  "text": "",
@@ -1353,7 +2068,7 @@ export class JaakStamps {
1353
2068
  },
1354
2069
  "resetCapture": {
1355
2070
  "complexType": {
1356
- "signature": "() => Promise<void>",
2071
+ "signature": "() => Promise<{ success: boolean; error?: undefined; } | { success: boolean; error: any; }>",
1357
2072
  "parameters": [],
1358
2073
  "references": {
1359
2074
  "Promise": {
@@ -1361,7 +2076,7 @@ export class JaakStamps {
1361
2076
  "id": "global::Promise"
1362
2077
  }
1363
2078
  },
1364
- "return": "Promise<void>"
2079
+ "return": "Promise<{ success: boolean; error?: undefined; } | { success: boolean; error: any; }>"
1365
2080
  },
1366
2081
  "docs": {
1367
2082
  "text": "",
@@ -1370,7 +2085,7 @@ export class JaakStamps {
1370
2085
  },
1371
2086
  "skipBackCapture": {
1372
2087
  "complexType": {
1373
- "signature": "() => Promise<void>",
2088
+ "signature": "() => Promise<{ success: boolean; error?: undefined; } | { success: boolean; error: any; }>",
1374
2089
  "parameters": [],
1375
2090
  "references": {
1376
2091
  "Promise": {
@@ -1378,7 +2093,7 @@ export class JaakStamps {
1378
2093
  "id": "global::Promise"
1379
2094
  }
1380
2095
  },
1381
- "return": "Promise<void>"
2096
+ "return": "Promise<{ success: boolean; error?: undefined; } | { success: boolean; error: any; }>"
1382
2097
  },
1383
2098
  "docs": {
1384
2099
  "text": "",
@@ -1448,7 +2163,7 @@ export class JaakStamps {
1448
2163
  },
1449
2164
  "setPreferredCamera": {
1450
2165
  "complexType": {
1451
- "signature": "(camera: \"auto\" | \"front\" | \"back\") => Promise<{ success: boolean; selectedCamera: string; availableCameras: number; }>",
2166
+ "signature": "(camera: \"auto\" | \"front\" | \"back\") => Promise<{ success: boolean; selectedCamera: string; availableCameras: number; error?: undefined; } | { success: boolean; error: any; selectedCamera: any; availableCameras: number; }>",
1452
2167
  "parameters": [{
1453
2168
  "name": "camera",
1454
2169
  "type": "\"auto\" | \"front\" | \"back\"",
@@ -1460,7 +2175,7 @@ export class JaakStamps {
1460
2175
  "id": "global::Promise"
1461
2176
  }
1462
2177
  },
1463
- "return": "Promise<{ success: boolean; selectedCamera: string; availableCameras: number; }>"
2178
+ "return": "Promise<{ success: boolean; selectedCamera: string; availableCameras: number; error?: undefined; } | { success: boolean; error: any; selectedCamera: any; availableCameras: number; }>"
1464
2179
  },
1465
2180
  "docs": {
1466
2181
  "text": "",
@@ -1469,7 +2184,7 @@ export class JaakStamps {
1469
2184
  },
1470
2185
  "setCaptureDelay": {
1471
2186
  "complexType": {
1472
- "signature": "(delay: number) => Promise<{ success: boolean; captureDelay: number; }>",
2187
+ "signature": "(delay: number) => Promise<{ success: boolean; error: string; captureDelay: number; } | { success: boolean; captureDelay: number; error?: undefined; }>",
1473
2188
  "parameters": [{
1474
2189
  "name": "delay",
1475
2190
  "type": "number",
@@ -1481,7 +2196,7 @@ export class JaakStamps {
1481
2196
  "id": "global::Promise"
1482
2197
  }
1483
2198
  },
1484
- "return": "Promise<{ success: boolean; captureDelay: number; }>"
2199
+ "return": "Promise<{ success: boolean; error: string; captureDelay: number; } | { success: boolean; captureDelay: number; error?: undefined; }>"
1485
2200
  },
1486
2201
  "docs": {
1487
2202
  "text": "",
@@ -1504,52 +2219,6 @@ export class JaakStamps {
1504
2219
  "text": "",
1505
2220
  "tags": []
1506
2221
  }
1507
- },
1508
- "setTorchEnabled": {
1509
- "complexType": {
1510
- "signature": "(enabled: boolean) => Promise<{ success: boolean; enabled: boolean; }>",
1511
- "parameters": [{
1512
- "name": "enabled",
1513
- "type": "boolean",
1514
- "docs": ""
1515
- }],
1516
- "references": {
1517
- "Promise": {
1518
- "location": "global",
1519
- "id": "global::Promise"
1520
- }
1521
- },
1522
- "return": "Promise<{ success: boolean; enabled: boolean; }>"
1523
- },
1524
- "docs": {
1525
- "text": "",
1526
- "tags": []
1527
- }
1528
- },
1529
- "focusAtPoint": {
1530
- "complexType": {
1531
- "signature": "(x: number, y: number) => Promise<{ success: boolean; coordinates: { x: number; y: number; }; }>",
1532
- "parameters": [{
1533
- "name": "x",
1534
- "type": "number",
1535
- "docs": ""
1536
- }, {
1537
- "name": "y",
1538
- "type": "number",
1539
- "docs": ""
1540
- }],
1541
- "references": {
1542
- "Promise": {
1543
- "location": "global",
1544
- "id": "global::Promise"
1545
- }
1546
- },
1547
- "return": "Promise<{ success: boolean; coordinates: { x: number; y: number; }; }>"
1548
- },
1549
- "docs": {
1550
- "text": "",
1551
- "tags": []
1552
- }
1553
2222
  }
1554
2223
  };
1555
2224
  }