@dssp/supervision 1.0.0-alpha.47 → 1.0.0-alpha.49

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 (23) hide show
  1. package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.d.ts +57 -0
  2. package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.js +800 -0
  3. package/dist-client/pages/building-inspection/building-inspection-detail-ai-measurement.js.map +1 -0
  4. package/dist-client/pages/building-inspection/building-inspection-detail-camera.d.ts +6 -33
  5. package/dist-client/pages/building-inspection/building-inspection-detail-camera.js +58 -427
  6. package/dist-client/pages/building-inspection/building-inspection-detail-camera.js.map +1 -1
  7. package/dist-client/pages/building-inspection/component/building-inspection-detail-header.d.ts +3 -0
  8. package/dist-client/pages/building-inspection/component/building-inspection-detail-header.js +77 -8
  9. package/dist-client/pages/building-inspection/component/building-inspection-detail-header.js.map +1 -1
  10. package/dist-client/pages/building-inspection/component/inspection-document/photo-album-popup.js +21 -6
  11. package/dist-client/pages/building-inspection/component/inspection-document/photo-album-popup.js.map +1 -1
  12. package/dist-client/route.d.ts +1 -1
  13. package/dist-client/route.js +3 -0
  14. package/dist-client/route.js.map +1 -1
  15. package/dist-client/tsconfig.tsbuildinfo +1 -1
  16. package/dist-server/service/checklist/checklist-query.d.ts +1 -0
  17. package/dist-server/service/checklist/checklist-query.js +20 -0
  18. package/dist-server/service/checklist/checklist-query.js.map +1 -1
  19. package/dist-server/service/checklist-template-item/index.d.ts +1 -1
  20. package/dist-server/service/index.d.ts +1 -1
  21. package/dist-server/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +3 -2
  23. package/things-factory.config.js +1 -0
