@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.
Files changed (62) hide show
  1. package/client/{themes → harness-theme}/brand.css +15 -4
  2. package/{dist-client/themes → client/harness-theme}/dark.css +4 -4
  3. package/client/{themes → harness-theme}/light.css +6 -6
  4. package/client/modeller/angle-dial.ts +70 -0
  5. package/client/modeller/figure-animations.ts +9 -9
  6. package/client/modeller/figure-canvas.ts +6 -3
  7. package/client/modeller/figure-inspector.ts +18 -3
  8. package/client/modeller/figure-preview.ts +56 -4
  9. package/client/modeller/figure-report.ts +11 -28
  10. package/client/modeller/figure-side.ts +1 -0
  11. package/client/modeller/joint-gizmo.ts +63 -1
  12. package/client/modeller/part-edits.ts +75 -9
  13. package/client/pages/figure-modeller-page.ts +20 -2
  14. package/client/types.ts +2 -0
  15. package/dist-client/{themes → harness-theme}/brand.css +15 -4
  16. package/{client/themes → dist-client/harness-theme}/dark.css +4 -4
  17. package/dist-client/{themes → harness-theme}/light.css +6 -6
  18. package/dist-client/modeller/angle-dial.d.ts +32 -0
  19. package/dist-client/modeller/angle-dial.js +41 -0
  20. package/dist-client/modeller/angle-dial.js.map +1 -0
  21. package/dist-client/modeller/figure-animations.d.ts +4 -4
  22. package/dist-client/modeller/figure-animations.js +9 -9
  23. package/dist-client/modeller/figure-animations.js.map +1 -1
  24. package/dist-client/modeller/figure-canvas.js +6 -3
  25. package/dist-client/modeller/figure-canvas.js.map +1 -1
  26. package/dist-client/modeller/figure-inspector.d.ts +11 -0
  27. package/dist-client/modeller/figure-inspector.js +20 -3
  28. package/dist-client/modeller/figure-inspector.js.map +1 -1
  29. package/dist-client/modeller/figure-preview.d.ts +11 -0
  30. package/dist-client/modeller/figure-preview.js +53 -3
  31. package/dist-client/modeller/figure-preview.js.map +1 -1
  32. package/dist-client/modeller/figure-report.d.ts +8 -4
  33. package/dist-client/modeller/figure-report.js +11 -31
  34. package/dist-client/modeller/figure-report.js.map +1 -1
  35. package/dist-client/modeller/figure-side.js +1 -0
  36. package/dist-client/modeller/figure-side.js.map +1 -1
  37. package/dist-client/modeller/joint-gizmo.d.ts +4 -1
  38. package/dist-client/modeller/joint-gizmo.js +54 -0
  39. package/dist-client/modeller/joint-gizmo.js.map +1 -1
  40. package/dist-client/modeller/part-edits.d.ts +29 -4
  41. package/dist-client/modeller/part-edits.js +61 -9
  42. package/dist-client/modeller/part-edits.js.map +1 -1
  43. package/dist-client/pages/figure-modeller-page.d.ts +7 -0
  44. package/dist-client/pages/figure-modeller-page.js +20 -2
  45. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  46. package/dist-client/tsconfig.tsbuildinfo +1 -1
  47. package/dist-client/types.d.ts +2 -0
  48. package/dist-client/types.js.map +1 -1
  49. package/package.json +5 -5
  50. package/test/angle-dial.test.ts +78 -0
  51. package/test/base-box.test.ts +11 -0
  52. package/test/brand-palette.test.ts +99 -18
  53. package/test/expect.ts +2 -0
  54. package/test/i18n-prefix-guard.test.ts +17 -0
  55. package/test/joint-needle.test.ts +162 -0
  56. package/test/part-edits.test.ts +100 -0
  57. package/test/scene-budget.test.ts +29 -0
  58. package/translations/en.json +3 -7
  59. package/translations/ja.json +3 -7
  60. package/translations/ko.json +3 -7
  61. package/translations/ms.json +3 -7
  62. package/translations/zh.json +3 -7
@@ -27,6 +27,13 @@ function freeName(taken: Set<string>, base: string): string {
27
27
  }
28
28
  }
29
29
 
