@things-factory/figure-ui 10.1.28 → 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/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 +55 -3
- 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/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/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
|
@@ -23,6 +23,11 @@ function freeName(taken, base) {
|
|
|
23
23
|
return candidate;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
const round = (value) => Math.round(value);
|
|
27
|
+
/** 값을 두 끝 사이로 당긴다. 끝이 뒤집혀 있으면(부품이 상자보다 크다) 앞의 끝을 따른다. */
|
|
28
|
+
function clamp(value, least, most) {
|
|
29
|
+
return Math.max(least, Math.min(most, value));
|
|
30
|
+
}
|
|
26
31
|
/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
|
|
27
32
|
export function partLimit(draft) {
|
|
28
33
|
return PART_LIMIT[(draft.detailLevel ?? 'M')];
|
|
@@ -30,24 +35,50 @@ export function partLimit(draft) {
|
|
|
30
35
|
/**
|
|
31
36
|
* 새 부품 하나를 만든다.
|
|
32
37
|
*
|
|
33
|
-
* 기준 상자의 3분의 1
|
|
34
|
-
*
|
|
38
|
+
* 기준 상자의 3분의 1 크기다. 추가하자마자 화면에 보여야 한다 — 크기 0 으로 놓으면
|
|
39
|
+
* 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
|
|
40
|
+
*
|
|
41
|
+
* ## 어디에 놓나 — 고른 부품 **위**다
|
|
42
|
+
*
|
|
43
|
+
* 전에는 언제나 바닥 한가운데였다. 첫 부품은 그 자리가 맞지만 둘째부터는 **첫 부품 속에**
|
|
44
|
+
* 들어간다 — 같은 자리에 같은 크기로 놓이니 화면에는 덩어리 하나뿐이고, 저작자는 무엇이
|
|
45
|
+
* 늘었는지 못 본다. 「추가하자마자 보여야 한다」는 그때 이미 깨져 있었다.
|
|
46
|
+
*
|
|
47
|
+
* 그래서 고른 부품이 있으면 그 부품의 **윗면**에 얹고 발자국을 가운데로 맞춘다. 기계는
|
|
48
|
+
* 대개 그렇게 쌓인다(받침 → 어깨 → 팔). 관절을 걸 때도 이 자리가 필요하다 — 맞닿는 면이
|
|
49
|
+
* 실제로 있어야 `seamOrigin` 이 회전 중심을 그 면에서 잡는다.
|
|
50
|
+
*
|
|
51
|
+
* 위에 얹으면 기준 상자를 넘는 경우에는 바닥 한가운데로 간다. 넘은 채로 놓으면 발행이
|
|
52
|
+
* 막히고(`part-outside-base`), 그것을 고치는 일이 「부품을 추가했다」의 대가가 된다.
|
|
53
|
+
* 가로·세로는 상자 안으로 당긴다 — 고른 부품이 가장자리에 있으면 그 위가 상자 밖이다.
|
|
35
54
|
*/
|
|
36
|
-
export function newPart(draft, parts, primitive) {
|
|
55
|
+
export function newPart(draft, parts, primitive, onTopOf) {
|
|
37
56
|
const taken = new Set(parts.map(part => part.name));
|
|
38
57
|
const width = Math.max(1, Math.round(draft.width / 3));
|
|
39
58
|
const height = Math.max(1, Math.round(draft.height / 3));
|
|
40
59
|
const depth = Math.max(1, Math.round(draft.depth / 3));
|
|
60
|
+
// 바닥 한가운데 — 고를 부품이 없을 때, 그리고 위가 상자 밖일 때의 자리.
|
|
61
|
+
const middle = {
|
|
62
|
+
left: Math.round((draft.width - width) / 2),
|
|
63
|
+
top: Math.round((draft.height - height) / 2),
|
|
64
|
+
// `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.
|
|
65
|
+
zPos: 0
|
|
66
|
+
};
|
|
67
|
+
const above = onTopOf && {
|
|
68
|
+
left: clamp(Math.round(onTopOf.left + onTopOf.width / 2 - width / 2), 0, draft.width - width),
|
|
69
|
+
top: clamp(Math.round(onTopOf.top + onTopOf.height / 2 - height / 2), 0, draft.height - height),
|
|
70
|
+
zPos: Math.round(onTopOf.zPos + onTopOf.depth)
|
|
71
|
+
};
|
|
72
|
+
const at = above && above.zPos + depth <= draft.depth ? above : middle;
|
|
41
73
|
const part = {
|
|
42
74
|
type: 'figure-part',
|
|
43
75
|
name: freeName(taken, primitive),
|
|
44
76
|
primitive,
|
|
45
|
-
left:
|
|
46
|
-
top:
|
|
77
|
+
left: at.left,
|
|
78
|
+
top: at.top,
|
|
47
79
|
width,
|
|
48
80
|
height,
|
|
49
|
-
|
|
50
|
-
zPos: 0,
|
|
81
|
+
zPos: at.zPos,
|
|
51
82
|
depth,
|
|
52
83
|
// 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.
|
|
53
84
|
token: parts[0]?.token ?? 'palette.primary'
|
|
@@ -69,8 +100,29 @@ export function newPart(draft, parts, primitive) {
|
|
|
69
100
|
}
|
|
70
101
|
return part;
|
|
71
102
|
}
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
/**
|
|
104
|
+
* 씬에서 온 부품을 밀리미터로 맞춘다.
|
|
105
|
+
*
|
|
106
|
+
* 기즈모는 비율을 곱해서 크기를 낸다 — 420 에 1.72 를 곱하면 722.4180046035418 이고, 손대지
|
|
107
|
+
* 않은 축도 384.99999999999994 처럼 흔들린다. 저작자가 끈 결과가 소수 넷째 자리까지 남을
|
|
108
|
+
* 까닭이 없고, 칸에는 「722.4…」로 잘려 보인다. 이 편집기의 다른 길은 전부 정수 밀리미터로
|
|
109
|
+
* 맞춘다(부품을 새로 놓을 때 · 가운데 맞추기 · 격자 맞추기).
|
|
110
|
+
*
|
|
111
|
+
* 크기는 1 밑으로 내려가지 않는다 — 0 이면 부피가 사라지고 배율이 0 으로 나뉜다.
|
|
112
|
+
*/
|
|
113
|
+
export function roundPart(part) {
|
|
114
|
+
return {
|
|
115
|
+
...part,
|
|
116
|
+
left: round(part.left),
|
|
117
|
+
top: round(part.top),
|
|
118
|
+
zPos: round(part.zPos),
|
|
119
|
+
width: Math.max(1, round(part.width)),
|
|
120
|
+
height: Math.max(1, round(part.height)),
|
|
121
|
+
depth: Math.max(1, round(part.depth))
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function addPart(draft, parts, primitive, onTopOf) {
|
|
125
|
+
return [...parts, newPart(draft, parts, primitive, onTopOf)];
|
|
74
126
|
}
|
|
75
127
|
export function removePart(parts, index) {
|
|
76
128
|
return parts.filter((_, at) => at !== index);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"part-edits.js","sourceRoot":"","sources":["../../client/modeller/part-edits.ts"],"names":[],"mappings":"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","sourcesContent":["import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model'\nimport type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport { AXIS_KEYS, draftExtent } from './figure-source.js'\nimport type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js'\n\n/**\n * 부품 편집 규칙. **씬 모델의 용어로** 다룬다.\n *\n * 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.\n * 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.\n * FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.\n *\n * 사용자가 보는 축(X·Y·Z)이 씬의 어느 속성인지도 `figure-source.ts` 의 `AXIS_KEYS` 가 기준이다.\n * 여기서 다시 정의하지 않는다.\n *\n * 모든 함수가 **새 배열·새 객체를 반환한다** — 원본을 수정하지 않는다. Lit 이 변경을\n * 감지하려면 참조가 달라져야 하고, 실행 취소를 붙일 때도 이전 상태가 남아 있어야 한다.\n */\n\n/** 중복되지 않는 이름을 만든다. 부품 이름은 저장되는 식별자라 중복되면 검증에서 걸린다. */\nfunction freeName(taken: Set<string>, base: string): string {\n if (!taken.has(base)) return base\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}`\n if (!taken.has(candidate)) return candidate\n }\n}\n\n/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */\nexport function partLimit(draft: FigureDraft): number {\n return PART_LIMIT[(draft.detailLevel ?? 'M') as DetailLevel]\n}\n\n/**\n * 새 부품 하나를 만든다.\n *\n * 기준 상자의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —\n * 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.\n */\nexport function newPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel {\n const taken = new Set(parts.map(part => part.name))\n\n const width = Math.max(1, Math.round(draft.width / 3))\n const height = Math.max(1, Math.round(draft.height / 3))\n const depth = Math.max(1, Math.round(draft.depth / 3))\n\n const part: PartModel = {\n type: 'figure-part',\n name: freeName(taken, primitive),\n primitive,\n left: Math.round((draft.width - width) / 2),\n top: Math.round((draft.height - height) / 2),\n width,\n height,\n // 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.\n zPos: 0,\n depth,\n // 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.\n token: parts[0]?.token ?? 'palette.primary'\n }\n\n if (SEGMENTED_PRIMITIVES.includes(primitive)) {\n part.segments = SEGMENT_PRESETS[1]\n }\n if (EXTRUDE_PRIMITIVES.includes(primitive)) {\n part.round = 0\n }\n if (primitive === 'polygon') {\n // polygon 은 점이 3개 이상이어야 한다. 없으면 검증에서 막히므로 삼각형으로 시작한다.\n // 단면의 점은 부품 중심을 기준으로 적는다.\n part.path = [\n { x: -Math.round(width / 2), y: -Math.round(height / 2) },\n { x: Math.round(width / 2), y: -Math.round(height / 2) },\n { x: 0, y: Math.round(height / 2) }\n ]\n }\n\n return part\n}\n\nexport function addPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel[] {\n return [...parts, newPart(draft, parts, primitive)]\n}\n\nexport function removePart(parts: PartModel[], index: number): PartModel[] {\n return parts.filter((_, at) => at !== index)\n}\n\n/** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */\nexport function duplicatePart(parts: PartModel[], index: number): PartModel[] {\n const origin = parts[index]\n if (!origin) return parts\n\n const copy: PartModel = JSON.parse(JSON.stringify(origin))\n copy.name = freeName(new Set(parts.map(part => part.name)), `${origin.name}-copy`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return next\n}\n\n/** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */\nexport function movePart(parts: PartModel[], index: number, to: number): PartModel[] {\n if (to < 0 || to >= parts.length || index === to) return parts\n\n const next = [...parts]\n const [moved] = next.splice(index, 1)\n next.splice(to, 0, moved!)\n return next\n}\n\nexport function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[] {\n return parts.map((old, at) => (at === index ? part : old))\n}\n\n/**\n * 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.\n *\n * 도면 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을\n * **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로\n * 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.\n */\n\nfunction put(parts: PartModel[], index: number, fields: Partial<PartModel>): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n return updatePart(parts, index, { ...part, ...fields })\n}\n\n/** 위치의 한 축만 바꾼다. 어느 속성인지는 `AXIS_KEYS` 가 결정한다. */\nfunction putAt(parts: PartModel[], index: number, axis: PlaneAxis, value: number): PartModel[] {\n return put(parts, index, { [AXIS_KEYS[axis].at]: value } as Partial<PartModel>)\n}\n\n/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */\nexport function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2))\n}\n\n/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */\nexport function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n return putAt(parts, index, axis, 0)\n}\n\n/** 부품을 상자의 끝에 붙인다. */\nexport function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]))\n}\n\n/**\n * 부품을 축 기준으로 대칭 복제한다.\n *\n * 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수\n * 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.\n */\nexport function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part || !canMirror(draft, parts, index, axis)) return parts\n\n const copy = JSON.parse(JSON.stringify(part)) as PartModel\n copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return putAt(next, index + 1, axis, mirroredAt(draft, part, axis))\n}\n\n/** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */\nfunction mirroredAt(draft: FigureDraft, part: PartModel, axis: PlaneAxis): number {\n const { at, size } = AXIS_KEYS[axis]\n return Math.round(draftExtent(draft, axis) - part[at] - part[size])\n}\n\n/**\n * 대칭이 의미가 있는지.\n *\n * 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는\n * 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.\n * 만들어 놓고 알리는 대신 미리 막는다.\n */\nexport function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean {\n const part = parts[index]\n if (!part) return false\n\n return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at])\n}\n\n/**\n * 부품의 위치와 크기를 격자에 맞춘다.\n *\n * 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰\n * 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.\n */\nexport function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n const step = gridStep(draft)\n const round = (value: number) => Math.round(value / step) * step\n\n return put(parts, index, {\n left: round(part.left),\n top: round(part.top),\n zPos: round(part.zPos),\n // 크기는 0 이 되면 안 된다 — 부품이 사라진 것처럼 보인다\n width: Math.max(step, round(part.width)),\n height: Math.max(step, round(part.height)),\n depth: Math.max(step, round(part.depth))\n })\n}\n\n/**\n * 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.\n *\n * **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워\n * (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가\n * 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.\n */\nexport function gridStep(draft: FigureDraft): number {\n return niceStep(Math.max(draft.width, draft.height, draft.depth))\n}\n\n/**\n * 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.\n *\n * 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기\n * 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.\n *\n * 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이\n * 먼저 보였다.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"part-edits.js","sourceRoot":"","sources":["../../client/modeller/part-edits.ts"],"names":[],"mappings":"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","sourcesContent":["import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model'\nimport type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport { AXIS_KEYS, draftExtent } from './figure-source.js'\nimport type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js'\n\n/**\n * 부품 편집 규칙. **씬 모델의 용어로** 다룬다.\n *\n * 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.\n * 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.\n * FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.\n *\n * 사용자가 보는 축(X·Y·Z)이 씬의 어느 속성인지도 `figure-source.ts` 의 `AXIS_KEYS` 가 기준이다.\n * 여기서 다시 정의하지 않는다.\n *\n * 모든 함수가 **새 배열·새 객체를 반환한다** — 원본을 수정하지 않는다. Lit 이 변경을\n * 감지하려면 참조가 달라져야 하고, 실행 취소를 붙일 때도 이전 상태가 남아 있어야 한다.\n */\n\n/** 중복되지 않는 이름을 만든다. 부품 이름은 저장되는 식별자라 중복되면 검증에서 걸린다. */\nfunction freeName(taken: Set<string>, base: string): string {\n if (!taken.has(base)) return base\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}`\n if (!taken.has(candidate)) return candidate\n }\n}\n\nconst round = (value: number) => Math.round(value)\n\n/** 값을 두 끝 사이로 당긴다. 끝이 뒤집혀 있으면(부품이 상자보다 크다) 앞의 끝을 따른다. */\nfunction clamp(value: number, least: number, most: number): number {\n return Math.max(least, Math.min(most, value))\n}\n\n/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */\nexport function partLimit(draft: FigureDraft): number {\n return PART_LIMIT[(draft.detailLevel ?? 'M') as DetailLevel]\n}\n\n/**\n * 새 부품 하나를 만든다.\n *\n * 기준 상자의 3분의 1 크기다. 추가하자마자 화면에 보여야 한다 — 크기 0 으로 놓으면\n * 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.\n *\n * ## 어디에 놓나 — 고른 부품 **위**다\n *\n * 전에는 언제나 바닥 한가운데였다. 첫 부품은 그 자리가 맞지만 둘째부터는 **첫 부품 속에**\n * 들어간다 — 같은 자리에 같은 크기로 놓이니 화면에는 덩어리 하나뿐이고, 저작자는 무엇이\n * 늘었는지 못 본다. 「추가하자마자 보여야 한다」는 그때 이미 깨져 있었다.\n *\n * 그래서 고른 부품이 있으면 그 부품의 **윗면**에 얹고 발자국을 가운데로 맞춘다. 기계는\n * 대개 그렇게 쌓인다(받침 → 어깨 → 팔). 관절을 걸 때도 이 자리가 필요하다 — 맞닿는 면이\n * 실제로 있어야 `seamOrigin` 이 회전 중심을 그 면에서 잡는다.\n *\n * 위에 얹으면 기준 상자를 넘는 경우에는 바닥 한가운데로 간다. 넘은 채로 놓으면 발행이\n * 막히고(`part-outside-base`), 그것을 고치는 일이 「부품을 추가했다」의 대가가 된다.\n * 가로·세로는 상자 안으로 당긴다 — 고른 부품이 가장자리에 있으면 그 위가 상자 밖이다.\n */\nexport function newPart(\n draft: FigureDraft,\n parts: PartModel[],\n primitive: PrimitiveKind,\n onTopOf?: PartModel\n): PartModel {\n const taken = new Set(parts.map(part => part.name))\n\n const width = Math.max(1, Math.round(draft.width / 3))\n const height = Math.max(1, Math.round(draft.height / 3))\n const depth = Math.max(1, Math.round(draft.depth / 3))\n\n // 바닥 한가운데 — 고를 부품이 없을 때, 그리고 위가 상자 밖일 때의 자리.\n const middle = {\n left: Math.round((draft.width - width) / 2),\n top: Math.round((draft.height - height) / 2),\n // `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.\n zPos: 0\n }\n const above = onTopOf && {\n left: clamp(Math.round(onTopOf.left + onTopOf.width / 2 - width / 2), 0, draft.width - width),\n top: clamp(Math.round(onTopOf.top + onTopOf.height / 2 - height / 2), 0, draft.height - height),\n zPos: Math.round(onTopOf.zPos + onTopOf.depth)\n }\n const at = above && above.zPos + depth <= draft.depth ? above : middle\n\n const part: PartModel = {\n type: 'figure-part',\n name: freeName(taken, primitive),\n primitive,\n left: at.left,\n top: at.top,\n width,\n height,\n zPos: at.zPos,\n depth,\n // 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.\n token: parts[0]?.token ?? 'palette.primary'\n }\n\n if (SEGMENTED_PRIMITIVES.includes(primitive)) {\n part.segments = SEGMENT_PRESETS[1]\n }\n if (EXTRUDE_PRIMITIVES.includes(primitive)) {\n part.round = 0\n }\n if (primitive === 'polygon') {\n // polygon 은 점이 3개 이상이어야 한다. 없으면 검증에서 막히므로 삼각형으로 시작한다.\n // 단면의 점은 부품 중심을 기준으로 적는다.\n part.path = [\n { x: -Math.round(width / 2), y: -Math.round(height / 2) },\n { x: Math.round(width / 2), y: -Math.round(height / 2) },\n { x: 0, y: Math.round(height / 2) }\n ]\n }\n\n return part\n}\n\n/**\n * 씬에서 온 부품을 밀리미터로 맞춘다.\n *\n * 기즈모는 비율을 곱해서 크기를 낸다 — 420 에 1.72 를 곱하면 722.4180046035418 이고, 손대지\n * 않은 축도 384.99999999999994 처럼 흔들린다. 저작자가 끈 결과가 소수 넷째 자리까지 남을\n * 까닭이 없고, 칸에는 「722.4…」로 잘려 보인다. 이 편집기의 다른 길은 전부 정수 밀리미터로\n * 맞춘다(부품을 새로 놓을 때 · 가운데 맞추기 · 격자 맞추기).\n *\n * 크기는 1 밑으로 내려가지 않는다 — 0 이면 부피가 사라지고 배율이 0 으로 나뉜다.\n */\nexport function roundPart(part: PartModel): PartModel {\n return {\n ...part,\n left: round(part.left),\n top: round(part.top),\n zPos: round(part.zPos),\n width: Math.max(1, round(part.width)),\n height: Math.max(1, round(part.height)),\n depth: Math.max(1, round(part.depth))\n }\n}\n\nexport function addPart(\n draft: FigureDraft,\n parts: PartModel[],\n primitive: PrimitiveKind,\n onTopOf?: PartModel\n): PartModel[] {\n return [...parts, newPart(draft, parts, primitive, onTopOf)]\n}\n\nexport function removePart(parts: PartModel[], index: number): PartModel[] {\n return parts.filter((_, at) => at !== index)\n}\n\n/** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */\nexport function duplicatePart(parts: PartModel[], index: number): PartModel[] {\n const origin = parts[index]\n if (!origin) return parts\n\n const copy: PartModel = JSON.parse(JSON.stringify(origin))\n copy.name = freeName(new Set(parts.map(part => part.name)), `${origin.name}-copy`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return next\n}\n\n/** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */\nexport function movePart(parts: PartModel[], index: number, to: number): PartModel[] {\n if (to < 0 || to >= parts.length || index === to) return parts\n\n const next = [...parts]\n const [moved] = next.splice(index, 1)\n next.splice(to, 0, moved!)\n return next\n}\n\nexport function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[] {\n return parts.map((old, at) => (at === index ? part : old))\n}\n\n/**\n * 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.\n *\n * 도면 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을\n * **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로\n * 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.\n */\n\nfunction put(parts: PartModel[], index: number, fields: Partial<PartModel>): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n return updatePart(parts, index, { ...part, ...fields })\n}\n\n/** 위치의 한 축만 바꾼다. 어느 속성인지는 `AXIS_KEYS` 가 결정한다. */\nfunction putAt(parts: PartModel[], index: number, axis: PlaneAxis, value: number): PartModel[] {\n return put(parts, index, { [AXIS_KEYS[axis].at]: value } as Partial<PartModel>)\n}\n\n/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */\nexport function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2))\n}\n\n/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */\nexport function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n return putAt(parts, index, axis, 0)\n}\n\n/** 부품을 상자의 끝에 붙인다. */\nexport function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]))\n}\n\n/**\n * 부품을 축 기준으로 대칭 복제한다.\n *\n * 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수\n * 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.\n */\nexport function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part || !canMirror(draft, parts, index, axis)) return parts\n\n const copy = JSON.parse(JSON.stringify(part)) as PartModel\n copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return putAt(next, index + 1, axis, mirroredAt(draft, part, axis))\n}\n\n/** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */\nfunction mirroredAt(draft: FigureDraft, part: PartModel, axis: PlaneAxis): number {\n const { at, size } = AXIS_KEYS[axis]\n return Math.round(draftExtent(draft, axis) - part[at] - part[size])\n}\n\n/**\n * 대칭이 의미가 있는지.\n *\n * 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는\n * 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.\n * 만들어 놓고 알리는 대신 미리 막는다.\n */\nexport function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean {\n const part = parts[index]\n if (!part) return false\n\n return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at])\n}\n\n/**\n * 부품의 위치와 크기를 격자에 맞춘다.\n *\n * 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰\n * 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.\n */\nexport function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n const step = gridStep(draft)\n const round = (value: number) => Math.round(value / step) * step\n\n return put(parts, index, {\n left: round(part.left),\n top: round(part.top),\n zPos: round(part.zPos),\n // 크기는 0 이 되면 안 된다 — 부품이 사라진 것처럼 보인다\n width: Math.max(step, round(part.width)),\n height: Math.max(step, round(part.height)),\n depth: Math.max(step, round(part.depth))\n })\n}\n\n/**\n * 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.\n *\n * **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워\n * (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가\n * 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.\n */\nexport function gridStep(draft: FigureDraft): number {\n return niceStep(Math.max(draft.width, draft.height, draft.depth))\n}\n\n/**\n * 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.\n *\n * 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기\n * 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.\n *\n * 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이\n * 먼저 보였다.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
|
|
@@ -258,6 +258,13 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
258
258
|
private loadReleased;
|
|
259
259
|
private startNew;
|
|
260
260
|
private rename;
|
|
261
|
+
/**
|
|
262
|
+
* 타입을 고친다 — **발행 전까지만**(ADR-0063 결정 4).
|
|
263
|
+
*
|
|
264
|
+
* 전에는 저장된 도형이면 무조건 돌아섰다. 그래서 기계가 지은 이름이 그대로 굳었다. 발행된
|
|
265
|
+
* 뒤에는 문이 거절하므로 여기서도 막는다 — 눌러서 되는 것처럼 보이고 저장에서 실패하면
|
|
266
|
+
* 저작자가 무엇을 잘못했는지 못 읽는다.
|
|
267
|
+
*/
|
|
261
268
|
private typeChanged;
|
|
262
269
|
/**
|
|
263
270
|
* 분류와 태그를 고친다.
|
|
@@ -1410,8 +1410,15 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1410
1410
|
this.figure = { ...this.figure, name };
|
|
1411
1411
|
this.dirty = true;
|
|
1412
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* 타입을 고친다 — **발행 전까지만**(ADR-0063 결정 4).
|
|
1415
|
+
*
|
|
1416
|
+
* 전에는 저장된 도형이면 무조건 돌아섰다. 그래서 기계가 지은 이름이 그대로 굳었다. 발행된
|
|
1417
|
+
* 뒤에는 문이 거절하므로 여기서도 막는다 — 눌러서 되는 것처럼 보이고 저장에서 실패하면
|
|
1418
|
+
* 저작자가 무엇을 잘못했는지 못 읽는다.
|
|
1419
|
+
*/
|
|
1413
1420
|
typeChanged(type) {
|
|
1414
|
-
if (!type || this.figure?.
|
|
1421
|
+
if (!type || (this.figure?.version ?? 0) > 0)
|
|
1415
1422
|
return;
|
|
1416
1423
|
const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();
|
|
1417
1424
|
if (this.draft)
|
|
@@ -1489,8 +1496,13 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1489
1496
|
addPart(primitive) {
|
|
1490
1497
|
if (!this.draft)
|
|
1491
1498
|
return;
|
|
1499
|
+
/*
|
|
1500
|
+
고른 부품 위에 얹는다 — 그 자리에서 보이고, 관절을 걸면 맞닿는 면이 회전 중심이 된다.
|
|
1501
|
+
고른 것이 없으면 `newPart` 가 바닥 한가운데로 놓는다.
|
|
1502
|
+
*/
|
|
1503
|
+
const onTopOf = this.parts[this.selected];
|
|
1492
1504
|
// 추가한 것을 바로 수정할 수 있게 선택 상태로 둔다.
|
|
1493
|
-
this.put(edits.addPart(this.draft, this.parts, primitive), this.parts.length);
|
|
1505
|
+
this.put(edits.addPart(this.draft, this.parts, primitive, onTopOf), this.parts.length);
|
|
1494
1506
|
}
|
|
1495
1507
|
removePart(index) {
|
|
1496
1508
|
this.put(edits.removePart(this.parts, index), Math.max(0, index - 1));
|
|
@@ -1758,6 +1770,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1758
1770
|
const sourceJson = JSON.stringify(this.folded);
|
|
1759
1771
|
const thumbnail = await captureThumbnail(this.draft, this.parts);
|
|
1760
1772
|
const result = await updateFigure(this.figure.id, {
|
|
1773
|
+
/*
|
|
1774
|
+
타입을 함께 보낸다. 정본 안의 `type` 만 고치고 행을 안 고치면 저장된 JSON 과 행이
|
|
1775
|
+
다른 이름을 말하고, 발행할 때 어느 쪽이 도면에 적히는지가 갈린다. 발행된 뒤라면
|
|
1776
|
+
문이 거절하고 그 사유가 화면에 그대로 뜬다.
|
|
1777
|
+
*/
|
|
1778
|
+
type: this.figure.type,
|
|
1761
1779
|
name: this.figure.name,
|
|
1762
1780
|
category: this.figure.category,
|
|
1763
1781
|
tags: this.figure.tags,
|