@things-factory/figure-ui 10.1.28 → 10.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/{themes → harness-theme}/brand.css +15 -4
- package/{dist-client/themes → client/harness-theme}/dark.css +4 -4
- package/client/{themes → harness-theme}/light.css +6 -6
- package/client/modeller/angle-dial.ts +70 -0
- package/client/modeller/figure-animations.ts +9 -9
- package/client/modeller/figure-canvas.ts +6 -3
- package/client/modeller/figure-inspector.ts +18 -3
- package/client/modeller/figure-preview.ts +56 -4
- package/client/modeller/figure-report.ts +11 -28
- package/client/modeller/figure-side.ts +1 -0
- package/client/modeller/joint-gizmo.ts +63 -1
- package/client/modeller/part-edits.ts +75 -9
- package/client/pages/figure-modeller-page.ts +20 -2
- package/client/types.ts +2 -0
- package/dist-client/{themes → harness-theme}/brand.css +15 -4
- package/{client/themes → dist-client/harness-theme}/dark.css +4 -4
- package/dist-client/{themes → harness-theme}/light.css +6 -6
- package/dist-client/modeller/angle-dial.d.ts +32 -0
- package/dist-client/modeller/angle-dial.js +41 -0
- package/dist-client/modeller/angle-dial.js.map +1 -0
- package/dist-client/modeller/figure-animations.d.ts +4 -4
- package/dist-client/modeller/figure-animations.js +9 -9
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.js +6 -3
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +11 -0
- package/dist-client/modeller/figure-inspector.js +20 -3
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +11 -0
- package/dist-client/modeller/figure-preview.js +53 -3
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +8 -4
- package/dist-client/modeller/figure-report.js +11 -31
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-side.js +1 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/joint-gizmo.d.ts +4 -1
- package/dist-client/modeller/joint-gizmo.js +54 -0
- package/dist-client/modeller/joint-gizmo.js.map +1 -1
- package/dist-client/modeller/part-edits.d.ts +29 -4
- package/dist-client/modeller/part-edits.js +61 -9
- package/dist-client/modeller/part-edits.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +7 -0
- package/dist-client/pages/figure-modeller-page.js +20 -2
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +5 -5
- package/test/angle-dial.test.ts +78 -0
- package/test/base-box.test.ts +11 -0
- package/test/brand-palette.test.ts +99 -18
- package/test/expect.ts +2 -0
- package/test/i18n-prefix-guard.test.ts +17 -0
- package/test/joint-needle.test.ts +162 -0
- package/test/part-edits.test.ts +100 -0
- package/test/scene-budget.test.ts +29 -0
- package/translations/en.json +3 -7
- package/translations/ja.json +3 -7
- package/translations/ko.json +3 -7
- package/translations/ms.json +3 -7
- package/translations/zh.json +3 -7
|
@@ -9,8 +9,19 @@
|
|
|
9
9
|
* operato-plant 가 같은 문제를 겪고 같은 방식으로 풀었다(`operato-plant/assets/brand.css`).
|
|
10
10
|
* 그 구조를 그대로 따른다 — 값은 여기 한 번만 적고, 앱 팔레트와 하네스가 이 파일을 읽는다.
|
|
11
11
|
*
|
|
12
|
+
* ── 왜 `themes/` 가 아닌가 ─────────────────────────────────────────────────
|
|
13
|
+
* `@things-factory/builder` 의 `ThemeOverridePlugin` 이 모든 모듈의 `client/themes/*.css` 를
|
|
14
|
+
* **파일 이름 하나당 하나만** 골라 테마 묶음에 싣는다(앱이 맨 앞, 나머지는 건너뜀). 그래서
|
|
15
|
+
* 모듈이 `themes/light.css` 를 두면 앱이 같은 이름을 갖는 순간 통째로 빠지고, 이름을 비켜서
|
|
16
|
+
* 두면 앞의 것과 **같은 선택자로 md-sys 칸을 덮어** 묶음 순서가 색을 정하게 된다. md-sys 슬롯은
|
|
17
|
+
* 모듈이 선언하지 않는다 — shell 이 열고 앱이 채운다(ADR-0060 결정 2).
|
|
18
|
+
*
|
|
19
|
+
* 이 폴더의 셋은 **하네스가 쓰는 것**이고, `themes` 밖에 있으므로 플러그인이 아예 보지 않는다.
|
|
20
|
+
* 앱 화면의 색은 앱의 테마 파일이 정한다. 여기 값을 앱 색으로 알고 고치지 말 것 — 지금 이 셋은
|
|
21
|
+
* navy 이고 앱은 teal 이다(수석 아키텍트가 사용자께 올려 둔 미결이다).
|
|
22
|
+
*
|
|
12
23
|
* ── 브랜드 색만 여기 둔다 ──────────────────────────────────────────────────
|
|
13
|
-
* 중성색은 여기 없다. 그것은 `client/
|
|
24
|
+
* 중성색은 여기 없다. 그것은 `client/harness-theme/light.css` 가 갖는다. 여기 있는 것은 **제품을
|
|
14
25
|
* 가리키는 색**뿐이고, 그것만 절대 갈라지면 안 된다.
|
|
15
26
|
*
|
|
16
27
|
* ── 밝은 것과 어두운 것을 함께 든다 ────────────────────────────────────────
|
|
@@ -37,15 +48,15 @@
|
|
|
37
48
|
* 고른 부품 색**이다. 틀이 진한 색으로 소리를 내면 캔버스 안의 색을 판단할 수 없다. 그래서
|
|
38
49
|
* 브랜드 색은 깊고 조용하게 두고, 고른 것의 밑줄 · 기본 단추 · 링크에만 좁게 쓴다.
|
|
39
50
|
*/
|
|
40
|
-
--brand
|
|
51
|
+
--figure-brand: #154473;
|
|
41
52
|
|
|
42
53
|
/*
|
|
43
54
|
* 어두운 면 위에서 쓰는 같은 색. L* 72 · C 0.130.
|
|
44
55
|
*
|
|
45
56
|
* 3D 캔버스의 선택 테두리와 어두운 배경의 강조가 이 값이다. 어두운 면(#12161b) 대비 7.02.
|
|
46
57
|
*/
|
|
47
|
-
--brand-
|
|
58
|
+
--figure-brand-2: #64a9f3;
|
|
48
59
|
|
|
49
60
|
/** 아주 옅게 깔 때 — 고른 줄의 바탕. 위 색과 같은 색상각이다. */
|
|
50
|
-
--brand-
|
|
61
|
+
--figure-brand-wash: #eaf3fd;
|
|
51
62
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ── 브랜드 색이 밝은 쪽으로 바뀐다 ────────────────────────────────────────
|
|
10
10
|
* `#154473` 은 어두운 바탕에서 대비 1.6 이라 보이지 않는다. 그래서 어두운 면에서는
|
|
11
|
-
* `--brand-
|
|
11
|
+
* `--figure-brand-2`(#64a9f3)가 브랜드 색이다. 이름이 뜻이고 값은 자리에 따라 다르다 —
|
|
12
12
|
* plant · twin 과 같은 규약이다.
|
|
13
13
|
*
|
|
14
14
|
* [측정] 어두운 면(#12161b) 대비 — #154473 1.60 · #64a9f3 7.02
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
--md-sys-color-on-surface-dim: #7d848d;
|
|
41
41
|
|
|
42
42
|
/* ── 브랜드 ──────────────────────────────────────────────────────────── */
|
|
43
|
-
--md-sys-color-primary: var(--brand-
|
|
43
|
+
--md-sys-color-primary: var(--figure-brand-2);
|
|
44
44
|
--md-sys-color-on-primary: #0d1116;
|
|
45
|
-
--md-sys-color-inverse-primary: var(--brand
|
|
45
|
+
--md-sys-color-inverse-primary: var(--figure-brand);
|
|
46
46
|
|
|
47
47
|
--md-sys-color-primary-container: #1c2b3d;
|
|
48
|
-
--md-sys-color-on-primary-container: var(--brand-
|
|
48
|
+
--md-sys-color-on-primary-container: var(--figure-brand-2);
|
|
49
49
|
|
|
50
50
|
--md-sys-color-secondary-container: #1a1f25;
|
|
51
51
|
--md-sys-color-on-secondary-container: #a8afb8;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/*
|
|
4
4
|
* figure 라이트 팔레트 — 이 파일이 이 도구 색의 **정본**이다.
|
|
5
5
|
*
|
|
6
|
-
* 브랜드 색은 `client/
|
|
7
|
-
* `--brand-
|
|
6
|
+
* 브랜드 색은 `client/harness-theme/brand.css` 가 갖는다. 여기서 쓸 수 있는 이름은 `--figure-brand` ·
|
|
7
|
+
* `--figure-brand-2` · `--figure-brand-wash` 셋이다. 값을 여기 다시 적지 않는다.
|
|
8
8
|
*
|
|
9
9
|
* ── 이 도구에서 색이 말해야 하는 것 ────────────────────────────────────────
|
|
10
10
|
* **저작자가 고른 부품 색**이다. 화면 한가운데 캔버스가 있고, 거기 놓인 부품의 색을 보고
|
|
@@ -52,16 +52,16 @@
|
|
|
52
52
|
--md-sys-color-on-surface-dim: #6a6f76;
|
|
53
53
|
|
|
54
54
|
/* ── 브랜드 ──────────────────────────────────────────────────────────── */
|
|
55
|
-
--md-sys-color-primary: var(--brand
|
|
55
|
+
--md-sys-color-primary: var(--figure-brand);
|
|
56
56
|
--md-sys-color-on-primary: #ffffff;
|
|
57
|
-
--md-sys-color-inverse-primary: var(--brand-
|
|
57
|
+
--md-sys-color-inverse-primary: var(--figure-brand-2);
|
|
58
58
|
|
|
59
59
|
/*
|
|
60
60
|
* 고른 줄의 바탕. 이것만 유일하게 색이 든 면이고, 그것도 거의 흰색이다(L* 96).
|
|
61
61
|
* 그 위에 오는 글자는 브랜드 색이며 대비 8.90 이다.
|
|
62
62
|
*/
|
|
63
|
-
--md-sys-color-primary-container: var(--brand-
|
|
64
|
-
--md-sys-color-on-primary-container: var(--brand
|
|
63
|
+
--md-sys-color-primary-container: var(--figure-brand-wash);
|
|
64
|
+
--md-sys-color-on-primary-container: var(--figure-brand);
|
|
65
65
|
|
|
66
66
|
/*
|
|
67
67
|
* 두 번째 면 — 파스텔을 쓰지 않으므로 무채색이다. 칩과 배지가 이것을 깐다.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 막대 옆에 서는 **각도 눈**. 지금 각이 범위의 어디인지를 한 눈에 보인다.
|
|
7
|
+
*
|
|
8
|
+
* ## 왜 3D 로는 모자랐나
|
|
9
|
+
*
|
|
10
|
+
* 관절이 돌아도 화면이 그대로인 경우가 있다 — 제 축에서 대칭인 부품이 그렇다. 그래서 관절이
|
|
11
|
+
* 모는 부품에 바늘을 달아 미리보기에서 함께 돌게 했는데(`joint-gizmo.ts` 의 `jointNeedles`),
|
|
12
|
+
* **미리보기에서는 그 바늘이 4 × 2 픽셀이다**(잰 값: 부품 100mm · 바늘 60 × 12mm · 여덟 대를
|
|
13
|
+
* 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가
|
|
14
|
+
* 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.
|
|
15
|
+
*
|
|
16
|
+
* 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 도형이 아무리
|
|
17
|
+
* 작게 서 있어도 같은 크기로 읽힌다.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface DialShape {
|
|
21
|
+
/** 가동 범위를 채운 부채꼴. SVG path 의 `d`. */
|
|
22
|
+
sector: string
|
|
23
|
+
/** 지금 값이 가리키는 끝점. */
|
|
24
|
+
needle: { x: number; y: number }
|
|
25
|
+
centre: { x: number; y: number }
|
|
26
|
+
radius: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 0 도가 위를 가리키고, 값이 커지면 시계 방향으로 돈다 — 화면의 각과 같은 방향이다. */
|
|
30
|
+
function at(centre: { x: number; y: number }, radius: number, degrees: number) {
|
|
31
|
+
const rad = (degrees * Math.PI) / 180
|
|
32
|
+
return {
|
|
33
|
+
x: +(centre.x + radius * Math.sin(rad)).toFixed(2),
|
|
34
|
+
y: +(centre.y - radius * Math.cos(rad)).toFixed(2)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 한 바퀴에 가까우면 부채꼴 대신 온전한 원이다. */
|
|
39
|
+
const FULL = 359.9
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.
|
|
43
|
+
*/
|
|
44
|
+
export function dialShape(min: number, max: number, value: number, radius = 9): DialShape {
|
|
45
|
+
const centre = { x: radius + 1, y: radius + 1 }
|
|
46
|
+
const span = Math.abs(max - min)
|
|
47
|
+
const held = Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max))
|
|
48
|
+
|
|
49
|
+
if (span >= FULL) {
|
|
50
|
+
const top = at(centre, radius, 0)
|
|
51
|
+
const bottom = at(centre, radius, 180)
|
|
52
|
+
return {
|
|
53
|
+
sector: `M ${top.x} ${top.y} A ${radius} ${radius} 0 1 1 ${bottom.x} ${bottom.y} A ${radius} ${radius} 0 1 1 ${top.x} ${top.y} Z`,
|
|
54
|
+
needle: at(centre, radius, held),
|
|
55
|
+
centre,
|
|
56
|
+
radius
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const from = at(centre, radius, min)
|
|
61
|
+
const to = at(centre, radius, max)
|
|
62
|
+
const large = span > 180 ? 1 : 0
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
sector: `M ${centre.x} ${centre.y} L ${from.x} ${from.y} A ${radius} ${radius} 0 ${large} 1 ${to.x} ${to.y} Z`,
|
|
66
|
+
needle: at(centre, radius, held),
|
|
67
|
+
centre,
|
|
68
|
+
radius
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -899,25 +899,25 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
899
899
|
}
|
|
900
900
|
|
|
901
901
|
/**
|
|
902
|
-
* 움직이는 부품 예산.
|
|
902
|
+
* 따로 움직이는 부품 예산.
|
|
903
903
|
*
|
|
904
|
-
* **넘어도 막지 않는다.**
|
|
905
|
-
*
|
|
906
|
-
* 그 값이 도면 예산에 드러나면 된다.
|
|
904
|
+
* **넘어도 막지 않는다.** 제 변환을 지는 부품은 저마다 단독 묶음이라 draw call 이 늘고,
|
|
905
|
+
* 그만큼 한 화면에 함께 세울 수 있는 대수가 준다 — 그 대가가 수로 보이면 저작자가 아낀다.
|
|
906
|
+
* 랜드마크 설비 하나는 정교해도 되고, 그 값이 도면 예산에 드러나면 된다.
|
|
907
907
|
*/
|
|
908
908
|
private renderBudget(): TemplateResult {
|
|
909
909
|
const used = this.movingParts
|
|
910
|
-
const over = used > LIMITS.
|
|
910
|
+
const over = used > LIMITS.independentParts
|
|
911
911
|
|
|
912
912
|
return html`
|
|
913
913
|
<section>
|
|
914
|
-
<h3>${i18next.t('figure.label.budget-
|
|
914
|
+
<h3>${i18next.t('figure.label.budget-independent')}</h3>
|
|
915
915
|
<div budget ?over=${over}>
|
|
916
916
|
<strong>${used}</strong>
|
|
917
|
-
<span quiet>/ ${LIMITS.
|
|
917
|
+
<span quiet>/ ${LIMITS.independentParts}</span>
|
|
918
918
|
</div>
|
|
919
|
-
<p quiet>${i18next.t('figure.text.
|
|
920
|
-
${over ? html`<p warn>${i18next.t('figure.text.
|
|
919
|
+
<p quiet>${i18next.t('figure.text.independent-parts-cost')}</p>
|
|
920
|
+
${over ? html`<p warn>${i18next.t('figure.text.independent-parts-over')}</p>` : nothing}
|
|
921
921
|
</section>
|
|
922
922
|
`
|
|
923
923
|
}
|
|
@@ -4,7 +4,7 @@ import { customElement, property, query, state } from 'lit/decorators.js'
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { create, Model } from '@hatiolab/things-scene'
|
|
6
6
|
|
|
7
|
-
import { gridStep } from './part-edits.js'
|
|
7
|
+
import { gridStep, roundPart } from './part-edits.js'
|
|
8
8
|
import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js'
|
|
9
9
|
import type { ViewSettings } from './figure-view.js'
|
|
10
10
|
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
@@ -536,8 +536,11 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
536
536
|
const was = this.parts.find(part => part.name === name)
|
|
537
537
|
if (!was) return
|
|
538
538
|
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
/*
|
|
540
|
+
씬 모델 그대로 올려 보낸다 — 모델러 페이지가 갖고 있는 것도 씬 모델이라 변환할 것이 없다.
|
|
541
|
+
밀리미터로만 맞춘다: 기즈모는 비율을 곱해 크기를 내므로 소수가 그대로 남는다.
|
|
542
|
+
*/
|
|
543
|
+
this.tell('part-changed', { name, part: roundPart({ ...was, ...changed!.state } as PartModel) })
|
|
541
544
|
})
|
|
542
545
|
}
|
|
543
546
|
|
|
@@ -856,6 +856,21 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
856
856
|
|
|
857
857
|
/** 도형의 DB 식별자. 비어 있으면 미저장 신규 모델이다. */
|
|
858
858
|
@property({ type: String }) figureId = ''
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* 발행 판 번호. 0 이면 한 번도 발행되지 않았고, 그때만 `type` 을 고칠 수 있다(ADR-0063 결정 4).
|
|
862
|
+
*/
|
|
863
|
+
@property({ type: Number }) figureVersion = 0
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* 타입이 굳었나 — **한 번이라도 발행됐으면 굳는다**(ADR-0063 결정 4).
|
|
867
|
+
*
|
|
868
|
+
* 저장되지 않은 새 도형과 아직 발행 전인 초안에서는 고칠 수 있다. 전에는 「저장됐나」로
|
|
869
|
+
* 갈랐고, 그래서 저작자가 기계가 지은 이름(`FIGURE_MU6RJH47`)을 한 번도 못 고쳤다.
|
|
870
|
+
*/
|
|
871
|
+
private get typeIsFixed(): boolean {
|
|
872
|
+
return !!this.figureId && (this.figureVersion ?? 0) > 0
|
|
873
|
+
}
|
|
859
874
|
/** 도형의 표시 이름. 정본은 저작면이 들고, 여기서는 보여 주고 바꾼다(`figure-rename` 으로 알린다). */
|
|
860
875
|
@property({ type: String }) figureName = ''
|
|
861
876
|
/** 분류 하나 — 팔레트에서 묶는 기준. */
|
|
@@ -1232,11 +1247,11 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1232
1247
|
/>
|
|
1233
1248
|
</div>
|
|
1234
1249
|
<div inline-field>
|
|
1235
|
-
<label>${i18next.t('figure.label.figure-type')}${this.
|
|
1236
|
-
${!this.
|
|
1250
|
+
<label>${i18next.t('figure.label.figure-type')}${this.typeIsFixed ? this.helpButton('figure.text.type-name-cannot-change') : nothing}</label>
|
|
1251
|
+
${!this.typeIsFixed
|
|
1237
1252
|
? html`<input
|
|
1238
1253
|
.value=${this.draft?.figureType ?? ''}
|
|
1239
|
-
placeholder=${i18next.t('figure.text.figure-type-placeholder'
|
|
1254
|
+
placeholder=${i18next.t('figure.text.figure-type-placeholder')}
|
|
1240
1255
|
@change=${(e: Event) =>
|
|
1241
1256
|
this.dispatchEvent(
|
|
1242
1257
|
new CustomEvent('figure-type-change', {
|
|
@@ -8,6 +8,8 @@ import { create, registerFigure } from '@hatiolab/things-scene'
|
|
|
8
8
|
|
|
9
9
|
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH } from './figure-view.js'
|
|
10
10
|
import type { ViewSettings } from './figure-view.js'
|
|
11
|
+
import { dialShape } from './angle-dial.js'
|
|
12
|
+
import { jointNeedles } from './joint-gizmo.js'
|
|
11
13
|
import { poseChanges } from './pose-defaults.js'
|
|
12
14
|
import type { FigureDraft } from './figure-source.js'
|
|
13
15
|
|
|
@@ -64,7 +66,7 @@ export const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' |
|
|
|
64
66
|
*
|
|
65
67
|
* 한동안 배치 기준을 높이로 옮기는 계산을 이 파일이 들고 있었다. 이제 씬이 한다 —
|
|
66
68
|
* `FigureRealObject.effectiveZPos` 가 청사진의 `placement` 와 레이어의 `heights` 로 정한다
|
|
67
|
-
* (ADR-0045, `operato-
|
|
69
|
+
* (ADR-0045, `operato-application/adr/ADR-0045.md`). 도면이 그렇게 세우므로 「도면에 놓았을
|
|
68
70
|
* 때의 모습」을 말하는 이 화면도 그렇게 세워야 한다 — 여기서 `zPos` 를 적으면 **적은 값이
|
|
69
71
|
* 이겨서**, 씬이 무엇을 할지가 이 화면에서만 안 보인다.
|
|
70
72
|
*
|
|
@@ -142,10 +144,27 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
142
144
|
}
|
|
143
145
|
div[clips] label {
|
|
144
146
|
display: grid;
|
|
145
|
-
grid-template-columns: auto 96px 34px;
|
|
147
|
+
grid-template-columns: auto 20px 96px 34px;
|
|
146
148
|
align-items: center;
|
|
147
149
|
gap: 8px;
|
|
148
150
|
}
|
|
151
|
+
/* 각도 눈. 3D 가 아무리 작게 서 있어도 여기서는 같은 크기로 읽힌다. */
|
|
152
|
+
div[clips] svg[dial] {
|
|
153
|
+
width: 20px;
|
|
154
|
+
height: 20px;
|
|
155
|
+
}
|
|
156
|
+
div[clips] svg[dial] path {
|
|
157
|
+
fill: rgba(255, 255, 255, 0.22);
|
|
158
|
+
}
|
|
159
|
+
div[clips] svg[dial] line {
|
|
160
|
+
stroke: var(--md-sys-color-primary, #7cc6ff);
|
|
161
|
+
stroke-width: 2;
|
|
162
|
+
stroke-linecap: round;
|
|
163
|
+
}
|
|
164
|
+
div[clips] svg[dial] circle {
|
|
165
|
+
fill: none;
|
|
166
|
+
stroke: rgba(255, 255, 255, 0.35);
|
|
167
|
+
}
|
|
149
168
|
/*
|
|
150
169
|
막대 위에 놓는 단추. 자세를 만든 자리에 있어야 「지금 이것을 기본으로」가 읽힌다.
|
|
151
170
|
앉힐 것이 있을 때만 나오므로, 늘 서 있는 칸이 아니다.
|
|
@@ -279,6 +298,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
279
298
|
return html`
|
|
280
299
|
<label>
|
|
281
300
|
<span>${parameter.label ?? parameter.name} <span drive>${range?.unit ?? ''}</span></span>
|
|
301
|
+
${this.renderDial(range, value)}
|
|
282
302
|
<input
|
|
283
303
|
type="range"
|
|
284
304
|
min=${range?.min ?? 0}
|
|
@@ -297,6 +317,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
297
317
|
return html`
|
|
298
318
|
<label>
|
|
299
319
|
<span>${clip.name} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
|
|
320
|
+
<span></span>
|
|
300
321
|
<input
|
|
301
322
|
type="range"
|
|
302
323
|
min="0"
|
|
@@ -313,6 +334,31 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
313
334
|
`
|
|
314
335
|
}
|
|
315
336
|
|
|
337
|
+
/**
|
|
338
|
+
* 각도 파라미터에만 다는 눈.
|
|
339
|
+
*
|
|
340
|
+
* 미리보기의 3D 는 여덟 대를 한 화면에 세우느라 한 대가 20 픽셀쯤이다. 관절이 돌았다는
|
|
341
|
+
* 표시를 그 안에 그리면 몇 픽셀에 그친다 — 부품에 단 바늘이 4 × 2 픽셀로 나왔다(잰 값).
|
|
342
|
+
* 각을 읽는 일은 배율에서 떼어 여기에 둔다.
|
|
343
|
+
*
|
|
344
|
+
* 각이 아닌 파라미터(밀리미터 · 퍼센트 · 초)에는 안 단다. 눈금이 도는 것이 아니라서
|
|
345
|
+
* 원으로 그리면 없는 뜻이 생긴다.
|
|
346
|
+
*/
|
|
347
|
+
private renderDial(range: { unit?: string; min: number; max: number } | undefined, value: number) {
|
|
348
|
+
if (!range || range.unit !== 'deg') return html`<span></span>`
|
|
349
|
+
|
|
350
|
+
const dial = dialShape(range.min, range.max, value)
|
|
351
|
+
const span = dial.radius * 2 + 2
|
|
352
|
+
|
|
353
|
+
return html`
|
|
354
|
+
<svg dial viewBox="0 0 ${span} ${span}" aria-hidden="true">
|
|
355
|
+
<path d=${dial.sector}></path>
|
|
356
|
+
<circle cx=${dial.centre.x} cy=${dial.centre.y} r=${dial.radius}></circle>
|
|
357
|
+
<line x1=${dial.centre.x} y1=${dial.centre.y} x2=${dial.needle.x} y2=${dial.needle.y}></line>
|
|
358
|
+
</svg>
|
|
359
|
+
`
|
|
360
|
+
}
|
|
361
|
+
|
|
316
362
|
/**
|
|
317
363
|
* 세워 둔 인스턴스 전부에 같은 값을 준다.
|
|
318
364
|
*
|
|
@@ -410,8 +456,14 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
410
456
|
}
|
|
411
457
|
|
|
412
458
|
const view = this.view ?? DEFAULT_VIEW
|
|
459
|
+
/*
|
|
460
|
+
관절마다 바늘 하나를 붙여 세운다(`joint-gizmo.ts`). 대칭인 부품은 돌아도 그림이 같아서,
|
|
461
|
+
바늘이 없으면 저작자가 막대를 끝까지 밀고도 관절이 도는 것을 못 본다. 여기서만 붙인다 —
|
|
462
|
+
저장되는 것도 저작면에 서는 것도 원본 그대로다.
|
|
463
|
+
*/
|
|
464
|
+
const shown: FigureSource = { ...source, parts: [...source.parts, ...jointNeedles(source)] }
|
|
413
465
|
/* 씬이 이 둘로 인스턴스 높이를 정한다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */
|
|
414
|
-
const signature = JSON.stringify([
|
|
466
|
+
const signature = JSON.stringify([shown, view.placement, view.ceilingHeight])
|
|
415
467
|
if (this.scene && this.standing === signature) return
|
|
416
468
|
|
|
417
469
|
const { errors } = validate(source)
|
|
@@ -426,7 +478,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
426
478
|
this.driving = {}
|
|
427
479
|
|
|
428
480
|
try {
|
|
429
|
-
registerFigure(compile(
|
|
481
|
+
registerFigure(compile(shown) as never)
|
|
430
482
|
|
|
431
483
|
/*
|
|
432
484
|
도면이 하는 일을 그대로 한다. 컴포넌트에는 타입 이름과 자리·크기만 적는다 —
|
|
@@ -433,7 +433,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
433
433
|
@state() private score?: FigureScore
|
|
434
434
|
@state() private curve = ''
|
|
435
435
|
/** 1만 개를 놓았을 때. 저작자가 실제로 궁금한 숫자다. */
|
|
436
|
-
@state() private many?: FigureCost
|
|
437
436
|
/** 몇 개까지 놓을 수 있나. 이 패널의 머리다. */
|
|
438
437
|
@state() private capacity?: FigureCapacity
|
|
439
438
|
/** 추이 그림을 제 크기로 펼쳤나. */
|
|
@@ -768,12 +767,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
768
767
|
<dt>${i18next.t('figure.label.triangles')}</dt>
|
|
769
768
|
<dd>${cost.triangles.toLocaleString()}</dd>
|
|
770
769
|
|
|
771
|
-
<dt>${i18next.t('figure.label.triangles-at-100')}</dt>
|
|
772
|
-
<dd>${cost.at.triangles.toLocaleString()}</dd>
|
|
773
|
-
|
|
774
|
-
<dt>${i18next.t('figure.label.draw-calls-at-100')}</dt>
|
|
775
|
-
<dd>${cost.at.drawCalls.toLocaleString()}</dd>
|
|
776
|
-
|
|
777
770
|
<dt>${i18next.t('figure.label.distinct-shapes')}</dt>
|
|
778
771
|
<dd>${cost.distinctShapes}</dd>
|
|
779
772
|
|
|
@@ -785,33 +778,26 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
785
778
|
}
|
|
786
779
|
|
|
787
780
|
/**
|
|
788
|
-
* 재활용 추이
|
|
781
|
+
* 재활용 추이 그림.
|
|
782
|
+
*
|
|
783
|
+
* **「몇 개 놓으면 얼마」를 숫자로 적지 않는다.** 놓는 개수는 값을 치르지 않는다 —
|
|
784
|
+
* 한 화면에 보이는 개수 × 그 도형의 묶음 수가 치른다(실측 2026-09-18, ADR-0051 ③).
|
|
785
|
+
* 전에는 여기에 「100개 기준 draw call」 · 「1만개 기준」을 적었고, 그 수는 실제로 드는
|
|
786
|
+
* 값과 어긋났다. 답은 위의 성숙도 칸이 한다 — 잰 기계와 날짜를 달고 「한 화면에 몇 대」를
|
|
787
|
+
* 말한다.
|
|
789
788
|
*
|
|
790
789
|
* ## 그림을 사이드바에 밀어 넣지 않는다
|
|
791
790
|
*
|
|
792
791
|
* `costCurveSvg` 의 자연 크기는 680x380 이고 글자가 11px 다. 300px 칸에서
|
|
793
792
|
* width:100% 로 줄이면 글자가 4px 이 되어 **아무것도 읽을 수 없다** — 화면을 띄워
|
|
794
|
-
* 보고서야 드러났다.
|
|
795
|
-
* 실제로 궁금한 숫자(1만 개일 때)를 두고 그림은 선택해서 제 크기로 본다.
|
|
793
|
+
* 보고서야 드러났다. 그림은 선택해서 제 크기로 본다.
|
|
796
794
|
*/
|
|
797
795
|
private renderCurve() {
|
|
798
|
-
|
|
799
|
-
if (!many && !this.curve) return nothing
|
|
796
|
+
if (!this.curve) return nothing
|
|
800
797
|
|
|
801
798
|
return html`
|
|
802
799
|
<section>
|
|
803
800
|
<h3>${i18next.t('figure.label.reuse-trend')}</h3>
|
|
804
|
-
${many
|
|
805
|
-
? html`
|
|
806
|
-
<dl metrics>
|
|
807
|
-
<dt>${i18next.t('figure.label.triangles-at-10k')}</dt>
|
|
808
|
-
<dd>${many.at.triangles.toLocaleString()}</dd>
|
|
809
|
-
|
|
810
|
-
<dt>${i18next.t('figure.label.draw-calls-at-10k')}</dt>
|
|
811
|
-
<dd>${many.at.drawCalls.toLocaleString()}</dd>
|
|
812
|
-
</dl>
|
|
813
|
-
`
|
|
814
|
-
: nothing}
|
|
815
801
|
${this.curve
|
|
816
802
|
? html`
|
|
817
803
|
<button expand @click=${() => (this.expanded = true)}>
|
|
@@ -899,7 +885,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
899
885
|
if (!source) {
|
|
900
886
|
this.cost = undefined
|
|
901
887
|
this.score = undefined
|
|
902
|
-
this.many = undefined
|
|
903
888
|
this.capacity = undefined
|
|
904
889
|
this.curve = ''
|
|
905
890
|
this.errors = []
|
|
@@ -916,7 +901,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
916
901
|
if (this.bare) {
|
|
917
902
|
this.cost = undefined
|
|
918
903
|
this.score = undefined
|
|
919
|
-
this.many = undefined
|
|
920
904
|
this.capacity = undefined
|
|
921
905
|
this.curve = ''
|
|
922
906
|
this.errors = []
|
|
@@ -938,7 +922,6 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
938
922
|
// 형식을 어겼으면 컴파일이 던진다. 셈을 비우고 오류만 보여 준다.
|
|
939
923
|
this.cost = undefined
|
|
940
924
|
this.score = undefined
|
|
941
|
-
this.many = undefined
|
|
942
925
|
this.capacity = undefined
|
|
943
926
|
this.curve = ''
|
|
944
927
|
this.sizing = []
|
|
@@ -963,9 +946,9 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
963
946
|
blocking: finding.code === 'sizing-splits'
|
|
964
947
|
}))
|
|
965
948
|
|
|
966
|
-
|
|
949
|
+
/* 개수 인자는 `cost.at` 만 정한다 — 그 칸을 화면에서 뺐으니 기본값 그대로 둔다. */
|
|
950
|
+
this.cost = costOf(blueprint)
|
|
967
951
|
// 1만 개는 figure-model 에 다시 묻는다 — 화면이 곱셈으로 짐작하지 않는다.
|
|
968
|
-
this.many = costOf(blueprint, 10000)
|
|
969
952
|
this.score = scoreOf(blueprint)
|
|
970
953
|
this.capacity = capacityOf(this.cost)
|
|
971
954
|
|
|
@@ -199,6 +199,7 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
199
199
|
<figure-inspector
|
|
200
200
|
?off=${this.tab !== 'properties'}
|
|
201
201
|
.figureId=${this.figureId}
|
|
202
|
+
.figureVersion=${this.figureVersion}
|
|
202
203
|
.figureName=${this.figureName}
|
|
203
204
|
.figureCategory=${this.figureCategory}
|
|
204
205
|
.figureTags=${this.figureTags}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { FigureJoint, FigurePart } from '@hatiolab/figure-model'
|
|
5
|
+
import type { FigureJoint, FigurePart, FigureSource } from '@hatiolab/figure-model'
|
|
6
6
|
|
|
7
7
|
import { partFromFigure } from './figure-source.js'
|
|
8
8
|
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
@@ -116,3 +116,65 @@ export function gizmoPart(joint: FigureJoint, child: PartModel | undefined, draf
|
|
|
116
116
|
|
|
117
117
|
return { ...partFromFigure(part, draft), locked: true }
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* 미리보기에서 **관절이 돌았다는 것**을 보이게 하는 바늘.
|
|
122
|
+
*
|
|
123
|
+
* 상자 위에 원기둥을 얹고 그 축으로 돌리면 화면이 그대로다 — 원기둥은 제 축에서 대칭이라
|
|
124
|
+
* 돌아도 같은 그림이다. 저작자는 막대를 76도까지 밀고도 아무것도 못 보고 「관절이 안 먹는다」로
|
|
125
|
+
* 읽는다. 값은 제대로 갔고 부품도 돌았는데 볼 것이 없었을 뿐이다.
|
|
126
|
+
*
|
|
127
|
+
* 그래서 관절이 모는 부품에 얇은 막대 하나를 **자식으로** 붙인다. 부모가 관절의 자식이라
|
|
128
|
+
* 관절이 도는 만큼 같이 돌고, 축에서 비켜나 있어 대칭인 부품에서도 움직임이 보인다. 매 프레임
|
|
129
|
+
* 무엇을 다시 그릴 일도, 막대를 밀 때마다 씬을 다시 세울 일도 없다.
|
|
130
|
+
*
|
|
131
|
+
* **미리보기에만 붙인다.** 저장 형식에도, 저작면에도, 카드 그림에도 안 들어간다 — 미리보기는
|
|
132
|
+
* 크기 반응을 보는 작업대이고 거기서만 쓰는 표시다.
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/** 미리보기에만 있는 이름. 저장 형식에는 없다. */
|
|
136
|
+
export const NEEDLE = '__needle-'
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 막대의 굵기 — **길이에 견주어** 잡는다.
|
|
140
|
+
*
|
|
141
|
+
* 8mm 로 고정했더니 미리보기에서 안 보였다. 미리보기는 여덟 대를 한 화면에 세우는 자리라
|
|
142
|
+
* 100mm 짜리 부품이 화면에서 20 픽셀쯤이고, 그 위의 8mm 는 한 픽셀이 안 된다. 세어 보면
|
|
143
|
+
* 막대는 분명히 서 있는데(부품 둘짜리 표본의 인스턴스에 mesh 셋 · 삼각형 72) 눈에 안 띈다.
|
|
144
|
+
*/
|
|
145
|
+
const needleThick = (radius: number) => Math.max(6, Math.round(radius / 5))
|
|
146
|
+
|
|
147
|
+
/** 축에서 비켜날 방향. 그 축과 직각이어야 도는 것이 보인다. */
|
|
148
|
+
const ASIDE: Record<string, 'x' | 'z'> = { x: 'z', y: 'x', z: 'x' }
|
|
149
|
+
|
|
150
|
+
export function jointNeedles(source: FigureSource): FigurePart[] {
|
|
151
|
+
const byName = new Map(source.parts.map(part => [part.name, part]))
|
|
152
|
+
|
|
153
|
+
return (source.joints ?? []).flatMap(joint => {
|
|
154
|
+
const axis = axisName(joint.axis)
|
|
155
|
+
if (!axis) return []
|
|
156
|
+
|
|
157
|
+
const child = byName.get(joint.child)
|
|
158
|
+
const size = child?.transform.size
|
|
159
|
+
const reach = size ? Math.max(size.x, size.y, size.z) : 0
|
|
160
|
+
const radius = Math.max(60, Math.round(reach * 0.6))
|
|
161
|
+
const aside = ASIDE[axis.slice(1)]!
|
|
162
|
+
const thick = needleThick(radius)
|
|
163
|
+
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
name: `${NEEDLE}${joint.name}`,
|
|
167
|
+
primitive: 'cube' as const,
|
|
168
|
+
transform: {
|
|
169
|
+
position: { ...joint.origin, [aside]: joint.origin[aside] + radius / 2 },
|
|
170
|
+
size:
|
|
171
|
+
aside === 'x'
|
|
172
|
+
? { x: radius, y: thick, z: thick }
|
|
173
|
+
: { x: thick, y: thick, z: radius }
|
|
174
|
+
},
|
|
175
|
+
material: { token: 'palette.accent' },
|
|
176
|
+
parent: joint.child
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
})
|
|
180
|
+
}
|