@@ -0,0 +1,800 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/input/ox-input-switch.js';
4
+ import '@operato/mini-map/ox-zoomable-image.js';
5
+ import '@material/web/progress/circular-progress.js';
6
+ import gql from 'graphql-tag';
7
+ import { css, html } from 'lit';
8
+ import { customElement, state, query } from 'lit/decorators.js';
9
+ import html2canvas from 'html2canvas';
10
+ import { PageView } from '@operato/shell';
11
+ import { CommonGristStyles, ScrollbarStyles } from '@operato/styles';
12
+ import { client } from '@operato/graphql';
13
+ import { notify } from '@operato/layout';
14
+ import './component/building-inspection-detail-header';
15
+ let BuildingInspectionAiMeasurement = class BuildingInspectionAiMeasurement extends PageView {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.KEYPOINT_RULER_API_BASE_URL = 'https://hatiolab-korea-uni.ettisoft.com';
19
+ this.project = {};
20
+ this.buildingInspection = {};
21
+ this.buildingInspectionId = '';
22
+ this.capturedVideoUrl = null;
23
+ this.videoFile = null;
24
+ this.frame = null;
25
+ this.analyzing = false;
26
+ this.selectedPoints = {
27
+ x1: null,
28
+ y1: null,
29
+ x2: null,
30
+ y2: null
31
+ };
32
+ this.frameNaturalWidth = 0;
33
+ this.frameNaturalHeight = 0;
34
+ this.displayedImageRect = null;
35
+ this.distance = null;
36
+ this.unit = null;
37
+ this.analyzingError = null;
38
+ this._onImageLoad = () => {
39
+ if (!this.frameImageEl || !this.imageWrapperEl)
40
+ return;
41
+ const img = this.frameImageEl;
42
+ const wrapperRect = this.imageWrapperEl.getBoundingClientRect();
43
+ const imgRect = img.getBoundingClientRect();
44
+ this.frameNaturalWidth = img.naturalWidth;
45
+ this.frameNaturalHeight = img.naturalHeight;
46
+ this.displayedImageRect = {
47
+ width: imgRect.width,
48
+ height: imgRect.height,
49
+ left: imgRect.left - wrapperRect.left,
50
+ top: imgRect.top - wrapperRect.top
51
+ };
52
+ };
53
+ this._onImageClick = (event) => {
54
+ if (!this.frameImageEl || !this.imageWrapperEl)
55
+ return;
56
+ const imgRect = this.frameImageEl.getBoundingClientRect();
57
+ const wrapperRect = this.imageWrapperEl.getBoundingClientRect();
58
+ const clientX = event.clientX;
59
+ const clientY = event.clientY;
60
+ // Ignore clicks outside the displayed image area
61
+ if (clientX < imgRect.left || clientX > imgRect.right || clientY < imgRect.top || clientY > imgRect.bottom) {
62
+ return;
63
+ }
64
+ // 새로운 마크 입력 시 이전 거리/단위 결과 초기화
65
+ this.distance = null;
66
+ this.unit = null;
67
+ this.analyzingError = null;
68
+ const relativeX = clientX - imgRect.left;
69
+ const relativeY = clientY - imgRect.top;
70
+ const xOnNatural = Math.round((relativeX * this.frameNaturalWidth) / imgRect.width);
71
+ const yOnNatural = Math.round((relativeY * this.frameNaturalHeight) / imgRect.height);
72
+ if (this.selectedPoints.x1 === null || this.selectedPoints.y1 === null) {
73
+ this.selectedPoints = Object.assign(Object.assign({}, this.selectedPoints), { x1: xOnNatural, y1: yOnNatural });
74
+ }
75
+ else if (this.selectedPoints.x2 === null || this.selectedPoints.y2 === null) {
76
+ this.selectedPoints = Object.assign(Object.assign({}, this.selectedPoints), { x2: xOnNatural, y2: yOnNatural });
77
+ }
78
+ else {
79
+ // 세 번째 클릭부터는 초기화 후 첫 점으로 설정
80
+ this.selectedPoints = { x1: xOnNatural, y1: yOnNatural, x2: null, y2: null };
81
+ }
82
+ // Update displayed rect cache (in case layout changed)
83
+ this.displayedImageRect = {
84
+ width: imgRect.width,
85
+ height: imgRect.height,
86
+ left: imgRect.left - wrapperRect.left,
87
+ top: imgRect.top - wrapperRect.top
88
+ };
89
+ };
90
+ }
91
+ get context() {
92
+ return {
93
+ title: '검측 관리 상세 - AI 거리 측정'
94
+ };
95
+ }
96
+ render() {
97
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
98
+ return html `
99
+ <building-inspection-detail-header
100
+ .buildingInspectionId=${(_a = this.buildingInspection) === null || _a === void 0 ? void 0 : _a.id}
101
+ .buildingLevelId=${(_c = (_b = this.buildingInspection) === null || _b === void 0 ? void 0 : _b.buildingLevel) === null || _c === void 0 ? void 0 : _c.id}
102
+ .projectName=${this.project.name}
103
+ .buildingName=${(_f = (_e = (_d = this.buildingInspection) === null || _d === void 0 ? void 0 : _d.buildingLevel) === null || _e === void 0 ? void 0 : _e.building) === null || _f === void 0 ? void 0 : _f.name}
104
+ .buildingLevelFloor=${(_h = (_g = this.buildingInspection) === null || _g === void 0 ? void 0 : _g.buildingLevel) === null || _h === void 0 ? void 0 : _h.floor}
105
+ ></building-inspection-detail-header>
106
+
107
+ <div body>
108
+ <!-- Preview selected or recorded video -->
109
+ <div preview>
110
+ <!-- analyzing 로딩 중일 때 -->
111
+ ${this.analyzing
112
+ ? html `<div class="loading-container">
113
+ <md-circular-progress indeterminate></md-circular-progress>
114
+ <div class="loading-text">분석 중...</div>
115
+ </div>`
116
+ : // 동영상 분석 완료 후, 단일 프레임 표시
117
+ ((_j = this.frame) === null || _j === void 0 ? void 0 : _j.download_url)
118
+ ? html `
119
+ <div class="image-wrapper" @click=${this._onImageClick}>
120
+ <img
121
+ id="frameImage"
122
+ src="${this.KEYPOINT_RULER_API_BASE_URL + ((_k = this.frame) === null || _k === void 0 ? void 0 : _k.download_url)}"
123
+ crossorigin="anonymous"
124
+ alt="frame"
125
+ loading="eager"
126
+ @load=${this._onImageLoad}
127
+ />
128
+ ${this._renderMarkers()}
129
+ </div>
130
+ `
131
+ : // 동영상 선택 후 동영상 프리뷰
132
+ this.capturedVideoUrl
133
+ ? html `<video src=${this.capturedVideoUrl} controls playsinline></video>`
134
+ : // 초기 상태, 동영상 선택 전
135
+ html `<md-icon>videocam</md-icon>`}
136
+ </div>
137
+
138
+ <div controls>
139
+ <span class="controls-title">동영상 촬영</span>
140
+
141
+ <!-- 가운데 버튼 -->
142
+ ${((_l = this.frame) === null || _l === void 0 ? void 0 : _l.download_url) && this._hasTwoPoints() && this.distance && this.unit
143
+ ? // 거리 측정 완료 시, 사진 저장 버튼 표시
144
+ html `<button class="save" ?disabled=${this.analyzing} @click=${this._savePhoto}>사진 저장</button>`
145
+ : // 거리 측정 완료 전, 동영상 촬영 버튼 표시
146
+ html `<div class="camera-shutter">
147
+ <md-icon>camera</md-icon>
148
+ <input id="cameraInput" type="file" accept="video/*" @change="${this._onVideoSelected}" />
149
+ </div>`}
150
+
151
+ <!-- Action buttons -->
152
+ <div action-buttons>
153
+ ${this.frame
154
+ ? html `
155
+ <button class="photo" ?disabled=${this.analyzing || !this._hasTwoPoints()} @click=${this._onAnalyzePhoto}>
156
+ 사진 분석
157
+ </button>
158
+ `
159
+ : html `
160
+ <button class="save" ?disabled=${this.analyzing || !this.capturedVideoUrl} @click=${this._onAnalyzeVideo}>
161
+ 동영상 분석
162
+ </button>
163
+ `}
164
+ <button class="retry" @click=${this._onRetry}>초기화</button>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ `;
169
+ }
170
+ async updated(changes) { }
171
+ async pageUpdated(changes, lifecycle) {
172
+ if (this.active) {
173
+ this._onRetry();
174
+ this.buildingInspectionId = lifecycle.resourceId || '';
175
+ await this.initBuildingInspection(this.buildingInspectionId);
176
+ }
177
+ }
178
+ async initBuildingInspection(buildingInspectionId = '') {
179
+ var _a, _b, _c, _d;
180
+ const response = await client.query({
181
+ query: gql `
182
+ query BuildingInspection($buildingInspectionId: String!) {
183
+ buildingInspection(id: $buildingInspectionId) {
184
+ id
185
+ status
186
+ requestDate
187
+ drawingMarker
188
+ checklist {
189
+ id
190
+ location
191
+ inspectionDrawingType
192
+ }
193
+ buildingLevel {
194
+ id
195
+ floor
196
+ mainDrawing {
197
+ id
198
+ name
199
+ fullpath
200
+ }
201
+ mainDrawingImage
202
+ building {
203
+ id
204
+ name
205
+ buildingComplex {
206
+ id
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ `,
213
+ variables: {
214
+ buildingInspectionId
215
+ }
216
+ });
217
+ if (response.errors)
218
+ return;
219
+ this.buildingInspection = response.data.buildingInspection;
220
+ await this._getProjectByBuildingComplexId((_d = (_c = (_b = (_a = this.buildingInspection) === null || _a === void 0 ? void 0 : _a.buildingLevel) === null || _b === void 0 ? void 0 : _b.building) === null || _c === void 0 ? void 0 : _c.buildingComplex) === null || _d === void 0 ? void 0 : _d.id);
221
+ }
222
+ async _getProjectByBuildingComplexId(buildingComplexId) {
223
+ const response = await client.query({
224
+ query: gql `
225
+ query ProjectByBuildingComplexId($buildingComplexId: String!) {
226
+ project: projectByBuildingComplexId(buildingComplexId: $buildingComplexId) {
227
+ id
228
+ name
229
+ }
230
+ }
231
+ `,
232
+ variables: {
233
+ buildingComplexId
234
+ }
235
+ });
236
+ if (response.errors)
237
+ return;
238
+ this.project = response.data.project;
239
+ }
240
+ _onVideoSelected(event) {
241
+ var _a;
242
+ const input = event.target;
243
+ const file = (_a = input === null || input === void 0 ? void 0 : input.files) === null || _a === void 0 ? void 0 : _a[0];
244
+ if (!file)
245
+ return;
246
+ // Only allow video files
247
+ if (!file.type.startsWith('video/')) {
248
+ console.warn('선택된 파일이 동영상이 아닙니다.');
249
+ return;
250
+ }
251
+ // Revoke old object URL if exists
252
+ if (this.capturedVideoUrl) {
253
+ URL.revokeObjectURL(this.capturedVideoUrl);
254
+ }
255
+ this.videoFile = file;
256
+ this.capturedVideoUrl = URL.createObjectURL(file);
257
+ }
258
+ async _onAnalyzeVideo() {
259
+ var _a, _b, _c, _d, _e, _f, _g;
260
+ if (!this.videoFile) {
261
+ console.warn('선택된 동영상이 없습니다.');
262
+ return;
263
+ }
264
+ try {
265
+ this.analyzing = true;
266
+ const form = new FormData();
267
+ form.append('files', this.videoFile, this.videoFile.name);
268
+ const response = await fetch(`${this.KEYPOINT_RULER_API_BASE_URL}/api/run/keypoint-ruler`, {
269
+ method: 'POST',
270
+ headers: { Authorization: `Basic ${btoa('admin:admin1234')}` },
271
+ body: form
272
+ });
273
+ const data = await response.json();
274
+ this.frame = {
275
+ file_id: (_b = (_a = data === null || data === void 0 ? void 0 : data.result) === null || _a === void 0 ? void 0 : _a.output_files[0]) === null || _b === void 0 ? void 0 : _b.file_id,
276
+ download_url: (_d = (_c = data === null || data === void 0 ? void 0 : data.result) === null || _c === void 0 ? void 0 : _c.output_files[0]) === null || _d === void 0 ? void 0 : _d.download_url,
277
+ file_path: (_f = (_e = data === null || data === void 0 ? void 0 : data.result) === null || _e === void 0 ? void 0 : _e.frames[0]) === null || _f === void 0 ? void 0 : _f.file_path,
278
+ calibration: (_g = data === null || data === void 0 ? void 0 : data.result) === null || _g === void 0 ? void 0 : _g.calibration
279
+ };
280
+ }
281
+ catch (error) {
282
+ console.error('동영상 업로드 실패:', error);
283
+ }
284
+ finally {
285
+ this.analyzing = false;
286
+ }
287
+ }
288
+ _onRetry() {
289
+ // Clear current selection and reopen camera
290
+ if (this.capturedVideoUrl) {
291
+ URL.revokeObjectURL(this.capturedVideoUrl);
292
+ }
293
+ this.capturedVideoUrl = null;
294
+ this.videoFile = null;
295
+ this.frame = null;
296
+ this.selectedPoints = { x1: null, y1: null, x2: null, y2: null };
297
+ this.displayedImageRect = null;
298
+ this.frameNaturalWidth = 0;
299
+ this.frameNaturalHeight = 0;
300
+ this.cameraInputEl.value = '';
301
+ this.distance = null;
302
+ this.unit = null;
303
+ this.analyzingError = null;
304
+ }
305
+ _hasTwoPoints() {
306
+ const { x1, y1, x2, y2 } = this.selectedPoints;
307
+ return x1 !== null && y1 !== null && x2 !== null && y2 !== null;
308
+ }
309
+ _renderMarkers() {
310
+ if (!this.displayedImageRect)
311
+ return html ``;
312
+ const { left, top, width, height } = this.displayedImageRect;
313
+ const { x1, y1, x2, y2 } = this.selectedPoints;
314
+ const toDisplay = (x, y) => {
315
+ const dx = left + (x * width) / (this.frameNaturalWidth || 1);
316
+ const dy = top + (y * height) / (this.frameNaturalHeight || 1);
317
+ return { dx, dy };
318
+ };
319
+ return html `
320
+ <!-- 첫 번째 점 -->
321
+ ${x1 !== null && y1 !== null
322
+ ? (() => {
323
+ const p = toDisplay(x1, y1);
324
+ return html `<div class="point-marker point-1" style="left:${p.dx}px; top:${p.dy}px;"></div>`;
325
+ })()
326
+ : ''}
327
+ <!-- 두 번째 점 -->
328
+ ${x2 !== null && y2 !== null
329
+ ? (() => {
330
+ const p = toDisplay(x2, y2);
331
+ return html `<div class="point-marker point-2" style="left:${p.dx}px; top:${p.dy}px;"></div>`;
332
+ })()
333
+ : ''}
334
+ <!-- 두점 사이 선 -->
335
+ ${this._hasTwoPoints()
336
+ ? (() => {
337
+ const p1 = toDisplay(x1, y1);
338
+ const p2 = toDisplay(x2, y2);
339
+ const cx = (p1.dx + p2.dx) / 2;
340
+ const cy = (p1.dy + p2.dy) / 2;
341
+ // 선의 길이와 각도 계산
342
+ const length = Math.sqrt(Math.pow(p2.dx - p1.dx, 2) + Math.pow(p2.dy - p1.dy, 2));
343
+ const angle = Math.atan2(p2.dy - p1.dy, p2.dx - p1.dx) * (180 / Math.PI);
344
+ return html `
345
+ <!-- DIV로 그린 선 -->
346
+ <div
347
+ class="line-overlay"
348
+ style="
349
+ left: ${p1.dx}px;
350
+ top: ${p1.dy}px;
351
+ width: ${length}px;
352
+ height: 3px;
353
+ transform: rotate(${angle}deg);
354
+ "
355
+ ></div>
356
+ <!-- 거리 라벨 -->
357
+ ${this.analyzingError
358
+ ? html `<div class="distance-label" style="left:${cx}px; top:${cy}px;">${this.analyzingError}</div>`
359
+ : this.distance && this.unit
360
+ ? html `<div class="distance-label" style="left:${cx}px; top:${cy}px;">${this.distance} ${this.unit}</div>`
361
+ : ''}
362
+ `;
363
+ })()
364
+ : ''}
365
+ `;
366
+ }
367
+ async _onAnalyzePhoto() {
368
+ if (!this.frame) {
369
+ console.warn('분석할 사진이 없습니다.');
370
+ return;
371
+ }
372
+ if (!this._hasTwoPoints()) {
373
+ console.warn('두 점을 먼저 선택해 주세요.');
374
+ return;
375
+ }
376
+ try {
377
+ this.analyzing = true;
378
+ const payload = {
379
+ x1: this.selectedPoints.x1,
380
+ y1: this.selectedPoints.y1,
381
+ x2: this.selectedPoints.x2,
382
+ y2: this.selectedPoints.y2,
383
+ file_path: this.frame.file_path,
384
+ calibration: this.frame.calibration
385
+ };
386
+ const form = new FormData();
387
+ form.append('input', JSON.stringify(payload));
388
+ const response = await fetch(`${this.KEYPOINT_RULER_API_BASE_URL}/api/run/keypoint-ruler`, {
389
+ method: 'POST',
390
+ headers: { Authorization: `Basic ${btoa('admin:admin1234')}` },
391
+ body: form
392
+ });
393
+ const result = await response.json();
394
+ const { distance, unit, error } = result.result || {};
395
+ this.distance = distance;
396
+ this.unit = unit;
397
+ this.analyzingError = error;
398
+ }
399
+ catch (error) {
400
+ console.error('사진 분석 실패:', error);
401
+ }
402
+ finally {
403
+ this.analyzing = false;
404
+ }
405
+ }
406
+ async _savePhoto() {
407
+ var _a, _b, _c;
408
+ if (!this.frame || !this._hasTwoPoints() || this.distance === null || !this.unit) {
409
+ notify({ message: '분석 완료 후 저장할 수 있습니다.', level: 'warn' });
410
+ return;
411
+ }
412
+ const checklistId = (_b = (_a = this.buildingInspection) === null || _a === void 0 ? void 0 : _a.checklist) === null || _b === void 0 ? void 0 : _b.id;
413
+ let file;
414
+ try {
415
+ file = await this.getCurrentImage();
416
+ }
417
+ catch (e) {
418
+ notify({ message: '이미지 합성에 실패했습니다.', level: 'error' });
419
+ return;
420
+ }
421
+ const response = await client.mutate({
422
+ mutation: gql `
423
+ mutation ($attachments: [NewAttachment!]!) {
424
+ createAttachments(attachments: $attachments) {
425
+ id
426
+ }
427
+ }
428
+ `,
429
+ variables: {
430
+ attachments: [{ file, refBy: checklistId, refType: 'Checklist', description: (_c = this.buildingInspection) === null || _c === void 0 ? void 0 : _c.status }]
431
+ },
432
+ context: { hasUpload: true }
433
+ });
434
+ if (!response.errors) {
435
+ notify({ message: '사진 대지를 저장하였습니다.', level: 'info' });
436
+ return;
437
+ }
438
+ return response.data.createAttachments;
439
+ }
440
+ async getCurrentImage() {
441
+ if (!this.imageWrapperEl) {
442
+ throw new Error('이미지 영역을 찾을 수 없습니다.');
443
+ }
444
+ try {
445
+ // html2canvas로 .image-wrapper 영역 캡처
446
+ const canvas = await html2canvas(this.imageWrapperEl, {
447
+ backgroundColor: null, // 투명 배경
448
+ scale: 1, // 1:1 스케일
449
+ useCORS: true, // CORS 이미지 허용
450
+ allowTaint: false, // 보안상 false
451
+ logging: false // 로그 비활성화
452
+ });
453
+ // 캔버스를 Blob으로 변환
454
+ const blob = await new Promise(resolve => {
455
+ canvas.toBlob(resolve, 'image/png', 1.0);
456
+ });
457
+ // File 객체로 변환하여 반환
458
+ return new File([blob], `inspection_${Date.now()}.png`, {
459
+ type: 'image/png'
460
+ });
461
+ }
462
+ catch (error) {
463
+ console.error('html2canvas 캡처 실패:', error);
464
+ throw new Error('화면 캡처에 실패했습니다.');
465
+ }
466
+ }
467
+ };
468
+ BuildingInspectionAiMeasurement.styles = [
469
+ ScrollbarStyles,
470
+ CommonGristStyles,
471
+ css `
472
+ :host {
473
+ display: grid;
474
+ grid-template-rows: 75px 1fr;
475
+ color: #4e5055;
476
+ width: 100%;
477
+ height: 100%;
478
+ background-color: #f7f7f7;
479
+ }
480
+
481
+ div[body] {
482
+ display: flex;
483
+ justify-items: center;
484
+ gap: var(--spacing-medium);
485
+ margin: var(--spacing-medium);
486
+ min-height: 0;
487
+ }
488
+
489
+ div[preview] {
490
+ flex: 1;
491
+ border: 2px solid #ddd;
492
+ border-radius: 10px;
493
+
494
+ display: flex;
495
+ justify-content: center;
496
+ align-items: center;
497
+ min-height: 0;
498
+ overflow: hidden;
499
+ }
500
+
501
+ div[preview] img {
502
+ max-width: 100%;
503
+ max-height: 100%;
504
+ object-fit: contain;
505
+ }
506
+
507
+ div[preview] video {
508
+ max-width: 100%;
509
+ max-height: 100%;
510
+ object-fit: contain;
511
+ }
512
+
513
+ div[preview] md-icon {
514
+ --md-icon-size: 160px;
515
+ }
516
+
517
+ .loading-container {
518
+ display: flex;
519
+ flex-direction: column;
520
+ align-items: center;
521
+ justify-content: center;
522
+ gap: 12px;
523
+ }
524
+
525
+ .loading-text {
526
+ font-size: 14px;
527
+ color: #666;
528
+ }
529
+
530
+ .frame-container {
531
+ width: 100%;
532
+ height: 100%;
533
+ max-height: 100%;
534
+ min-height: 0;
535
+ overflow-y: auto;
536
+ display: flex;
537
+ flex-direction: column;
538
+ gap: 12px;
539
+ padding: 12px;
540
+ }
541
+
542
+ .frames-header {
543
+ display: flex;
544
+ align-items: center;
545
+ justify-content: center;
546
+ gap: 12px;
547
+ font-size: 20px;
548
+ font-weight: 700;
549
+ color: #333;
550
+ text-align: center;
551
+ padding-block: 8px;
552
+ }
553
+
554
+ .frames-count {
555
+ font-size: 16px;
556
+ color: #777;
557
+ font-weight: 600;
558
+ }
559
+
560
+ .frames-grid {
561
+ display: grid;
562
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
563
+ gap: 12px;
564
+ }
565
+
566
+ .frame-item {
567
+ border: 1px solid #ddd;
568
+ border-radius: 8px;
569
+ overflow: hidden;
570
+ background: #fff;
571
+ cursor: pointer;
572
+ transition:
573
+ box-shadow 0.2s,
574
+ transform 0.1s;
575
+ }
576
+
577
+ .frame-item:hover {
578
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
579
+ transform: translateY(-1px);
580
+ }
581
+
582
+ .frame-item img {
583
+ display: block;
584
+ width: 100%;
585
+ height: 120px;
586
+ object-fit: cover;
587
+ }
588
+
589
+ .image-wrapper {
590
+ position: relative;
591
+ width: 100%;
592
+ height: 100%;
593
+ align-content: center;
594
+ text-align: center;
595
+ }
596
+
597
+ .point-marker {
598
+ position: absolute;
599
+ width: 14px;
600
+ height: 14px;
601
+ border-radius: 50%;
602
+ border: 2px solid #fff;
603
+ transform: translate(-50%, -50%);
604
+ pointer-events: none;
605
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25);
606
+ }
607
+
608
+ .point-1 {
609
+ background-color: #e53935;
610
+ }
611
+
612
+ .point-2 {
613
+ background-color: #1e88e5;
614
+ }
615
+
616
+ .line-overlay {
617
+ position: absolute;
618
+ pointer-events: none;
619
+ background-color: #ff9800;
620
+ transform-origin: left center;
621
+ }
622
+
623
+ .distance-label {
624
+ position: absolute;
625
+ background: rgba(0, 0, 0, 0.7);
626
+ color: #fff;
627
+ font-size: 14px;
628
+ padding: 2px 6px;
629
+ border-radius: 4px;
630
+ transform: translate(-50%, -120%);
631
+ pointer-events: none;
632
+ white-space: nowrap;
633
+ }
634
+
635
+ div[controls] {
636
+ width: 240px;
637
+ display: flex;
638
+ flex-direction: column;
639
+ justify-content: space-between;
640
+ align-items: center;
641
+ }
642
+
643
+ div[action-buttons] {
644
+ display: flex;
645
+ flex-direction: row;
646
+ justify-content: space-between;
647
+ gap: 15px;
648
+ }
649
+
650
+ .switch-container {
651
+ display: flex;
652
+ align-items: center;
653
+ gap: 10px;
654
+ font-size: 24px;
655
+ font-weight: bold;
656
+ }
657
+
658
+ ox-input-switch {
659
+ --ox-simple-switch-fullwidth: 68px;
660
+ --ox-simple-switch-fullheight: 34px;
661
+ --ox-simple-switch-thumbnail-size: 34px;
662
+ }
663
+
664
+ .camera-shutter {
665
+ display: flex;
666
+ justify-content: center;
667
+ align-items: center;
668
+ cursor: pointer;
669
+ position: relative;
670
+ }
671
+
672
+ .controls-title {
673
+ font-size: 18px;
674
+ font-weight: 700;
675
+ color: #333;
676
+ margin-bottom: 8px;
677
+ text-align: center;
678
+ }
679
+
680
+ .camera-shutter md-icon {
681
+ --md-icon-size: 100px;
682
+ }
683
+
684
+ .camera-shutter input {
685
+ opacity: 0;
686
+ width: 100%;
687
+ height: 100%;
688
+ position: absolute;
689
+ left: 0;
690
+ top: 0;
691
+ cursor: pointer;
692
+ }
693
+
694
+ button {
695
+ padding: 10px 20px;
696
+ font-size: 16px;
697
+ border-radius: 5px;
698
+ border: none;
699
+ cursor: pointer;
700
+ }
701
+
702
+ button.save {
703
+ background-color: #4caf50;
704
+ color: white;
705
+
706
+ &:disabled {
707
+ background-color: #ccc;
708
+ cursor: default;
709
+ }
710
+ }
711
+
712
+ button.retry {
713
+ background-color: #f0ad4e;
714
+ color: white;
715
+ }
716
+
717
+ button.photo {
718
+ background-color: #1e88e5;
719
+ color: white;
720
+
721
+ &:disabled {
722
+ background-color: #ccc;
723
+ cursor: default;
724
+ }
725
+ }
726
+ `
727
+ ];
728
+ __decorate([
729
+ state(),
730
+ __metadata("design:type", Object)
731
+ ], BuildingInspectionAiMeasurement.prototype, "project", void 0);
732
+ __decorate([
733
+ state(),
734
+ __metadata("design:type", Object)
735
+ ], BuildingInspectionAiMeasurement.prototype, "buildingInspection", void 0);
736
+ __decorate([
737
+ state(),
738
+ __metadata("design:type", String)
739
+ ], BuildingInspectionAiMeasurement.prototype, "buildingInspectionId", void 0);
740
+ __decorate([
741
+ state(),
742
+ __metadata("design:type", Object)
743
+ ], BuildingInspectionAiMeasurement.prototype, "capturedVideoUrl", void 0);
744
+ __decorate([
745
+ state(),
746
+ __metadata("design:type", Object)
747
+ ], BuildingInspectionAiMeasurement.prototype, "videoFile", void 0);
748
+ __decorate([
749
+ state(),
750
+ __metadata("design:type", Object)
751
+ ], BuildingInspectionAiMeasurement.prototype, "frame", void 0);
752
+ __decorate([
753
+ state(),
754
+ __metadata("design:type", Boolean)
755
+ ], BuildingInspectionAiMeasurement.prototype, "analyzing", void 0);
756
+ __decorate([
757
+ state(),
758
+ __metadata("design:type", Object)
759
+ ], BuildingInspectionAiMeasurement.prototype, "selectedPoints", void 0);
760
+ __decorate([
761
+ state(),
762
+ __metadata("design:type", Number)
763
+ ], BuildingInspectionAiMeasurement.prototype, "frameNaturalWidth", void 0);
764
+ __decorate([
765
+ state(),
766
+ __metadata("design:type", Number)
767
+ ], BuildingInspectionAiMeasurement.prototype, "frameNaturalHeight", void 0);
768
+ __decorate([
769
+ state(),
770
+ __metadata("design:type", Object)
771
+ ], BuildingInspectionAiMeasurement.prototype, "displayedImageRect", void 0);
772
+ __decorate([
773
+ state(),
774
+ __metadata("design:type", Object)
775
+ ], BuildingInspectionAiMeasurement.prototype, "distance", void 0);
776
+ __decorate([
777
+ state(),
778
+ __metadata("design:type", Object)
779
+ ], BuildingInspectionAiMeasurement.prototype, "unit", void 0);
780
+ __decorate([
781
+ state(),
782
+ __metadata("design:type", Object)
783
+ ], BuildingInspectionAiMeasurement.prototype, "analyzingError", void 0);
784
+ __decorate([
785
+ query('img#frameImage'),
786
+ __metadata("design:type", HTMLImageElement)
787
+ ], BuildingInspectionAiMeasurement.prototype, "frameImageEl", void 0);
788
+ __decorate([
789
+ query('div.image-wrapper'),
790
+ __metadata("design:type", HTMLDivElement)
791
+ ], BuildingInspectionAiMeasurement.prototype, "imageWrapperEl", void 0);
792
+ __decorate([
793
+ query('#cameraInput'),
794
+ __metadata("design:type", HTMLInputElement)
795
+ ], BuildingInspectionAiMeasurement.prototype, "cameraInputEl", void 0);
796
+ BuildingInspectionAiMeasurement = __decorate([
797
+ customElement('building-inspection-detail-ai-measurement')
798
+ ], BuildingInspectionAiMeasurement);
799
+ export { BuildingInspectionAiMeasurement };
800
+ //# sourceMappingURL=building-inspection-detail-ai-measurement.js.map