@dssp/supervision 1.0.0-alpha.41 → 1.0.0-alpha.48

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