@things-factory/figure-ui 10.1.27 → 10.1.29
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/graphql/index.ts +23 -0
- package/client/modeller/angle-dial.ts +70 -0
- package/client/modeller/base-box.ts +104 -0
- package/client/modeller/figure-animations.ts +90 -17
- package/client/modeller/figure-canvas.ts +24 -6
- package/client/modeller/figure-inspector.ts +169 -5
- package/client/modeller/figure-preview.ts +110 -3
- package/client/modeller/figure-report.ts +20 -32
- package/client/modeller/figure-side.ts +10 -1
- package/client/modeller/figure-source.ts +8 -0
- package/client/modeller/joint-edits.ts +66 -5
- package/client/modeller/joint-gizmo.ts +180 -0
- package/client/modeller/part-edits.ts +75 -9
- package/client/modeller/pose-defaults.ts +67 -0
- package/client/modeller/proposal.ts +9 -3
- package/client/modeller/scene-budget.ts +31 -40
- package/client/pages/figure-list-page.ts +4 -9
- package/client/pages/figure-modeller-page.ts +144 -43
- package/client/types.ts +2 -0
- package/dist-client/graphql/index.d.ts +7 -0
- package/dist-client/graphql/index.js +21 -0
- package/dist-client/graphql/index.js.map +1 -1
- 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/base-box.d.ts +47 -0
- package/dist-client/modeller/base-box.js +52 -0
- package/dist-client/modeller/base-box.js.map +1 -0
- package/dist-client/modeller/figure-animations.d.ts +19 -4
- package/dist-client/modeller/figure-animations.js +86 -17
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +9 -0
- package/dist-client/modeller/figure-canvas.js +20 -6
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +37 -0
- package/dist-client/modeller/figure-inspector.js +159 -5
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +26 -0
- package/dist-client/modeller/figure-preview.js +99 -3
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +11 -6
- package/dist-client/modeller/figure-report.js +20 -35
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +5 -1
- package/dist-client/modeller/figure-side.js +13 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +7 -0
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/joint-edits.d.ts +23 -3
- package/dist-client/modeller/joint-edits.js +53 -5
- package/dist-client/modeller/joint-edits.js.map +1 -1
- package/dist-client/modeller/joint-gizmo.d.ts +34 -0
- package/dist-client/modeller/joint-gizmo.js +158 -0
- package/dist-client/modeller/joint-gizmo.js.map +1 -0
- 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/modeller/pose-defaults.d.ts +9 -0
- package/dist-client/modeller/pose-defaults.js +35 -0
- package/dist-client/modeller/pose-defaults.js.map +1 -0
- package/dist-client/modeller/proposal.d.ts +1 -1
- package/dist-client/modeller/proposal.js +8 -3
- package/dist-client/modeller/proposal.js.map +1 -1
- package/dist-client/modeller/scene-budget.d.ts +31 -34
- package/dist-client/modeller/scene-budget.js +28 -37
- package/dist-client/modeller/scene-budget.js.map +1 -1
- package/dist-client/pages/figure-list-page.js +4 -9
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +50 -0
- package/dist-client/pages/figure-modeller-page.js +141 -42
- 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/ai-proposal-contract.test.ts +16 -11
- package/test/angle-dial.test.ts +78 -0
- package/test/base-box.test.ts +165 -0
- package/test/expect.ts +2 -0
- package/test/i18n-prefix-guard.test.ts +17 -0
- package/test/joint-edits.test.ts +37 -2
- package/test/joint-gizmo.test.ts +174 -0
- package/test/joint-needle.test.ts +162 -0
- package/test/part-edits.test.ts +100 -0
- package/test/pose-defaults.test.ts +71 -0
- package/test/scene-budget.test.ts +82 -0
- package/translations/en.json +16 -9
- package/translations/ja.json +16 -9
- package/translations/ko.json +16 -9
- package/translations/ms.json +16 -9
- package/translations/zh.json +16 -9
package/client/graphql/index.ts
CHANGED
|
@@ -323,6 +323,29 @@ export async function inspectFigure(id: string): Promise<FigureInspection> {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
326
|
+
/**
|
|
327
|
+
* 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.
|
|
328
|
+
*
|
|
329
|
+
* 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장
|
|
330
|
+
* 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.
|
|
331
|
+
*/
|
|
332
|
+
export async function fetchReleasedSource(id: string): Promise<string | undefined> {
|
|
333
|
+
const response = await client.query({
|
|
334
|
+
query: gql`
|
|
335
|
+
query FigureReleased($id: String!) {
|
|
336
|
+
figureReleased(id: $id) {
|
|
337
|
+
version
|
|
338
|
+
source
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
`,
|
|
342
|
+
variables: { id },
|
|
343
|
+
fetchPolicy: 'network-only'
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return unwrap<{ source?: string }>(response, 'figureReleased')?.source ?? undefined
|
|
347
|
+
}
|
|
348
|
+
|
|
326
349
|
export async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {
|
|
327
350
|
const response = await client.query({
|
|
328
351
|
query: gql`
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 기준 상자를 고친다 — **부품은 제자리에 둔 채.**
|
|
9
|
+
*
|
|
10
|
+
* ## 왜 화면에 있어야 하나
|
|
11
|
+
*
|
|
12
|
+
* 기준 상자는 도면에 놓일 때의 배율을 정하는 수다(ADR-0044 ①). 부품이 그 상자를 넘으면 발행이
|
|
13
|
+
* 막히고, 고치는 길은 「상자 숫자 하나를 키우는 것」인데 **화면에 그 칸이 없었다.** 저작자는
|
|
14
|
+
* 넘쳤다는 말만 듣고 고칠 자리를 못 찾았다.
|
|
15
|
+
*
|
|
16
|
+
* ## 상자를 키우면 부품이 움직인다 — 그것을 되돌린다
|
|
17
|
+
*
|
|
18
|
+
* 저작면의 부품 자리는 판의 왼쪽 위에서 잰 값이고, 저장 형식의 자리는 **상자의 한가운데**에서 잰
|
|
19
|
+
* 값이다(`toFigureSource` 가 절반을 뺀다). 그래서 상자만 키우면 한가운데가 옮겨 가고 부품이 전부
|
|
20
|
+
* 반대로 밀린다. 여기서 늘어난 절반만큼 부품을 함께 옮겨, 저장되는 자리가 한 값도 안 바뀌게 한다.
|
|
21
|
+
*
|
|
22
|
+
* ## 「부품에 맞추기」는 한가운데를 지킨다
|
|
23
|
+
*
|
|
24
|
+
* 부품을 감싸는 가장 작은 상자를 만들 때, 부품 쪽을 **옮기지 않는다.** 한가운데에서 가장 먼
|
|
25
|
+
* 부품까지의 거리를 양쪽으로 잡는다. 부품이 한쪽으로 치우쳐 있으면 반대쪽에 여유가 남는데,
|
|
26
|
+
* 그것이 사실이다 — 치우친 것을 가운데로 끌어오면 저작자가 놓은 자리가 소리 없이 달라지고,
|
|
27
|
+
* 발행된 도형에서는 문(port)의 자리가 함께 움직인다.
|
|
28
|
+
*
|
|
29
|
+
* 높이는 밑면이 바닥이라 한쪽만 늘린다. 바닥 아래로 내려간 부품이 있으면 그 부품은 상자 밖에
|
|
30
|
+
* 남는다 — 그것을 담으려면 부품을 올려야 하고, 그것은 저작자가 정할 일이다. 발행 관문이 이미
|
|
31
|
+
* `part-outside-base` 로 말한다.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export interface BaseSize {
|
|
35
|
+
/** 저작면의 가로 — 형식의 `base.x`. */
|
|
36
|
+
width: number
|
|
37
|
+
/** 저작면의 세로 — 형식의 `base.z`. */
|
|
38
|
+
height: number
|
|
39
|
+
/** 높이 — 형식의 `base.y`. */
|
|
40
|
+
depth: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 상자는 밀리미터 하나보다 작아질 수 없다. 0 이 되면 배율이 0 으로 나뉜다. */
|
|
44
|
+
const LEAST = 1
|
|
45
|
+
|
|
46
|
+
const round = (value: number) => Math.round(value)
|
|
47
|
+
|
|
48
|
+
export const baseOf = (draft: FigureDraft): BaseSize => ({
|
|
49
|
+
width: draft.width,
|
|
50
|
+
height: draft.height,
|
|
51
|
+
depth: draft.depth
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/** 부품 전부를 담는 가장 작은 상자. 부품이 없으면 잴 것이 없다. */
|
|
55
|
+
export function baseFittingParts(draft: FigureDraft, parts: readonly PartModel[]): BaseSize | undefined {
|
|
56
|
+
if (parts.length === 0) return undefined
|
|
57
|
+
|
|
58
|
+
const middle = { x: draft.width / 2, y: draft.height / 2 }
|
|
59
|
+
let half = { x: 0, y: 0 }
|
|
60
|
+
let tallest = 0
|
|
61
|
+
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
half = {
|
|
64
|
+
x: Math.max(half.x, Math.abs(part.left - middle.x), Math.abs(part.left + part.width - middle.x)),
|
|
65
|
+
y: Math.max(half.y, Math.abs(part.top - middle.y), Math.abs(part.top + part.height - middle.y))
|
|
66
|
+
}
|
|
67
|
+
tallest = Math.max(tallest, part.zPos + part.depth)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
width: Math.max(LEAST, round(half.x * 2)),
|
|
72
|
+
height: Math.max(LEAST, round(half.y * 2)),
|
|
73
|
+
depth: Math.max(LEAST, round(tallest))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 상자를 이 크기로. 부품은 저장되는 자리가 그대로이도록 함께 옮긴다.
|
|
79
|
+
*
|
|
80
|
+
* 달라질 것이 없으면 **받은 것을 그대로 돌려준다** — 고친 것이 없는데 「저장 안 됨」이 붙지 않게.
|
|
81
|
+
*/
|
|
82
|
+
export function resizeBase(
|
|
83
|
+
draft: FigureDraft,
|
|
84
|
+
parts: readonly PartModel[],
|
|
85
|
+
next: BaseSize
|
|
86
|
+
): { draft: FigureDraft; parts: readonly PartModel[] } {
|
|
87
|
+
const size = {
|
|
88
|
+
width: Math.max(LEAST, round(next.width)),
|
|
89
|
+
height: Math.max(LEAST, round(next.height)),
|
|
90
|
+
depth: Math.max(LEAST, round(next.depth))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (size.width === draft.width && size.height === draft.height && size.depth === draft.depth) {
|
|
94
|
+
return { draft, parts }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const shift = { x: (size.width - draft.width) / 2, y: (size.height - draft.height) / 2 }
|
|
98
|
+
const moved =
|
|
99
|
+
shift.x === 0 && shift.y === 0
|
|
100
|
+
? parts
|
|
101
|
+
: parts.map(part => ({ ...part, left: part.left + shift.x, top: part.top + shift.y }))
|
|
102
|
+
|
|
103
|
+
return { draft: { ...draft, ...size }, parts: moved }
|
|
104
|
+
}
|
|
@@ -130,6 +130,18 @@ function freeName(taken: readonly { name: string }[], stem: string): string {
|
|
|
130
130
|
for (let n = 2; ; n++) if (!used.has(`${stem}-${n}`)) return `${stem}-${n}`
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* 파라미터 범위에 적히는 단위 — **권하는 것이지 가두는 것이 아니다.**
|
|
135
|
+
*
|
|
136
|
+
* 관절이 만드는 파라미터가 `deg` 와 `mm` 을 쓰고(회전과 직선), 그리퍼·문 같은 값은 `%` 를 쓴다.
|
|
137
|
+
* 속도는 `mm/s` 와 `rpm`, 시간은 `s` 다. 이 여섯이 지금 쓰이는 전부이고, 목록에 없는 말을 적는
|
|
138
|
+
* 길은 그대로 열려 있다.
|
|
139
|
+
*/
|
|
140
|
+
const PARAMETER_UNITS = ['deg', 'mm', '%', 'mm/s', 'rpm', 's'] as const
|
|
141
|
+
|
|
142
|
+
/** 목록 대신 손으로 적겠다는 표. `select` 의 값으로 쓴다. */
|
|
143
|
+
const UNIT_TYPE_IT = '__type-it'
|
|
144
|
+
|
|
133
145
|
@customElement('figure-animations')
|
|
134
146
|
export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
135
147
|
static styles = [
|
|
@@ -225,6 +237,21 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
225
237
|
min-width: 3.2em;
|
|
226
238
|
color: var(--md-sys-color-on-surface-variant);
|
|
227
239
|
}
|
|
240
|
+
/*
|
|
241
|
+
칸이 줄어들 줄 알아야 한다. 숫자 입력칸은 제 기본 너비(20자 남짓)를 갖고 있고 flex 는
|
|
242
|
+
기본 크기 아래로는 안 줄이므로, 범위처럼 한 줄에 셋이 서는 줄은 패널 밖으로 밀려
|
|
243
|
+
오른쪽 칸이 잘렸다.
|
|
244
|
+
*/
|
|
245
|
+
div[row] > input {
|
|
246
|
+
flex: 1 1 0;
|
|
247
|
+
min-width: 0;
|
|
248
|
+
}
|
|
249
|
+
/* 단위는 「mm」 정도의 짧은 말이라 숫자 칸보다 좁게 둔다. 고르는 칸도 같은 폭이다. */
|
|
250
|
+
div[row] > input[unit],
|
|
251
|
+
div[row] > select[unit] {
|
|
252
|
+
flex: 0 1 5.5em;
|
|
253
|
+
min-width: 0;
|
|
254
|
+
}
|
|
228
255
|
|
|
229
256
|
/* ── keyframe ─────────────────────────────────────────────────── */
|
|
230
257
|
|
|
@@ -341,6 +368,9 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
341
368
|
@state() private open = ''
|
|
342
369
|
@state() private recipeTarget = ''
|
|
343
370
|
|
|
371
|
+
/** 단위를 손으로 적는 중인 파라미터. 목록에 없는 말을 적으려고 연 칸이다. */
|
|
372
|
+
@state() private typingUnit = new Set<number>()
|
|
373
|
+
|
|
344
374
|
private get clips(): Clip[] {
|
|
345
375
|
return this.draft?.animations ?? []
|
|
346
376
|
}
|
|
@@ -542,14 +572,7 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
542
572
|
aria-label="max"
|
|
543
573
|
@change=${(e: Event) => this.setRange(at, { max: Number((e.target as HTMLInputElement).value) || 0 })}
|
|
544
574
|
/>
|
|
545
|
-
|
|
546
|
-
unit
|
|
547
|
-
.value=${live(range.unit ?? '')}
|
|
548
|
-
placeholder="mm"
|
|
549
|
-
aria-label="unit"
|
|
550
|
-
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
551
|
-
@change=${(e: Event) => this.setRange(at, { unit: (e.target as HTMLInputElement).value.trim() })}
|
|
552
|
-
/>
|
|
575
|
+
${this.renderUnit(at, range.unit ?? '')}
|
|
553
576
|
</div>
|
|
554
577
|
${backwards ? html`<p warn>${i18next.t('figure.text.range-needs-room')}</p>` : nothing}
|
|
555
578
|
|
|
@@ -876,25 +899,25 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
876
899
|
}
|
|
877
900
|
|
|
878
901
|
/**
|
|
879
|
-
* 움직이는 부품 예산.
|
|
902
|
+
* 따로 움직이는 부품 예산.
|
|
880
903
|
*
|
|
881
|
-
* **넘어도 막지 않는다.**
|
|
882
|
-
*
|
|
883
|
-
* 그 값이 도면 예산에 드러나면 된다.
|
|
904
|
+
* **넘어도 막지 않는다.** 제 변환을 지는 부품은 저마다 단독 묶음이라 draw call 이 늘고,
|
|
905
|
+
* 그만큼 한 화면에 함께 세울 수 있는 대수가 준다 — 그 대가가 수로 보이면 저작자가 아낀다.
|
|
906
|
+
* 랜드마크 설비 하나는 정교해도 되고, 그 값이 도면 예산에 드러나면 된다.
|
|
884
907
|
*/
|
|
885
908
|
private renderBudget(): TemplateResult {
|
|
886
909
|
const used = this.movingParts
|
|
887
|
-
const over = used > LIMITS.
|
|
910
|
+
const over = used > LIMITS.independentParts
|
|
888
911
|
|
|
889
912
|
return html`
|
|
890
913
|
<section>
|
|
891
|
-
<h3>${i18next.t('figure.label.budget-
|
|
914
|
+
<h3>${i18next.t('figure.label.budget-independent')}</h3>
|
|
892
915
|
<div budget ?over=${over}>
|
|
893
916
|
<strong>${used}</strong>
|
|
894
|
-
<span quiet>/ ${LIMITS.
|
|
917
|
+
<span quiet>/ ${LIMITS.independentParts}</span>
|
|
895
918
|
</div>
|
|
896
|
-
<p quiet>${i18next.t('figure.text.
|
|
897
|
-
${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}
|
|
898
921
|
</section>
|
|
899
922
|
`
|
|
900
923
|
}
|
|
@@ -1009,6 +1032,56 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
1009
1032
|
this.patchParameter(at, { name: name.trim() })
|
|
1010
1033
|
}
|
|
1011
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* 단위 칸 — **목록이 보이는 채로, 적는 길도 열어 둔다.**
|
|
1037
|
+
*
|
|
1038
|
+
* 처음에는 `datalist` 를 붙였다. 그것은 브라우저가 쥐고 있는 기능이라 **눌러 보기 전에는 목록이
|
|
1039
|
+
* 있는지조차 안 보인다** — 화살표도 없고, 어디에 어떻게 뜨는지도 브라우저가 정한다. 고를 수
|
|
1040
|
+
* 있다는 것을 알리는 것이 이 칸의 목적이므로 `select` 로 바꿨다.
|
|
1041
|
+
*
|
|
1042
|
+
* 형식은 단위를 규칙으로 쓰지 않으므로(표시용 말이다) 목록에 가두지 않는다. 마지막 줄이
|
|
1043
|
+
* 「직접 적기」이고, 목록에 없는 말이 이미 적혀 있으면 처음부터 적는 칸으로 연다.
|
|
1044
|
+
*/
|
|
1045
|
+
private renderUnit(at: number, unit: string) {
|
|
1046
|
+
const listed = (PARAMETER_UNITS as readonly string[]).includes(unit)
|
|
1047
|
+
const typing = this.typingUnit.has(at) || (unit !== '' && !listed)
|
|
1048
|
+
|
|
1049
|
+
if (typing) {
|
|
1050
|
+
return html`<input
|
|
1051
|
+
unit
|
|
1052
|
+
.value=${live(unit)}
|
|
1053
|
+
placeholder="mm"
|
|
1054
|
+
aria-label="unit"
|
|
1055
|
+
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
1056
|
+
@change=${(e: Event) => this.setRange(at, { unit: (e.target as HTMLInputElement).value.trim() })}
|
|
1057
|
+
/>`
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return html`<select
|
|
1061
|
+
unit
|
|
1062
|
+
aria-label="unit"
|
|
1063
|
+
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
1064
|
+
@change=${(e: Event) => this.chooseUnit(at, (e.target as HTMLSelectElement).value)}
|
|
1065
|
+
>
|
|
1066
|
+
<option value="" ?selected=${unit === ''}>${i18next.t('figure.label.unit-none')}</option>
|
|
1067
|
+
${PARAMETER_UNITS.map(one => html`<option value=${one} ?selected=${one === unit}>${one}</option>`)}
|
|
1068
|
+
<option value=${UNIT_TYPE_IT}>${i18next.t('figure.label.unit-type-it')}</option>
|
|
1069
|
+
</select>`
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** 고른 것을 넣는다. 「직접 적기」는 값이 아니라 **칸을 바꾸는 것**이다. */
|
|
1073
|
+
private chooseUnit(at: number, value: string): void {
|
|
1074
|
+
if (value === UNIT_TYPE_IT) {
|
|
1075
|
+
this.typingUnit = new Set([...this.typingUnit, at])
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const next = new Set(this.typingUnit)
|
|
1080
|
+
next.delete(at)
|
|
1081
|
+
this.typingUnit = next
|
|
1082
|
+
this.setRange(at, { unit: value })
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1012
1085
|
private setRange(at: number, patch: Partial<Parameter['range']>): void {
|
|
1013
1086
|
const parameter = this.parameters[at]
|
|
1014
1087
|
if (!parameter) return
|
|
@@ -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'
|
|
@@ -192,6 +192,14 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
192
192
|
/** 지금 고른 부품 이름. 목록에서 고른 것을 캔버스에 알린다. */
|
|
193
193
|
@property({ type: String }) picked?: string
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 관절이 도는 자리를 그리는 부채꼴(`joint-gizmo.ts`). 저작면에만 있고 저장되지 않는다.
|
|
197
|
+
*
|
|
198
|
+
* 부품과 같은 길로 세운다 — 씬에 겹쳐 그리는 통로를 따로 뚫지 않는다. `locked` 라 클릭도
|
|
199
|
+
* 마퀴도 집지 않는다.
|
|
200
|
+
*/
|
|
201
|
+
@property({ type: Object }) gizmo?: PartModel
|
|
202
|
+
|
|
195
203
|
/**
|
|
196
204
|
* 보기 전용 — 씬에서 일어난 일을 밖에 알리지 않는다.
|
|
197
205
|
*
|
|
@@ -278,6 +286,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
278
286
|
|
|
279
287
|
if (changed.has('revision')) this.standing = undefined
|
|
280
288
|
if (changed.has('draft') || changed.has('parts') || changed.has('figureId') || changed.has('revision')) this.standUp()
|
|
289
|
+
else if (changed.has('gizmo')) this.syncParts()
|
|
281
290
|
if (changed.has('view')) this.applyView()
|
|
282
291
|
if (changed.has('picked')) this.showPicked()
|
|
283
292
|
}
|
|
@@ -358,7 +367,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
358
367
|
없어야 한다: 좌표 모눈과 겹쳐 싸우고 원점 아래쪽을 가린다.
|
|
359
368
|
*/
|
|
360
369
|
fillStyle: PAPER,
|
|
361
|
-
components: this.
|
|
370
|
+
components: this.standing3d().map(part => ({ ...part }))
|
|
362
371
|
},
|
|
363
372
|
// **편집 모드**로 만든다. 도면의 3D 모델러가 그 모드에서 카메라 시점 버튼 ·
|
|
364
373
|
// 기즈모 모드 · 도움말 · 단축키를 띄운다.
|
|
@@ -449,11 +458,17 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
449
458
|
* 다시 세우면 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로
|
|
450
459
|
* 돌아간다.
|
|
451
460
|
*/
|
|
461
|
+
/** 판에 세울 것 전부 — 저작한 부품과, 있으면 관절 부채꼴. */
|
|
462
|
+
private standing3d(): PartModel[] {
|
|
463
|
+
return this.gizmo ? [...this.parts, this.gizmo] : this.parts
|
|
464
|
+
}
|
|
465
|
+
|
|
452
466
|
private syncParts() {
|
|
453
467
|
const root = this.scene?.root
|
|
454
468
|
if (!root) return
|
|
455
469
|
|
|
456
|
-
const
|
|
470
|
+
const all = this.standing3d()
|
|
471
|
+
const wanted = new Map(all.map(part => [part.name, part]))
|
|
457
472
|
const standing = this.partsOnCanvas()
|
|
458
473
|
|
|
459
474
|
// 없어진 것을 제거한다
|
|
@@ -464,7 +479,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
464
479
|
}
|
|
465
480
|
}
|
|
466
481
|
|
|
467
|
-
for (const part of
|
|
482
|
+
for (const part of all) {
|
|
468
483
|
const found = standing.get(part.name)
|
|
469
484
|
if (found) {
|
|
470
485
|
/*
|
|
@@ -521,8 +536,11 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
521
536
|
const was = this.parts.find(part => part.name === name)
|
|
522
537
|
if (!was) return
|
|
523
538
|
|
|
524
|
-
|
|
525
|
-
|
|
539
|
+
/*
|
|
540
|
+
씬 모델 그대로 올려 보낸다 — 모델러 페이지가 갖고 있는 것도 씬 모델이라 변환할 것이 없다.
|
|
541
|
+
밀리미터로만 맞춘다: 기즈모는 비율을 곱해 크기를 내므로 소수가 그대로 남는다.
|
|
542
|
+
*/
|
|
543
|
+
this.tell('part-changed', { name, part: roundPart({ ...was, ...changed!.state } as PartModel) })
|
|
526
544
|
})
|
|
527
545
|
}
|
|
528
546
|
|