30
+ const round = (value: number) => Math.round(value)
31
+
32
+ /** 값을 두 끝 사이로 당긴다. 끝이 뒤집혀 있으면(부품이 상자보다 크다) 앞의 끝을 따른다. */
33
+ function clamp(value: number, least: number, most: number): number {
34
+ return Math.max(least, Math.min(most, value))
35
+ }
36
+
30
37
  /** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
31
38
  export function partLimit(draft: FigureDraft): number {
32
39
  return PART_LIMIT[(draft.detailLevel ?? 'M') as DetailLevel]
@@ -35,26 +42,58 @@ export function partLimit(draft: FigureDraft): number {
35
42
  /**
36
43
  * 새 부품 하나를 만든다.
37
44
  *
38
- * 기준 상자의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —
39
- * 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
45
+ * 기준 상자의 3분의 1 크기다. 추가하자마자 화면에 보여야 한다 — 크기 0 으로 놓으면
46
+ * 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
47
+ *
48
+ * ## 어디에 놓나 — 고른 부품 **위**다
49
+ *
50
+ * 전에는 언제나 바닥 한가운데였다. 첫 부품은 그 자리가 맞지만 둘째부터는 **첫 부품 속에**
51
+ * 들어간다 — 같은 자리에 같은 크기로 놓이니 화면에는 덩어리 하나뿐이고, 저작자는 무엇이
52
+ * 늘었는지 못 본다. 「추가하자마자 보여야 한다」는 그때 이미 깨져 있었다.
53
+ *
54
+ * 그래서 고른 부품이 있으면 그 부품의 **윗면**에 얹고 발자국을 가운데로 맞춘다. 기계는
55
+ * 대개 그렇게 쌓인다(받침 → 어깨 → 팔). 관절을 걸 때도 이 자리가 필요하다 — 맞닿는 면이
56
+ * 실제로 있어야 `seamOrigin` 이 회전 중심을 그 면에서 잡는다.
57
+ *
58
+ * 위에 얹으면 기준 상자를 넘는 경우에는 바닥 한가운데로 간다. 넘은 채로 놓으면 발행이
59
+ * 막히고(`part-outside-base`), 그것을 고치는 일이 「부품을 추가했다」의 대가가 된다.
60
+ * 가로·세로는 상자 안으로 당긴다 — 고른 부품이 가장자리에 있으면 그 위가 상자 밖이다.
40
61
  */
