@things-factory/figure-ui 10.1.65 → 10.1.67
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 +0 -45
- package/client/modeller/figure-history-panel.ts +6 -145
- package/client/modeller/figure-v3-capability.ts +2 -2
- package/client/modeller/figure-v3-clips.ts +2 -2
- package/client/modeller/figure-v3-controls.ts +2 -2
- package/client/modeller/figure-v3-first-save.ts +1 -1
- package/client/modeller/figure-v3-links.ts +6 -6
- package/client/modeller/figure-v3-material.ts +11 -11
- package/client/modeller/figure-v3-parts.ts +7 -10
- package/client/modeller/figure-v3-preview.ts +3 -3
- package/client/modeller/figure-v3-properties.ts +5 -6
- package/client/modeller/figure-v3-verdict.ts +9 -10
- package/client/modeller/figure-view.ts +2 -3
- package/client/modeller/modeller-route.ts +2 -3
- package/client/modeller/scene-budget.ts +0 -14
- package/client/modeller/side-panel-styles.ts +5 -5
- package/client/modeller/v3-align.ts +16 -6
- package/client/modeller/v3-canvas.ts +12 -12
- package/client/modeller/v3-clips.ts +1 -1
- package/client/modeller/v3-controls.ts +1 -1
- package/client/modeller/v3-editor.ts +21 -21
- package/client/modeller/v3-fields.ts +1 -1
- package/client/modeller/v3-figure-facts.ts +1 -1
- package/client/modeller/v3-first-save.ts +3 -4
- package/client/modeller/v3-gizmo.ts +5 -5
- package/client/modeller/v3-help.ts +1 -1
- package/client/modeller/v3-joint-fan.ts +4 -5
- package/client/modeller/v3-keys.ts +1 -1
- package/client/modeller/v3-links.ts +9 -12
- package/client/modeller/v3-maturity.ts +22 -26
- package/client/modeller/v3-refusals.ts +17 -0
- package/client/modeller/v3-renderer.ts +1 -1
- package/client/modeller/v3-stage-model.ts +2 -2
- package/client/modeller/v3-thumbnail.ts +1 -3
- package/client/pages/figure-v3-modeller-page.ts +8 -11
- package/client/types.ts +2 -31
- package/dist-client/graphql/index.js +0 -45
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +4 -37
- package/dist-client/modeller/figure-history-panel.js +8 -154
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
- package/dist-client/modeller/figure-v3-capability.js +1 -1
- package/dist-client/modeller/figure-v3-capability.js.map +1 -1
- package/dist-client/modeller/figure-v3-clips.d.ts +2 -2
- package/dist-client/modeller/figure-v3-clips.js +2 -2
- package/dist-client/modeller/figure-v3-clips.js.map +1 -1
- package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
- package/dist-client/modeller/figure-v3-controls.js +2 -2
- package/dist-client/modeller/figure-v3-controls.js.map +1 -1
- package/dist-client/modeller/figure-v3-first-save.d.ts +1 -1
- package/dist-client/modeller/figure-v3-first-save.js +1 -1
- package/dist-client/modeller/figure-v3-first-save.js.map +1 -1
- package/dist-client/modeller/figure-v3-links.d.ts +5 -5
- package/dist-client/modeller/figure-v3-links.js +6 -6
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +6 -6
- package/dist-client/modeller/figure-v3-material.js +11 -11
- package/dist-client/modeller/figure-v3-material.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +6 -8
- package/dist-client/modeller/figure-v3-parts.js +1 -1
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-preview.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +4 -5
- package/dist-client/modeller/figure-v3-properties.js +5 -6
- package/dist-client/modeller/figure-v3-properties.js.map +1 -1
- package/dist-client/modeller/figure-v3-verdict.d.ts +2 -2
- package/dist-client/modeller/figure-v3-verdict.js +9 -10
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +2 -3
- package/dist-client/modeller/figure-view.js.map +1 -1
- package/dist-client/modeller/modeller-route.d.ts +2 -3
- package/dist-client/modeller/modeller-route.js +2 -3
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/scene-budget.d.ts +0 -11
- package/dist-client/modeller/scene-budget.js +0 -12
- package/dist-client/modeller/scene-budget.js.map +1 -1
- package/dist-client/modeller/side-panel-styles.d.ts +2 -2
- package/dist-client/modeller/side-panel-styles.js +5 -5
- package/dist-client/modeller/side-panel-styles.js.map +1 -1
- package/dist-client/modeller/v3-align.d.ts +9 -5
- package/dist-client/modeller/v3-align.js +13 -3
- package/dist-client/modeller/v3-align.js.map +1 -1
- package/dist-client/modeller/v3-canvas.d.ts +4 -4
- package/dist-client/modeller/v3-canvas.js +9 -9
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.js.map +1 -1
- package/dist-client/modeller/v3-controls.js.map +1 -1
- package/dist-client/modeller/v3-editor.d.ts +13 -13
- package/dist-client/modeller/v3-editor.js +19 -18
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.js.map +1 -1
- package/dist-client/modeller/v3-figure-facts.d.ts +1 -1
- package/dist-client/modeller/v3-figure-facts.js +1 -1
- package/dist-client/modeller/v3-figure-facts.js.map +1 -1
- package/dist-client/modeller/v3-first-save.js +3 -4
- package/dist-client/modeller/v3-first-save.js.map +1 -1
- package/dist-client/modeller/v3-gizmo.d.ts +5 -5
- package/dist-client/modeller/v3-gizmo.js +1 -1
- package/dist-client/modeller/v3-gizmo.js.map +1 -1
- package/dist-client/modeller/v3-help.d.ts +1 -1
- package/dist-client/modeller/v3-help.js +1 -1
- package/dist-client/modeller/v3-help.js.map +1 -1
- package/dist-client/modeller/v3-joint-fan.js +4 -5
- package/dist-client/modeller/v3-joint-fan.js.map +1 -1
- package/dist-client/modeller/v3-keys.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +5 -7
- package/dist-client/modeller/v3-links.js +6 -8
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-maturity.d.ts +7 -7
- package/dist-client/modeller/v3-maturity.js +20 -24
- package/dist-client/modeller/v3-maturity.js.map +1 -1
- package/dist-client/modeller/v3-refusals.d.ts +6 -0
- package/dist-client/modeller/v3-refusals.js +18 -0
- package/dist-client/modeller/v3-refusals.js.map +1 -1
- package/dist-client/modeller/v3-renderer.js +1 -1
- package/dist-client/modeller/v3-renderer.js.map +1 -1
- package/dist-client/modeller/v3-stage-model.js.map +1 -1
- package/dist-client/modeller/v3-thumbnail.js +1 -3
- package/dist-client/modeller/v3-thumbnail.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +4 -5
- package/dist-client/pages/figure-v3-modeller-page.js +8 -11
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -29
- package/dist-client/types.js +3 -0
- package/dist-client/types.js.map +1 -1
- package/docs/v3-product-cutover.md +14 -24
- package/package.json +5 -5
- package/test/browser/modeller-page.js +6 -7
- package/test/browser/modeller.check.mjs +9 -16
- package/test/fixtures/joint-column.json +1138 -0
- package/test/fixtures/joint-roller.json +872 -0
- package/test/fixtures/robot-arm-6dof.json +2776 -0
- package/test/i18n-prefix-guard.test.ts +0 -31
- package/test/modeller-route.test.ts +2 -3
- package/test/v3-align.test.ts +9 -4
- package/test/v3-clips.test.ts +1 -1
- package/test/v3-controls.test.ts +1 -1
- package/test/v3-fields.test.ts +4 -4
- package/test/v3-first-save.test.ts +1 -1
- package/test/v3-gizmo.test.ts +5 -5
- package/test/v3-joint-fan.test.ts +12 -42
- package/test/v3-keys.test.ts +1 -1
- package/test/v3-layout.test.ts +4 -5
- package/test/v3-links.test.ts +3 -3
- package/test/v3-material.test.ts +3 -3
- package/test/v3-maturity.test.ts +8 -9
- package/test/v3-parts-wiring.test.ts +3 -3
- package/test/v3-preview.test.ts +1 -1
- package/test/v3-product-write.test.ts +1 -1
- package/test/v3-properties.test.ts +3 -3
- package/test/v3-refusals.test.ts +12 -0
- package/test/v3-release-panel.test.ts +5 -5
- package/test/v3-scene-canvas.test.ts +2 -2
- package/test/v3-theme.test.ts +2 -2
- package/test/v3-verdict.test.ts +2 -2
- package/test/v3-view.test.ts +2 -2
- package/translations/en.json +2 -143
- package/translations/ja.json +2 -143
- package/translations/ko.json +2 -143
- package/translations/ms.json +2 -143
- package/translations/zh.json +2 -143
- package/client/modeller/figure-source.ts +0 -437
- package/client/modeller/part-edits.ts +0 -312
- package/dist-client/modeller/figure-source.d.ts +0 -238
- package/dist-client/modeller/figure-source.js +0 -193
- package/dist-client/modeller/figure-source.js.map +0 -1
- package/dist-client/modeller/part-edits.d.ts +0 -89
- package/dist-client/modeller/part-edits.js +0 -270
- package/dist-client/modeller/part-edits.js.map +0 -1
- package/test/expect.ts +0 -57
- package/test/figure-source.test.ts +0 -511
- package/test/part-edits.test.ts +0 -350
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { FIGURE_SOURCE_VERSION } from '@hatiolab/figure-model';
|
|
2
|
-
const RAD_TO_DEG = 180 / Math.PI;
|
|
3
|
-
const DEG_TO_RAD = Math.PI / 180;
|
|
4
|
-
/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */
|
|
5
|
-
function withoutEmpty(value) {
|
|
6
|
-
const out = {};
|
|
7
|
-
for (const [key, item] of Object.entries(value)) {
|
|
8
|
-
if (item !== undefined)
|
|
9
|
-
out[key] = item;
|
|
10
|
-
}
|
|
11
|
-
return out;
|
|
12
|
-
}
|
|
13
|
-
/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */
|
|
14
|
-
export function round4(value) {
|
|
15
|
-
return Math.round(value * 1e4) / 1e4;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.
|
|
19
|
-
*
|
|
20
|
-
* 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.
|
|
21
|
-
*/
|
|
22
|
-
export function halfOf(draft) {
|
|
23
|
-
return { x: draft.width / 2, y: draft.depth / 2, z: draft.height / 2 };
|
|
24
|
-
}
|
|
25
|
-
/*
|
|
26
|
-
* ── The placement is not in these numbers ────────────────────────────────────
|
|
27
|
-
*
|
|
28
|
-
* `placement` says which reference surface a figure of this kind attaches to — a conveyor stands
|
|
29
|
-
* on the floor, an OHT hangs from the ceiling. That is a fact about the figure *type*, and the
|
|
30
|
-
* scene acts on it when an instance is placed: `floor` puts the box on the ground, `ceiling` at
|
|
31
|
-
* the ceiling height less the figure's own height. Nothing about the authored shape changes.
|
|
32
|
-
*
|
|
33
|
-
* So the conversion below does not read it, and the editing canvas does not either. The box is
|
|
34
|
-
* always `0 … base.y` there and `zPos` is always the part's underside. Two figures with identical
|
|
35
|
-
* parts and different placements have identical `parts` arrays, which is what the format says.
|
|
36
|
-
*
|
|
37
|
-
* This used to be wired the other way: the canvas fed the figure's placement to the model layer's
|
|
38
|
-
* `state.placement`, which is a scene-wide coordinate mode ('floor' | 'space' | 'inverted') that
|
|
39
|
-
* flips what `zPos` names for everything in that layer. It stood in for the archetype because the
|
|
40
|
-
* modeller holds exactly one figure — and it cost a whole day of defects, because every number the
|
|
41
|
-
* author typed then meant a different face depending on a dropdown. That coordinate mode is now
|
|
42
|
-
* where it belongs: a workbench setting in `figure-view.ts`, applied to the preview alone.
|
|
43
|
-
*/
|
|
44
|
-
/**
|
|
45
|
-
* 저작자가 보는 축이 씬의 어느 용어인가.
|
|
46
|
-
*
|
|
47
|
-
* 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —
|
|
48
|
-
* 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.
|
|
49
|
-
*/
|
|
50
|
-
export const AXIS_KEYS = {
|
|
51
|
-
x: { at: 'left', size: 'width' },
|
|
52
|
-
y: { at: 'zPos', size: 'depth' },
|
|
53
|
-
z: { at: 'top', size: 'height' }
|
|
54
|
-
};
|
|
55
|
-
/** 판이 그 축으로 얼마나 넓은가. */
|
|
56
|
-
export function draftExtent(draft, axis) {
|
|
57
|
-
return axis === 'x' ? draft.width : axis === 'y' ? draft.depth : draft.height;
|
|
58
|
-
}
|
|
59
|
-
/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */
|
|
60
|
-
export function partToFigure(part, draft) {
|
|
61
|
-
return partTo(part, halfOf(draft));
|
|
62
|
-
}
|
|
63
|
-
/** 부품 하나를 씬 용어로 되돌린다. */
|
|
64
|
-
export function partFromFigure(part, draft) {
|
|
65
|
-
return partFrom(part, halfOf(draft));
|
|
66
|
-
}
|
|
67
|
-
/** 씬 모델을 저장 형식으로 변환한다. */
|
|
68
|
-
export function toFigureSource(draft, parts) {
|
|
69
|
-
const half = halfOf(draft);
|
|
70
|
-
return withoutEmpty({
|
|
71
|
-
version: draft.version ?? FIGURE_SOURCE_VERSION,
|
|
72
|
-
type: draft.figureType ?? '',
|
|
73
|
-
base: { x: draft.width, y: draft.depth, z: draft.height },
|
|
74
|
-
placement: draft.placement,
|
|
75
|
-
detailLevel: draft.detailLevel,
|
|
76
|
-
styleKit: draft.styleKit,
|
|
77
|
-
parts: parts.map(part => partTo(part, half)),
|
|
78
|
-
animations: draft.animations,
|
|
79
|
-
parameters: draft.parameters,
|
|
80
|
-
capabilities: draft.capabilities,
|
|
81
|
-
joints: draft.joints?.length ? draft.joints : undefined
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
function partTo(part, half) {
|
|
85
|
-
const rotation = withoutEmpty({
|
|
86
|
-
x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,
|
|
87
|
-
y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,
|
|
88
|
-
z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined
|
|
89
|
-
});
|
|
90
|
-
const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow });
|
|
91
|
-
/*
|
|
92
|
-
재질이 하나도 없으면 **칸 자체를 안 적는다.**
|
|
93
|
-
|
|
94
|
-
빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어
|
|
95
|
-
`material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이
|
|
96
|
-
정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.
|
|
97
|
-
*/
|
|
98
|
-
const material = withoutEmpty({
|
|
99
|
-
token: part.token,
|
|
100
|
-
preset: part.preset,
|
|
101
|
-
flatShading: part.flatShading,
|
|
102
|
-
transparent: part.transparent,
|
|
103
|
-
emissive: part.emissive,
|
|
104
|
-
surface: part.surface
|
|
105
|
-
});
|
|
106
|
-
return withoutEmpty({
|
|
107
|
-
name: part.name,
|
|
108
|
-
primitive: part.primitive,
|
|
109
|
-
transform: withoutEmpty({
|
|
110
|
-
position: {
|
|
111
|
-
x: round4(part.left + part.width / 2 - half.x),
|
|
112
|
-
y: round4(part.zPos + part.depth / 2),
|
|
113
|
-
z: round4(part.top + part.height / 2 - half.z)
|
|
114
|
-
},
|
|
115
|
-
size: { x: part.width, y: part.depth, z: part.height },
|
|
116
|
-
rotation: Object.keys(rotation).length > 0 ? rotation : undefined
|
|
117
|
-
}),
|
|
118
|
-
segments: part.segments,
|
|
119
|
-
shape: Object.keys(shape).length > 0 ? shape : undefined,
|
|
120
|
-
material: Object.keys(material).length > 0 ? material : undefined,
|
|
121
|
-
materialSlot: part.materialSlot,
|
|
122
|
-
keepRound: part.keepRound,
|
|
123
|
-
sizing: part.sizing,
|
|
124
|
-
anchor: part.anchor,
|
|
125
|
-
repeat: part.repeat,
|
|
126
|
-
label: part.label,
|
|
127
|
-
capability: part.capability,
|
|
128
|
-
parent: part.parent
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
/** 형식을 씬 모델로 편다. */
|
|
132
|
-
export function fromFigureSource(source) {
|
|
133
|
-
const half = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 };
|
|
134
|
-
const draft = withoutEmpty({
|
|
135
|
-
version: source.version,
|
|
136
|
-
width: source.base.x,
|
|
137
|
-
height: source.base.z,
|
|
138
|
-
depth: source.base.y,
|
|
139
|
-
figureType: source.type,
|
|
140
|
-
placement: source.placement,
|
|
141
|
-
detailLevel: source.detailLevel,
|
|
142
|
-
styleKit: source.styleKit,
|
|
143
|
-
/* Opened as stored. A legacy `drive: 'hold'` clip is refused by the format now; stored ones were migrated. */
|
|
144
|
-
animations: source.animations?.length ? source.animations : undefined,
|
|
145
|
-
parameters: source.parameters?.length ? source.parameters : undefined,
|
|
146
|
-
capabilities: source.capabilities,
|
|
147
|
-
joints: source.joints?.length ? source.joints : undefined
|
|
148
|
-
});
|
|
149
|
-
return { draft, parts: source.parts.map(part => partFrom(part, half)) };
|
|
150
|
-
}
|
|
151
|
-
function partFrom(part, half) {
|
|
152
|
-
const { position, size, rotation } = part.transform;
|
|
153
|
-
const material = part.material ?? {};
|
|
154
|
-
return withoutEmpty({
|
|
155
|
-
type: 'figure-part',
|
|
156
|
-
name: part.name,
|
|
157
|
-
primitive: part.primitive,
|
|
158
|
-
left: round4(half.x + position.x - size.x / 2),
|
|
159
|
-
top: round4(half.z + position.z - size.z / 2),
|
|
160
|
-
width: size.x,
|
|
161
|
-
height: size.z,
|
|
162
|
-
zPos: round4(position.y - size.y / 2),
|
|
163
|
-
depth: size.y,
|
|
164
|
-
/*
|
|
165
|
-
라디안은 자르지 않는다.
|
|
166
|
-
|
|
167
|
-
소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.
|
|
168
|
-
자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.
|
|
169
|
-
*/
|
|
170
|
-
rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,
|
|
171
|
-
rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,
|
|
172
|
-
rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,
|
|
173
|
-
segments: part.segments,
|
|
174
|
-
round: part.shape?.round,
|
|
175
|
-
path: part.shape?.path,
|
|
176
|
-
hollow: part.shape?.hollow,
|
|
177
|
-
token: material.token,
|
|
178
|
-
preset: material.preset,
|
|
179
|
-
flatShading: material.flatShading,
|
|
180
|
-
transparent: material.transparent,
|
|
181
|
-
emissive: material.emissive,
|
|
182
|
-
surface: material.surface,
|
|
183
|
-
materialSlot: part.materialSlot,
|
|
184
|
-
keepRound: part.keepRound,
|
|
185
|
-
sizing: part.sizing,
|
|
186
|
-
anchor: part.anchor,
|
|
187
|
-
repeat: part.repeat,
|
|
188
|
-
label: part.label,
|
|
189
|
-
capability: part.capability,
|
|
190
|
-
parent: part.parent
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
//# sourceMappingURL=figure-source.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n FIGURE_SOURCE_VERSION,\n type DetailLevel,\n type FigurePart,\n type FigurePlacement,\n type FigureSource,\n type Vec3\n} from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a draft. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The draft is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x draft width\n * base.y draft depth <- height\n * base.z draft height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - draft width/2\n * position.y zPos + depth/2 <- both measured from the box bottom\n * position.z top + height/2 - draft height/2\n *\n * (Source edition 2 measures y from the bottom face of the base box, as the scene's zPos does, so\n * that row needs no half-box shift. x and z are still measured from the box centre. ADR-0065.)\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n *\n * All four passed the round-trip test, because the fixture it uses did not\n * carry the field. So the test now reads the **field list the format declares**\n * rather than trusting the fixture.\n *\n * `shape` is unpacked into `round`, `path` and `hollow` rather than carried\n * whole, because the editor holds those as separate state. Every field added\n * under `shape` costs a line here, and forgetting that line was the fourth\n * of the four.\n */\n/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface FigureDraft {\n /** 형식 버전. */\n version?: number\n /*\n The three sizes keep the scene component's names, and the scene assigns its axes differently\n from the format: here `depth` is the vertical one. Naming the axis alone (\"the y axis\") does\n not say which of the two conventions is meant, and this interface sits on the boundary between\n them — so each field names the `FigureSource` field it becomes. `toFigureSource` and\n `fromFigureSource` below are the only places that may disagree with these three lines.\n */\n /** 가로 너비 (mm) → `base.x`. */\n width: number\n /** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */\n height: number\n /** 높이 (mm) → `base.y`. 서 있는 방향이다. */\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */\n placement?: FigurePlacement\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n /**\n * 값이 만드는 자세 — **애니메이션과 다른 칸이다.**\n *\n * 저것은 시각이 흘러 움직이는 것(도는 롤러)이고 이것은 인스턴스가 준 값이 그대로 자세가\n * 되는 것(1200mm 내려온 호이스트)이다. 한동안 애니메이션 안에 `drive: 'hold'` 로 숨어\n * 있었고, 그래서 「길이값을 변수로 노출하라」는 요청에 아무도 이것을 못 찾았다(ADR-0051).\n */\n parameters?: FigureSource['parameters']\n /**\n * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.\n *\n * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가\n * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐\n * 주나**를 말하는 다른 칸이다.\n */\n capabilities?: FigureSource['capabilities']\n /**\n * How parts move relative to their parents (ADR-0066). Kept in figure coordinates as stored, the same\n * terms the inspector shows. Carried even when nothing edits them, so opening and saving never drops\n * them.\n */\n joints?: FigureSource['joints']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n /**\n * 씬이 이것을 **집지 않는다** — 클릭·마퀴·기즈모 어디에도 안 걸린다.\n *\n * 저작면에만 서는 것(관절 부채꼴)이 쓴다. 저장 형식에는 없는 칸이라 `toFigureSource` 가\n * 싣지 않는다.\n */\n locked?: boolean\n\n /** 색 토큰 이름 (`palette.primary` 등). */\n token?: string\n /*\n `FigurePart['material']` is required in the format, so these index into it directly. The\n `NonNullable` on `shape` above is not the same case — `shape` is optional there.\n */\n preset?: FigurePart['material']['preset']\n flatShading?: boolean\n transparent?: boolean\n /**\n * 자체발광. 없으면 안 빛난다.\n *\n * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진\n * 전구는 빨갛지 않고 어두운 플라스틱이다.\n */\n emissive?: FigurePart['material']['emissive']\n surface?: FigurePart['material']['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n /**\n * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.\n *\n * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,\n * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.\n */\n capability?: FigurePart['capability']\n /** The part this one is attached to (ADR-0066). The one place a parent is written. */\n parent?: FigurePart['parent']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nexport function round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(draft: FigureDraft): Vec3 {\n return { x: draft.width / 2, y: draft.depth / 2, z: draft.height / 2 }\n}\n\n/*\n * ── The placement is not in these numbers ────────────────────────────────────\n *\n * `placement` says which reference surface a figure of this kind attaches to — a conveyor stands\n * on the floor, an OHT hangs from the ceiling. That is a fact about the figure *type*, and the\n * scene acts on it when an instance is placed: `floor` puts the box on the ground, `ceiling` at\n * the ceiling height less the figure's own height. Nothing about the authored shape changes.\n *\n * So the conversion below does not read it, and the editing canvas does not either. The box is\n * always `0 … base.y` there and `zPos` is always the part's underside. Two figures with identical\n * parts and different placements have identical `parts` arrays, which is what the format says.\n *\n * This used to be wired the other way: the canvas fed the figure's placement to the model layer's\n * `state.placement`, which is a scene-wide coordinate mode ('floor' | 'space' | 'inverted') that\n * flips what `zPos` names for everything in that layer. It stood in for the archetype because the\n * modeller holds exactly one figure — and it cost a whole day of defects, because every number the\n * author typed then meant a different face depending on a dropdown. That coordinate mode is now\n * where it belongs: a workbench setting in `figure-view.ts`, applied to the preview alone.\n */\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function draftExtent(draft: FigureDraft, axis: PlaneAxis): number {\n return axis === 'x' ? draft.width : axis === 'y' ? draft.depth : draft.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, draft: FigureDraft): FigurePart {\n return partTo(part, halfOf(draft))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, draft: FigureDraft): PartModel {\n return partFrom(part, halfOf(draft))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(draft: FigureDraft, parts: PartModel[]): FigureSource {\n const half = halfOf(draft)\n\n return withoutEmpty({\n version: draft.version ?? FIGURE_SOURCE_VERSION,\n type: draft.figureType ?? '',\n base: { x: draft.width, y: draft.depth, z: draft.height },\n placement: draft.placement,\n detailLevel: draft.detailLevel,\n styleKit: draft.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: draft.animations,\n parameters: draft.parameters,\n capabilities: draft.capabilities,\n joints: draft.joints?.length ? draft.joints : undefined\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n /*\n 재질이 하나도 없으면 **칸 자체를 안 적는다.**\n\n 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어\n `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이\n 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.\n */\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material: Object.keys(material).length > 0 ? material : undefined,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability,\n parent: part.parent\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { draft: FigureDraft; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const draft = withoutEmpty({\n version: source.version,\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n placement: source.placement,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n /* Opened as stored. A legacy `drive: 'hold'` clip is refused by the format now; stored ones were migrated. */\n animations: source.animations?.length ? source.animations : undefined,\n parameters: source.parameters?.length ? source.parameters : undefined,\n capabilities: source.capabilities,\n joints: source.joints?.length ? source.joints : undefined\n }) as FigureDraft\n\n return { draft, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability,\n parent: part.parent\n }) as PartModel\n}\n"]}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { PrimitiveKind } from '@hatiolab/figure-model';
|
|
2
|
-
import type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js';
|
|
3
|
-
/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
|
|
4
|
-
export declare function partLimit(draft: FigureDraft): number;
|
|
5
|
-
/**
|
|
6
|
-
* 새 부품 하나를 만든다.
|
|
7
|
-
*
|
|
8
|
-
* 기준 상자의 3분의 1 크기다. 추가하자마자 화면에 보여야 한다 — 크기 0 으로 놓으면
|
|
9
|
-
* 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
|
|
10
|
-
*
|
|
11
|
-
* ## 어디에 놓나 — 고른 부품 **위**다
|
|
12
|
-
*
|
|
13
|
-
* 전에는 언제나 바닥 한가운데였다. 첫 부품은 그 자리가 맞지만 둘째부터는 **첫 부품 속에**
|
|
14
|
-
* 들어간다 — 같은 자리에 같은 크기로 놓이니 화면에는 덩어리 하나뿐이고, 저작자는 무엇이
|
|
15
|
-
* 늘었는지 못 본다. 「추가하자마자 보여야 한다」는 그때 이미 깨져 있었다.
|
|
16
|
-
*
|
|
17
|
-
* 그래서 고른 부품이 있으면 그 부품의 **윗면**에 얹고 발자국을 가운데로 맞춘다. 기계는
|
|
18
|
-
* 대개 그렇게 쌓인다(받침 → 어깨 → 팔). 관절을 걸 때도 이 자리가 필요하다 — 맞닿는 면이
|
|
19
|
-
* 실제로 있어야 `seamOrigin` 이 회전 중심을 그 면에서 잡는다.
|
|
20
|
-
*
|
|
21
|
-
* 위에 얹으면 기준 상자를 넘는 경우에는 바닥 한가운데로 간다. 넘은 채로 놓으면 발행이
|
|
22
|
-
* 막히고(`part-outside-base`), 그것을 고치는 일이 「부품을 추가했다」의 대가가 된다.
|
|
23
|
-
* 가로·세로는 상자 안으로 당긴다 — 고른 부품이 가장자리에 있으면 그 위가 상자 밖이다.
|
|
24
|
-
*/
|
|
25
|
-
export declare function newPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind, onTopOf?: PartModel): PartModel;
|
|
26
|
-
/**
|
|
27
|
-
* 씬에서 온 부품을 밀리미터로 맞춘다.
|
|
28
|
-
*
|
|
29
|
-
* 기즈모는 비율을 곱해서 크기를 낸다 — 420 에 1.72 를 곱하면 722.4180046035418 이고, 손대지
|
|
30
|
-
* 않은 축도 384.99999999999994 처럼 흔들린다. 저작자가 끈 결과가 소수 넷째 자리까지 남을
|
|
31
|
-
* 까닭이 없고, 칸에는 「722.4…」로 잘려 보인다. 이 편집기의 다른 길은 전부 정수 밀리미터로
|
|
32
|
-
* 맞춘다(부품을 새로 놓을 때 · 가운데 맞추기 · 격자 맞추기).
|
|
33
|
-
*
|
|
34
|
-
* 크기는 1 밑으로 내려가지 않는다 — 0 이면 부피가 사라지고 배율이 0 으로 나뉜다.
|
|
35
|
-
*/
|
|
36
|
-
export declare function roundPart(part: PartModel): PartModel;
|
|
37
|
-
export declare function addPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind, onTopOf?: PartModel): PartModel[];
|
|
38
|
-
export declare function removePart(parts: PartModel[], index: number): PartModel[];
|
|
39
|
-
/** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */
|
|
40
|
-
export declare function duplicatePart(parts: PartModel[], index: number): PartModel[];
|
|
41
|
-
/** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */
|
|
42
|
-
export declare function movePart(parts: PartModel[], index: number, to: number): PartModel[];
|
|
43
|
-
export declare function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[];
|
|
44
|
-
/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */
|
|
45
|
-
export declare function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
|
|
46
|
-
/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */
|
|
47
|
-
export declare function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
|
|
48
|
-
/** 부품을 상자의 끝에 붙인다. */
|
|
49
|
-
export declare function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
|
|
50
|
-
/**
|
|
51
|
-
* 부품을 축 기준으로 대칭 복제한다.
|
|
52
|
-
*
|
|
53
|
-
* 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수
|
|
54
|
-
* 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.
|
|
55
|
-
*/
|
|
56
|
-
export declare function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[];
|
|
57
|
-
/**
|
|
58
|
-
* 대칭이 의미가 있는지.
|
|
59
|
-
*
|
|
60
|
-
* 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는
|
|
61
|
-
* 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.
|
|
62
|
-
* 만들어 놓고 알리는 대신 미리 막는다.
|
|
63
|
-
*/
|
|
64
|
-
export declare function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean;
|
|
65
|
-
/**
|
|
66
|
-
* 부품의 위치와 크기를 격자에 맞춘다.
|
|
67
|
-
*
|
|
68
|
-
* 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰
|
|
69
|
-
* 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.
|
|
70
|
-
*/
|
|
71
|
-
export declare function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[];
|
|
72
|
-
/**
|
|
73
|
-
* 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.
|
|
74
|
-
*
|
|
75
|
-
* **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워
|
|
76
|
-
* (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가
|
|
77
|
-
* 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.
|
|
78
|
-
*/
|
|
79
|
-
export declare function gridStep(draft: FigureDraft): number;
|
|
80
|
-
/**
|
|
81
|
-
* 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.
|
|
82
|
-
*
|
|
83
|
-
* 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기
|
|
84
|
-
* 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.
|
|
85
|
-
*
|
|
86
|
-
* 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이
|
|
87
|
-
* 먼저 보였다.
|
|
88
|
-
*/
|
|
89
|
-
export declare function niceStep(span: number): number;
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model';
|
|
2
|
-
import { AXIS_KEYS, draftExtent } from './figure-source.js';
|
|
3
|
-
/**
|
|
4
|
-
* 부품 편집 규칙. **씬 모델의 용어로** 다룬다.
|
|
5
|
-
*
|
|
6
|
-
* 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.
|
|
7
|
-
* 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.
|
|
8
|
-
* FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.
|
|
9
|
-
*
|
|
10
|
-
* 사용자가 보는 축(X·Y·Z)이 씬의 어느 속성인지도 `figure-source.ts` 의 `AXIS_KEYS` 가 기준이다.
|
|
11
|
-
* 여기서 다시 정의하지 않는다.
|
|
12
|
-
*
|
|
13
|
-
* 모든 함수가 **새 배열·새 객체를 반환한다** — 원본을 수정하지 않는다. Lit 이 변경을
|
|
14
|
-
* 감지하려면 참조가 달라져야 하고, 실행 취소를 붙일 때도 이전 상태가 남아 있어야 한다.
|
|
15
|
-
*/
|
|
16
|
-
/** 중복되지 않는 이름을 만든다. 부품 이름은 저장되는 식별자라 중복되면 검증에서 걸린다. */
|
|
17
|
-
function freeName(taken, base) {
|
|
18
|
-
if (!taken.has(base))
|
|
19
|
-
return base;
|
|
20
|
-
for (let n = 2;; n++) {
|
|
21
|
-
const candidate = `${base}-${n}`;
|
|
22
|
-
if (!taken.has(candidate))
|
|
23
|
-
return candidate;
|
|
24
|
-
}
|
|
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
|
-
}
|
|
31
|
-
/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */
|
|
32
|
-
export function partLimit(draft) {
|
|
33
|
-
return PART_LIMIT[(draft.detailLevel ?? 'M')];
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* 새 부품 하나를 만든다.
|
|
37
|
-
*
|
|
38
|
-
* 기준 상자의 3분의 1 크기다. 추가하자마자 화면에 보여야 한다 — 크기 0 으로 놓으면
|
|
39
|
-
* 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.
|
|
40
|
-
*
|
|
41
|
-
* ## 어디에 놓나 — 고른 부품 **위**다
|
|
42
|
-
*
|
|
43
|
-
* 전에는 언제나 바닥 한가운데였다. 첫 부품은 그 자리가 맞지만 둘째부터는 **첫 부품 속에**
|
|
44
|
-
* 들어간다 — 같은 자리에 같은 크기로 놓이니 화면에는 덩어리 하나뿐이고, 저작자는 무엇이
|
|
45
|
-
* 늘었는지 못 본다. 「추가하자마자 보여야 한다」는 그때 이미 깨져 있었다.
|
|
46
|
-
*
|
|
47
|
-
* 그래서 고른 부품이 있으면 그 부품의 **윗면**에 얹고 발자국을 가운데로 맞춘다. 기계는
|
|
48
|
-
* 대개 그렇게 쌓인다(받침 → 어깨 → 팔). 관절을 걸 때도 이 자리가 필요하다 — 맞닿는 면이
|
|
49
|
-
* 실제로 있어야 `seamOrigin` 이 회전 중심을 그 면에서 잡는다.
|
|
50
|
-
*
|
|
51
|
-
* 위에 얹으면 기준 상자를 넘는 경우에는 바닥 한가운데로 간다. 넘은 채로 놓으면 발행이
|
|
52
|
-
* 막히고(`part-outside-base`), 그것을 고치는 일이 「부품을 추가했다」의 대가가 된다.
|
|
53
|
-
* 가로·세로는 상자 안으로 당긴다 — 고른 부품이 가장자리에 있으면 그 위가 상자 밖이다.
|
|
54
|
-
*/
|
|
55
|
-
export function newPart(draft, parts, primitive, onTopOf) {
|
|
56
|
-
const taken = new Set(parts.map(part => part.name));
|
|
57
|
-
const width = Math.max(1, Math.round(draft.width / 3));
|
|
58
|
-
const height = Math.max(1, Math.round(draft.height / 3));
|
|
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;
|
|
73
|
-
const part = {
|
|
74
|
-
type: 'figure-part',
|
|
75
|
-
name: freeName(taken, primitive),
|
|
76
|
-
primitive,
|
|
77
|
-
left: at.left,
|
|
78
|
-
top: at.top,
|
|
79
|
-
width,
|
|
80
|
-
height,
|
|
81
|
-
zPos: at.zPos,
|
|
82
|
-
depth,
|
|
83
|
-
// 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.
|
|
84
|
-
token: parts[0]?.token ?? 'palette.primary'
|
|
85
|
-
};
|
|
86
|
-
if (SEGMENTED_PRIMITIVES.includes(primitive)) {
|
|
87
|
-
part.segments = SEGMENT_PRESETS[1];
|
|
88
|
-
}
|
|
89
|
-
if (EXTRUDE_PRIMITIVES.includes(primitive)) {
|
|
90
|
-
part.round = 0;
|
|
91
|
-
}
|
|
92
|
-
if (primitive === 'polygon') {
|
|
93
|
-
// polygon 은 점이 3개 이상이어야 한다. 없으면 검증에서 막히므로 삼각형으로 시작한다.
|
|
94
|
-
// 단면의 점은 부품 중심을 기준으로 적는다.
|
|
95
|
-
part.path = [
|
|
96
|
-
{ x: -Math.round(width / 2), y: -Math.round(height / 2) },
|
|
97
|
-
{ x: Math.round(width / 2), y: -Math.round(height / 2) },
|
|
98
|
-
{ x: 0, y: Math.round(height / 2) }
|
|
99
|
-
];
|
|
100
|
-
}
|
|
101
|
-
return part;
|
|
102
|
-
}
|
|
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)];
|
|
126
|
-
}
|
|
127
|
-
export function removePart(parts, index) {
|
|
128
|
-
return parts.filter((_, at) => at !== index);
|
|
129
|
-
}
|
|
130
|
-
/** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */
|
|
131
|
-
export function duplicatePart(parts, index) {
|
|
132
|
-
const origin = parts[index];
|
|
133
|
-
if (!origin)
|
|
134
|
-
return parts;
|
|
135
|
-
const copy = JSON.parse(JSON.stringify(origin));
|
|
136
|
-
copy.name = freeName(new Set(parts.map(part => part.name)), `${origin.name}-copy`);
|
|
137
|
-
const next = [...parts];
|
|
138
|
-
next.splice(index + 1, 0, copy);
|
|
139
|
-
return next;
|
|
140
|
-
}
|
|
141
|
-
/** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */
|
|
142
|
-
export function movePart(parts, index, to) {
|
|
143
|
-
if (to < 0 || to >= parts.length || index === to)
|
|
144
|
-
return parts;
|
|
145
|
-
const next = [...parts];
|
|
146
|
-
const [moved] = next.splice(index, 1);
|
|
147
|
-
next.splice(to, 0, moved);
|
|
148
|
-
return next;
|
|
149
|
-
}
|
|
150
|
-
export function updatePart(parts, index, part) {
|
|
151
|
-
return parts.map((old, at) => (at === index ? part : old));
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.
|
|
155
|
-
*
|
|
156
|
-
* 도면 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을
|
|
157
|
-
* **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로
|
|
158
|
-
* 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.
|
|
159
|
-
*/
|
|
160
|
-
function put(parts, index, fields) {
|
|
161
|
-
const part = parts[index];
|
|
162
|
-
if (!part)
|
|
163
|
-
return parts;
|
|
164
|
-
return updatePart(parts, index, { ...part, ...fields });
|
|
165
|
-
}
|
|
166
|
-
/** 위치의 한 축만 바꾼다. 어느 속성인지는 `AXIS_KEYS` 가 결정한다. */
|
|
167
|
-
function putAt(parts, index, axis, value) {
|
|
168
|
-
return put(parts, index, { [AXIS_KEYS[axis].at]: value });
|
|
169
|
-
}
|
|
170
|
-
/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */
|
|
171
|
-
export function centerPart(draft, parts, index, axis) {
|
|
172
|
-
const part = parts[index];
|
|
173
|
-
if (!part)
|
|
174
|
-
return parts;
|
|
175
|
-
return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2));
|
|
176
|
-
}
|
|
177
|
-
/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */
|
|
178
|
-
export function alignPartToStart(parts, index, axis) {
|
|
179
|
-
return putAt(parts, index, axis, 0);
|
|
180
|
-
}
|
|
181
|
-
/** 부품을 상자의 끝에 붙인다. */
|
|
182
|
-
export function alignPartToEnd(draft, parts, index, axis) {
|
|
183
|
-
const part = parts[index];
|
|
184
|
-
if (!part)
|
|
185
|
-
return parts;
|
|
186
|
-
return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]));
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* 부품을 축 기준으로 대칭 복제한다.
|
|
190
|
-
*
|
|
191
|
-
* 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수
|
|
192
|
-
* 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.
|
|
193
|
-
*/
|
|
194
|
-
export function mirrorPart(draft, parts, index, axis) {
|
|
195
|
-
const part = parts[index];
|
|
196
|
-
if (!part || !canMirror(draft, parts, index, axis))
|
|
197
|
-
return parts;
|
|
198
|
-
const copy = JSON.parse(JSON.stringify(part));
|
|
199
|
-
copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`);
|
|
200
|
-
const next = [...parts];
|
|
201
|
-
next.splice(index + 1, 0, copy);
|
|
202
|
-
return putAt(next, index + 1, axis, mirroredAt(draft, part, axis));
|
|
203
|
-
}
|
|
204
|
-
/** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */
|
|
205
|
-
function mirroredAt(draft, part, axis) {
|
|
206
|
-
const { at, size } = AXIS_KEYS[axis];
|
|
207
|
-
return Math.round(draftExtent(draft, axis) - part[at] - part[size]);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* 대칭이 의미가 있는지.
|
|
211
|
-
*
|
|
212
|
-
* 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는
|
|
213
|
-
* 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.
|
|
214
|
-
* 만들어 놓고 알리는 대신 미리 막는다.
|
|
215
|
-
*/
|
|
216
|
-
export function canMirror(draft, parts, index, axis) {
|
|
217
|
-
const part = parts[index];
|
|
218
|
-
if (!part)
|
|
219
|
-
return false;
|
|
220
|
-
return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at]);
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* 부품의 위치와 크기를 격자에 맞춘다.
|
|
224
|
-
*
|
|
225
|
-
* 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰
|
|
226
|
-
* 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.
|
|
227
|
-
*/
|
|
228
|
-
export function snapPart(draft, parts, index) {
|
|
229
|
-
const part = parts[index];
|
|
230
|
-
if (!part)
|
|
231
|
-
return parts;
|
|
232
|
-
const step = gridStep(draft);
|
|
233
|
-
const round = (value) => Math.round(value / step) * step;
|
|
234
|
-
return put(parts, index, {
|
|
235
|
-
left: round(part.left),
|
|
236
|
-
top: round(part.top),
|
|
237
|
-
zPos: round(part.zPos),
|
|
238
|
-
// 크기는 0 이 되면 안 된다 — 부품이 사라진 것처럼 보인다
|
|
239
|
-
width: Math.max(step, round(part.width)),
|
|
240
|
-
height: Math.max(step, round(part.height)),
|
|
241
|
-
depth: Math.max(step, round(part.depth))
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.
|
|
246
|
-
*
|
|
247
|
-
* **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워
|
|
248
|
-
* (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가
|
|
249
|
-
* 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.
|
|
250
|
-
*/
|
|
251
|
-
export function gridStep(draft) {
|
|
252
|
-
return niceStep(Math.max(draft.width, draft.height, draft.depth));
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.
|
|
256
|
-
*
|
|
257
|
-
* 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기
|
|
258
|
-
* 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.
|
|
259
|
-
*
|
|
260
|
-
* 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이
|
|
261
|
-
* 먼저 보였다.
|
|
262
|
-
*/
|
|
263
|
-
export function niceStep(span) {
|
|
264
|
-
const raw = Math.max(span, 1) / 5;
|
|
265
|
-
const magnitude = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
266
|
-
const n = raw / magnitude;
|
|
267
|
-
const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10;
|
|
268
|
-
return Math.max(1, multiple * magnitude);
|
|
269
|
-
}
|
|
270
|
-
//# sourceMappingURL=part-edits.js.map
|