41
- export function newPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel {
62
+ export function newPart(
63
+ draft: FigureDraft,
64
+ parts: PartModel[],
65
+ primitive: PrimitiveKind,
66
+ onTopOf?: PartModel
67
+ ): PartModel {
42
68
  const taken = new Set(parts.map(part => part.name))
43
69
 
44
70
  const width = Math.max(1, Math.round(draft.width / 3))
45
71
  const height = Math.max(1, Math.round(draft.height / 3))
46
72
  const depth = Math.max(1, Math.round(draft.depth / 3))
47
73
 
74
+ // 바닥 한가운데 — 고를 부품이 없을 때, 그리고 위가 상자 밖일 때의 자리.
75
+ const middle = {
76
+ left: Math.round((draft.width - width) / 2),
77
+ top: Math.round((draft.height - height) / 2),
78
+ // `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.
79
+ zPos: 0
80
+ }
81
+ const above = onTopOf && {
82
+ left: clamp(Math.round(onTopOf.left + onTopOf.width / 2 - width / 2), 0, draft.width - width),
83
+ top: clamp(Math.round(onTopOf.top + onTopOf.height / 2 - height / 2), 0, draft.height - height),
84
+ zPos: Math.round(onTopOf.zPos + onTopOf.depth)
85
+ }
86
+ const at = above && above.zPos + depth <= draft.depth ? above : middle
87
+
48
88
  const part: PartModel = {
49
89
  type: 'figure-part',
50
90
  name: freeName(taken, primitive),
51
91
  primitive,
52
- left: Math.round((draft.width - width) / 2),
53
- top: Math.round((draft.height - height) / 2),
92
+ left: at.left,
93
+ top: at.top,
54
94
  width,
55
95
  height,
56
- // 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.
57
- zPos: 0,
96
+ zPos: at.zPos,
58
97
  depth,
59
98
  // 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.
60
99
  token: parts[0]?.token ?? 'palette.primary'
@@ -79,8 +118,35 @@ export function newPart(draft: FigureDraft, parts: PartModel[], primitive: Primi
79
118
  return part
80
119
  }
81
120
 
82
- export function addPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel[] {
83
- return [...parts, newPart(draft, parts, primitive)]
121
+ /**
122
+ * 씬에서 온 부품을 밀리미터로 맞춘다.
123
+ *
124
+ * 기즈모는 비율을 곱해서 크기를 낸다 — 420 에 1.72 를 곱하면 722.4180046035418 이고, 손대지
125
+ * 않은 축도 384.99999999999994 처럼 흔들린다. 저작자가 끈 결과가 소수 넷째 자리까지 남을
126
+ * 까닭이 없고, 칸에는 「722.4…」로 잘려 보인다. 이 편집기의 다른 길은 전부 정수 밀리미터로
127
+ * 맞춘다(부품을 새로 놓을 때 · 가운데 맞추기 · 격자 맞추기).
128
+ *
129
+ * 크기는 1 밑으로 내려가지 않는다 — 0 이면 부피가 사라지고 배율이 0 으로 나뉜다.
130
+ */
131
+ export function roundPart(part: PartModel): PartModel {
132
+ return {
133
+ ...part,
134
+ left: round(part.left),
135
+ top: round(part.top),
136
+ zPos: round(part.zPos),
137
+ width: Math.max(1, round(part.width)),
138
+ height: Math.max(1, round(part.height)),
139
+ depth: Math.max(1, round(part.depth))
140
+ }
141
+ }
142
+
143
+ export function addPart(
144
+ draft: FigureDraft,
145
+ parts: PartModel[],
146
+ primitive: PrimitiveKind,
147
+ onTopOf?: PartModel
148
+ ): PartModel[] {
149
+ return [...parts, newPart(draft, parts, primitive, onTopOf)]
84
150
  }
85
151
 
86
152
  export function removePart(parts: PartModel[], index: number): PartModel[] {
@@ -1510,8 +1510,15 @@ export class FigureModellerPage extends FigureModellerPageBase {
1510
1510
  this.dirty = true
1511
1511
  }
1512
1512
 
1513
+ /**
1514
+ * 타입을 고친다 — **발행 전까지만**(ADR-0063 결정 4).
1515
+ *
1516
+ * 전에는 저장된 도형이면 무조건 돌아섰다. 그래서 기계가 지은 이름이 그대로 굳었다. 발행된
1517
+ * 뒤에는 문이 거절하므로 여기서도 막는다 — 눌러서 되는 것처럼 보이고 저장에서 실패하면
1518
+ * 저작자가 무엇을 잘못했는지 못 읽는다.
1519
+ */
1513
1520
  private typeChanged(type: string) {
1514
- if (!type || this.figure?.id) return
1521
+ if (!type || (this.figure?.version ?? 0) > 0) return
1515
1522
  const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()
1516
1523
  if (this.draft) this.draft.figureType = sanitized
1517
1524
  this.figure = { ...this.figure, type: sanitized } as Figure
@@ -1595,8 +1602,13 @@ export class FigureModellerPage extends FigureModellerPageBase {
1595
1602
 
1596
1603
  private addPart(primitive: PrimitiveKind) {
1597
1604
  if (!this.draft) return
1605
+ /*
1606
+ 고른 부품 위에 얹는다 — 그 자리에서 보이고, 관절을 걸면 맞닿는 면이 회전 중심이 된다.
1607
+ 고른 것이 없으면 `newPart` 가 바닥 한가운데로 놓는다.
1608
+ */
1609
+ const onTopOf = this.parts[this.selected]
1598
1610
  // 추가한 것을 바로 수정할 수 있게 선택 상태로 둔다.
1599
- this.put(edits.addPart(this.draft, this.parts, primitive), this.parts.length)
1611
+ this.put(edits.addPart(this.draft, this.parts, primitive, onTopOf), this.parts.length)
1600
1612
  }
1601
1613
 
1602
1614
  private removePart(index: number) {
@@ -1886,6 +1898,12 @@ export class FigureModellerPage extends FigureModellerPageBase {
1886
1898
  const thumbnail = await captureThumbnail(this.draft, this.parts)
1887
1899
 
1888
1900
  const result = await updateFigure(this.figure.id, {
1901
+ /*
1902
+ 타입을 함께 보낸다. 정본 안의 `type` 만 고치고 행을 안 고치면 저장된 JSON 과 행이
1903
+ 다른 이름을 말하고, 발행할 때 어느 쪽이 도면에 적히는지가 갈린다. 발행된 뒤라면
1904
+ 문이 거절하고 그 사유가 화면에 그대로 뜬다.
1905
+ */
1906
+ type: this.figure.type,
1889
1907
  name: this.figure.name,
1890
1908
  category: this.figure.category,
1891
1909
  tags: this.figure.tags,
package/client/types.ts CHANGED
@@ -80,6 +80,8 @@ export interface NewFigure {
80
80
  * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.
81
81
  */
82
82
  export interface FigurePatch {
83
+ /** 발행 전 초안에서만 받는다 — 발행된 뒤에는 문이 거절한다(ADR-0063 결정 4). */
84
+ type?: string
83
85
  name?: string
84
86
  description?: string
85
87
  category?: string
@@ -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/themes/light.css` 가 갖는다. 여기 있는 것은 **제품을
24
+ * 중성색은 여기 없다. 그것은 `client/harness-theme/light.css` 가 갖는다. 여기 있는 것은 **제품을
14
25
  * 가리키는 색**뿐이고, 그것만 절대 갈라지면 안 된다.
15
26
  *
16
27
  * ── 밝은 것과 어두운 것을 함께 든다 ────────────────────────────────────────
@@ -37,15 +48,15 @@
37
48
  * 고른 부품 색**이다. 틀이 진한 색으로 소리를 내면 캔버스 안의 색을 판단할 수 없다. 그래서
38
49
  * 브랜드 색은 깊고 조용하게 두고, 고른 것의 밑줄 · 기본 단추 · 링크에만 좁게 쓴다.
39
50
  */
40
- --brand-figure: #154473;
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-figure-2: #64a9f3;
58
+ --figure-brand-2: #64a9f3;
48
59
 
49
60
  /** 아주 옅게 깔 때 — 고른 줄의 바탕. 위 색과 같은 색상각이다. */
50
- --brand-figure-wash: #eaf3fd;
61
+ --figure-brand-wash: #eaf3fd;
51
62
  }
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ── 브랜드 색이 밝은 쪽으로 바뀐다 ────────────────────────────────────────
10
10
  * `#154473` 은 어두운 바탕에서 대비 1.6 이라 보이지 않는다. 그래서 어두운 면에서는
11
- * `--brand-figure-2`(#64a9f3)가 브랜드 색이다. 이름이 뜻이고 값은 자리에 따라 다르다 —
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-figure-2);
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-figure);
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-figure-2);
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/themes/brand.css` 가 갖는다. 여기서 쓸 수 있는 이름은 `--brand-figure` ·
7
- * `--brand-figure-2` · `--brand-figure-wash` 셋이다. 값을 여기 다시 적지 않는다.
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-figure);
55
+ --md-sys-color-primary: var(--figure-brand);
56
56
  --md-sys-color-on-primary: #ffffff;
57
- --md-sys-color-inverse-primary: var(--brand-figure-2);
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-figure-wash);
64
- --md-sys-color-on-primary-container: var(--brand-figure);
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,32 @@
1
+ /**
2
+ * 막대 옆에 서는 **각도 눈**. 지금 각이 범위의 어디인지를 한 눈에 보인다.
3
+ *
4
+ * ## 왜 3D 로는 모자랐나
5
+ *
6
+ * 관절이 돌아도 화면이 그대로인 경우가 있다 — 제 축에서 대칭인 부품이 그렇다. 그래서 관절이
7
+ * 모는 부품에 바늘을 달아 미리보기에서 함께 돌게 했는데(`joint-gizmo.ts` 의 `jointNeedles`),
8
+ * **미리보기에서는 그 바늘이 4 × 2 픽셀이다**(잰 값: 부품 100mm · 바늘 60 × 12mm · 여덟 대를
9
+ * 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가
10
+ * 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.
11
+ *
12
+ * 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 도형이 아무리
13
+ * 작게 서 있어도 같은 크기로 읽힌다.
14
+ */
15
+ export interface DialShape {
16
+ /** 가동 범위를 채운 부채꼴. SVG path 의 `d`. */
17
+ sector: string;
18
+ /** 지금 값이 가리키는 끝점. */
19
+ needle: {
20
+ x: number;
21
+ y: number;
22
+ };
23
+ centre: {
24
+ x: number;
25
+ y: number;
26
+ };
27
+ radius: number;
28
+ }
29
+ /**
30
+ * 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.
31
+ */
32
+ export declare function dialShape(min: number, max: number, value: number, radius?: number): DialShape;
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ /** 0 도가 위를 가리키고, 값이 커지면 시계 방향으로 돈다 — 화면의 각과 같은 방향이다. */
5
+ function at(centre, radius, degrees) {
6
+ const rad = (degrees * Math.PI) / 180;
7
+ return {
8
+ x: +(centre.x + radius * Math.sin(rad)).toFixed(2),
9
+ y: +(centre.y - radius * Math.cos(rad)).toFixed(2)
10
+ };
11
+ }
12
+ /** 한 바퀴에 가까우면 부채꼴 대신 온전한 원이다. */
13
+ const FULL = 359.9;
14
+ /**
15
+ * 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.
16
+ */
17
+ export function dialShape(min, max, value, radius = 9) {
18
+ const centre = { x: radius + 1, y: radius + 1 };
19
+ const span = Math.abs(max - min);
20
+ const held = Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max));
21
+ if (span >= FULL) {
22
+ const top = at(centre, radius, 0);
23
+ const bottom = at(centre, radius, 180);
24
+ return {
25
+ 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`,
26
+ needle: at(centre, radius, held),
27
+ centre,
28
+ radius
29
+ };
30
+ }
31
+ const from = at(centre, radius, min);
32
+ const to = at(centre, radius, max);
33
+ const large = span > 180 ? 1 : 0;
34
+ return {
35
+ sector: `M ${centre.x} ${centre.y} L ${from.x} ${from.y} A ${radius} ${radius} 0 ${large} 1 ${to.x} ${to.y} Z`,
36
+ needle: at(centre, radius, held),
37
+ centre,
38
+ radius
39
+ };
40
+ }
41
+ //# sourceMappingURL=angle-dial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"angle-dial.js","sourceRoot":"","sources":["../../client/modeller/angle-dial.ts"],"names":[],"mappings":"AAAA;;GAEG;AA0BH,wDAAwD;AACxD,SAAS,EAAE,CAAC,MAAgC,EAAE,MAAc,EAAE,OAAe;IAC3E,MAAM,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAA;IACrC,OAAO;QACL,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QAClD,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;KACnD,CAAA;AACH,CAAC;AAED,iCAAiC;AACjC,MAAM,IAAI,GAAG,KAAK,CAAA;AAElB;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,GAAW,EAAE,KAAa,EAAE,MAAM,GAAG,CAAC;IAC3E,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAAA;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;IAE9E,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;QACjC,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;QACtC,OAAO;YACL,MAAM,EAAE,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,UAAU,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI;YACjI,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC;YAChC,MAAM;YACN,MAAM;SACP,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEhC,OAAO;QACL,MAAM,EAAE,KAAK,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI;QAC9G,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC;QAChC,MAAM;QACN,MAAM;KACP,CAAA;AACH,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\n/**\n * 막대 옆에 서는 **각도 눈**. 지금 각이 범위의 어디인지를 한 눈에 보인다.\n *\n * ## 왜 3D 로는 모자랐나\n *\n * 관절이 돌아도 화면이 그대로인 경우가 있다 — 제 축에서 대칭인 부품이 그렇다. 그래서 관절이\n * 모는 부품에 바늘을 달아 미리보기에서 함께 돌게 했는데(`joint-gizmo.ts` 의 `jointNeedles`),\n * **미리보기에서는 그 바늘이 4 × 2 픽셀이다**(잰 값: 부품 100mm · 바늘 60 × 12mm · 여덟 대를\n * 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가\n * 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.\n *\n * 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 도형이 아무리\n * 작게 서 있어도 같은 크기로 읽힌다.\n */\n\nexport interface DialShape {\n /** 가동 범위를 채운 부채꼴. SVG path 의 `d`. */\n sector: string\n /** 지금 값이 가리키는 끝점. */\n needle: { x: number; y: number }\n centre: { x: number; y: number }\n radius: number\n}\n\n/** 0 도가 위를 가리키고, 값이 커지면 시계 방향으로 돈다 — 화면의 각과 같은 방향이다. */\nfunction at(centre: { x: number; y: number }, radius: number, degrees: number) {\n const rad = (degrees * Math.PI) / 180\n return {\n x: +(centre.x + radius * Math.sin(rad)).toFixed(2),\n y: +(centre.y - radius * Math.cos(rad)).toFixed(2)\n }\n}\n\n/** 한 바퀴에 가까우면 부채꼴 대신 온전한 원이다. */\nconst FULL = 359.9\n\n/**\n * 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.\n */\nexport function dialShape(min: number, max: number, value: number, radius = 9): DialShape {\n const centre = { x: radius + 1, y: radius + 1 }\n const span = Math.abs(max - min)\n const held = Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max))\n\n if (span >= FULL) {\n const top = at(centre, radius, 0)\n const bottom = at(centre, radius, 180)\n return {\n 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`,\n needle: at(centre, radius, held),\n centre,\n radius\n }\n }\n\n const from = at(centre, radius, min)\n const to = at(centre, radius, max)\n const large = span > 180 ? 1 : 0\n\n return {\n sector: `M ${centre.x} ${centre.y} L ${from.x} ${from.y} A ${radius} ${radius} 0 ${large} 1 ${to.x} ${to.y} Z`,\n needle: at(centre, radius, held),\n centre,\n radius\n }\n}\n"]}
@@ -80,11 +80,11 @@ export declare class FigureAnimations extends FigureAnimations_base {
80
80
  private renderJointKeys;
81
81
  private renderKeys;
82
82
  /**
83
- * 움직이는 부품 예산.
83
+ * 따로 움직이는 부품 예산.
84
84
  *
85
- * **넘어도 막지 않는다.** 움직이는 부품은 저마다 단독 묶음이라 draw call 이 늘고 프레임마다
86
- * 갱신이 는다 — 그 대가가 수로 보이면 저작자가 아낀다. 랜드마크 설비 하나는 정교해도 되고,
87
- * 그 값이 도면 예산에 드러나면 된다.
85
+ * **넘어도 막지 않는다.** 제 변환을 지는 부품은 저마다 단독 묶음이라 draw call 이 늘고,
86
+ * 그만큼 한 화면에 함께 세울 수 있는 대수가 준다 — 그 대가가 수로 보이면 저작자가 아낀다.
87
+ * 랜드마크 설비 하나는 정교해도 되고, 그 값이 도면 예산에 드러나면 된다.
88
88
  */
89
89
  private renderBudget;
90
90
  /**
@@ -792,24 +792,24 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
792
792
  `;
793
793
  }
794
794
  /**
795
- * 움직이는 부품 예산.
795
+ * 따로 움직이는 부품 예산.
796
796
  *
797
- * **넘어도 막지 않는다.** 움직이는 부품은 저마다 단독 묶음이라 draw call 이 늘고 프레임마다
798
- * 갱신이 는다 — 그 대가가 수로 보이면 저작자가 아낀다. 랜드마크 설비 하나는 정교해도 되고,
799
- * 그 값이 도면 예산에 드러나면 된다.
797
+ * **넘어도 막지 않는다.** 제 변환을 지는 부품은 저마다 단독 묶음이라 draw call 이 늘고,
798
+ * 그만큼 한 화면에 함께 세울 수 있는 대수가 준다 — 그 대가가 수로 보이면 저작자가 아낀다.
799
+ * 랜드마크 설비 하나는 정교해도 되고, 그 값이 도면 예산에 드러나면 된다.
800
800
  */
801
801
  renderBudget() {
802
802
  const used = this.movingParts;
803
- const over = used > LIMITS.animatedParts;
803
+ const over = used > LIMITS.independentParts;
804
804
  return html `
805
805
  <section>
806
- <h3>${i18next.t('figure.label.budget-animations')}</h3>
806
+ <h3>${i18next.t('figure.label.budget-independent')}</h3>
807
807
  <div budget ?over=${over}>
808
808
  <strong>${used}</strong>
809
- <span quiet>/ ${LIMITS.animatedParts}</span>
809
+ <span quiet>/ ${LIMITS.independentParts}</span>
810
810
  </div>
811
- <p quiet>${i18next.t('figure.text.animated-parts-cost')}</p>
812
- ${over ? html `<p warn>${i18next.t('figure.text.animated-parts-over')}</p>` : nothing}
811
+ <p quiet>${i18next.t('figure.text.independent-parts-cost')}</p>
812
+ ${over ? html `<p warn>${i18next.t('figure.text.independent-parts-over')}</p>` : nothing}
813
813
  </section>
814
814
  `;
815
815
  }