@things-factory/figure-ui 10.1.25 → 10.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/client/graphql/index.ts +4 -1
  2. package/client/modeller/figure-animations.ts +229 -29
  3. package/client/modeller/figure-history-panel.ts +2 -2
  4. package/client/modeller/figure-inspector.ts +151 -5
  5. package/client/modeller/figure-preview.ts +5 -5
  6. package/client/modeller/figure-source.ts +33 -45
  7. package/client/modeller/figure-view.ts +20 -0
  8. package/client/modeller/joint-edits.ts +201 -0
  9. package/client/modeller/proposal-session.ts +8 -2
  10. package/client/pages/figure-modeller-page.ts +50 -15
  11. package/dist-client/graphql/index.js +4 -1
  12. package/dist-client/graphql/index.js.map +1 -1
  13. package/dist-client/modeller/figure-animations.d.ts +19 -0
  14. package/dist-client/modeller/figure-animations.js +206 -22
  15. package/dist-client/modeller/figure-animations.js.map +1 -1
  16. package/dist-client/modeller/figure-history-panel.js +2 -2
  17. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  18. package/dist-client/modeller/figure-inspector.d.ts +11 -0
  19. package/dist-client/modeller/figure-inspector.js +124 -4
  20. package/dist-client/modeller/figure-inspector.js.map +1 -1
  21. package/dist-client/modeller/figure-preview.js +5 -5
  22. package/dist-client/modeller/figure-preview.js.map +1 -1
  23. package/dist-client/modeller/figure-source.d.ts +12 -1
  24. package/dist-client/modeller/figure-source.js +13 -40
  25. package/dist-client/modeller/figure-source.js.map +1 -1
  26. package/dist-client/modeller/figure-view.d.ts +16 -0
  27. package/dist-client/modeller/figure-view.js +15 -0
  28. package/dist-client/modeller/figure-view.js.map +1 -1
  29. package/dist-client/modeller/joint-edits.d.ts +29 -0
  30. package/dist-client/modeller/joint-edits.js +175 -0
  31. package/dist-client/modeller/joint-edits.js.map +1 -0
  32. package/dist-client/modeller/proposal-session.d.ts +5 -0
  33. package/dist-client/modeller/proposal-session.js +3 -2
  34. package/dist-client/modeller/proposal-session.js.map +1 -1
  35. package/dist-client/pages/figure-modeller-page.d.ts +2 -22
  36. package/dist-client/pages/figure-modeller-page.js +46 -15
  37. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  38. package/dist-client/tsconfig.tsbuildinfo +1 -0
  39. package/dist-server/tsconfig.tsbuildinfo +1 -0
  40. package/package.json +3 -3
  41. package/test/ai-proposal-contract.test.ts +3 -2
  42. package/test/figure-source.test.ts +43 -57
  43. package/test/i18n-prefix-guard.test.ts +2 -3
  44. package/test/joint-edits.test.ts +130 -0
  45. package/test/preview-board-depth.test.ts +27 -0
  46. package/translations/en.json +119 -80
  47. package/translations/ja.json +208 -58
  48. package/translations/ko.json +139 -100
  49. package/translations/ms.json +206 -56
  50. package/translations/zh.json +207 -57
@@ -0,0 +1,175 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ /** The limits a new joint starts with. Revolute in degrees, prismatic in millimetres. */
5
+ const NEW_LIMITS = {
6
+ revolute: { min: -90, max: 90 },
7
+ prismatic: { min: 0, max: 500 }
8
+ };
9
+ /** The span a parameter driving a continuous joint offers: one turn either way. */
10
+ const CONTINUOUS_SPAN = { min: -360, max: 360 };
11
+ /** The joint whose child is this part, if any. */
12
+ export function jointOf(draft, part) {
13
+ return draft?.joints?.find(joint => joint.child === part);
14
+ }
15
+ /**
16
+ * Parts a part may be attached to: every other part except itself and anything attached below it, since
17
+ * either would close a loop the format refuses (`parent-cycle`).
18
+ */
19
+ export function parentChoices(parts, part) {
20
+ const below = new Set([part]);
21
+ let grew = true;
22
+ while (grew) {
23
+ grew = false;
24
+ for (const one of parts) {
25
+ if (one.parent !== undefined && below.has(one.parent) && !below.has(one.name)) {
26
+ below.add(one.name);
27
+ grew = true;
28
+ }
29
+ }
30
+ }
31
+ return parts.map(one => one.name).filter(name => !!name && !below.has(name));
32
+ }
33
+ function freeName(taken, stem) {
34
+ const used = new Set(taken);
35
+ if (!used.has(stem))
36
+ return stem;
37
+ for (let n = 2;; n++)
38
+ if (!used.has(`${stem}-${n}`))
39
+ return `${stem}-${n}`;
40
+ }
41
+ /** The span the driving parameter offers for a joint: its limits, or one turn either way when it has none. */
42
+ function spanOf(joint) {
43
+ return joint.limits ?? CONTINUOUS_SPAN;
44
+ }
45
+ function drivingParameter(joint) {
46
+ const span = spanOf(joint);
47
+ return {
48
+ name: joint.name,
49
+ range: { unit: joint.type === 'prismatic' ? 'mm' : 'deg', min: span.min, max: span.max },
50
+ default: 0,
51
+ clip: {
52
+ channels: [
53
+ {
54
+ target: joint.name,
55
+ keys: [
56
+ { at: 0, value: span.min },
57
+ { at: 1, value: span.max }
58
+ ]
59
+ }
60
+ ]
61
+ }
62
+ };
63
+ }
64
+ /**
65
+ * Whether a parameter is the plain one `setJointType` made for a joint: one channel on that joint, keys at
66
+ * 0 and 1 spanning the given limits. Only such a parameter follows the joint when its limits or type change;
67
+ * one the author reshaped is left as written.
68
+ */
69
+ function isPlainDriver(parameter, joint, span) {
70
+ const channels = parameter.clip?.channels ?? [];
71
+ if (channels.length !== 1)
72
+ return false;
73
+ const [channel] = channels;
74
+ if (channel.target !== joint.name || channel.path !== undefined)
75
+ return false;
76
+ const keys = channel.keys;
77
+ return (keys.length === 2 &&
78
+ keys[0].at === 0 &&
79
+ keys[1].at === 1 &&
80
+ keys[0].value === span.min &&
81
+ keys[1].value === span.max &&
82
+ parameter.range?.min === span.min &&
83
+ parameter.range?.max === span.max);
84
+ }
85
+ /**
86
+ * Sets how a part moves relative to its parent, or clears the joint with `undefined`.
87
+ *
88
+ * - From none: adds a joint whose origin is the centre of the part's bottom face, with a vertical axis and
89
+ * starting limits, and a parameter that drives it over those limits.
90
+ * - To none: removes the joint and every channel that drove it; a parameter or clip left with no channel
91
+ * is removed too, because the format refuses an empty one.
92
+ * - Between types: keeps the origin and axis, sets limits for the new type (none for continuous), and
93
+ * moves a plain driving parameter along with them.
94
+ */
95
+ export function setJointType(draft, part, type) {
96
+ const joints = draft.joints ?? [];
97
+ const existing = joints.find(joint => joint.child === part.name);
98
+ if (type === undefined) {
99
+ if (!existing)
100
+ return draft;
101
+ return withoutJoint(draft, existing.name);
102
+ }
103
+ if (!existing) {
104
+ const { position, size } = part.transform;
105
+ const joint = {
106
+ name: freeName([...joints.map(one => one.name)], `${part.name}-joint`),
107
+ child: part.name,
108
+ type,
109
+ origin: { x: position.x, y: position.y - size.y / 2, z: position.z },
110
+ axis: { x: 0, y: 1, z: 0 }
111
+ };
112
+ if (type !== 'continuous')
113
+ joint.limits = { ...NEW_LIMITS[type] };
114
+ const parameters = draft.parameters ?? [];
115
+ const taken = [...parameters.map(one => one.name), ...(draft.animations ?? []).map(one => one.name)];
116
+ const driver = drivingParameter(joint);
117
+ driver.name = freeName(taken, joint.name);
118
+ return { ...draft, joints: [...joints, joint], parameters: [...parameters, driver] };
119
+ }
120
+ if (existing.type === type)
121
+ return draft;
122
+ const next = { ...existing, type };
123
+ if (type === 'continuous')
124
+ delete next.limits;
125
+ else if (existing.type === 'continuous' || (existing.type === 'prismatic') !== (type === 'prismatic')) {
126
+ next.limits = { ...NEW_LIMITS[type] };
127
+ }
128
+ return replaceJoint(draft, existing, next);
129
+ }
130
+ /** Changes a joint's origin, axis or limits. A plain driving parameter follows new limits. */
131
+ export function patchJoint(draft, name, patch) {
132
+ const existing = draft.joints?.find(joint => joint.name === name);
133
+ if (!existing)
134
+ return draft;
135
+ const next = { ...existing, ...patch };
136
+ if (existing.type === 'continuous')
137
+ delete next.limits;
138
+ return replaceJoint(draft, existing, next);
139
+ }
140
+ function replaceJoint(draft, before, after) {
141
+ const joints = (draft.joints ?? []).map(joint => (joint === before ? after : joint));
142
+ const was = spanOf(before);
143
+ const unitChanged = (before.type === 'prismatic') !== (after.type === 'prismatic');
144
+ const spanChanged = was.min !== spanOf(after).min || was.max !== spanOf(after).max;
145
+ if (!unitChanged && !spanChanged)
146
+ return { ...draft, joints };
147
+ const parameters = (draft.parameters ?? []).map(parameter => {
148
+ if (!isPlainDriver(parameter, before, was))
149
+ return parameter;
150
+ const driver = drivingParameter(after);
151
+ return { ...parameter, range: driver.range, clip: { ...parameter.clip, channels: driver.clip.channels } };
152
+ });
153
+ return { ...draft, joints, parameters };
154
+ }
155
+ function withoutJoint(draft, name) {
156
+ const keep = (one) => (one.channels ?? []).filter(channel => channel.target !== name);
157
+ const parameters = (draft.parameters ?? [])
158
+ .map(parameter => ({ ...parameter, clip: { ...parameter.clip, channels: keep(parameter.clip) } }))
159
+ .filter(parameter => parameter.clip.channels.length > 0);
160
+ const animations = (draft.animations ?? [])
161
+ .map(clip => ({ ...clip, channels: keep(clip) }))
162
+ .filter(clip => clip.channels.length > 0);
163
+ const joints = (draft.joints ?? []).filter(joint => joint.name !== name);
164
+ const next = { ...draft };
165
+ if (joints.length > 0)
166
+ next.joints = joints;
167
+ else
168
+ delete next.joints;
169
+ if (draft.parameters !== undefined)
170
+ next.parameters = parameters;
171
+ if (draft.animations !== undefined)
172
+ next.animations = animations;
173
+ return next;
174
+ }
175
+ //# sourceMappingURL=joint-edits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joint-edits.js","sourceRoot":"","sources":["../../client/modeller/joint-edits.ts"],"names":[],"mappings":"AAAA;;GAEG;AAiBH,yFAAyF;AACzF,MAAM,UAAU,GAA2E;IACzF,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IAC/B,SAAS,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE;CAChC,CAAA;AAED,mFAAmF;AACnF,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAA;AAE/C,kDAAkD;AAClD,MAAM,UAAU,OAAO,CAAC,KAA8B,EAAE,IAAY;IAClE,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,CAAA;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAA2B,EAAE,IAAY;IACrE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,CAAC,IAAI,CAAC,CAAC,CAAA;IACrC,IAAI,IAAI,GAAG,IAAI,CAAA;IACf,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,GAAG,KAAK,CAAA;QACZ,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;YACxB,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC9E,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;gBACnB,IAAI,GAAG,IAAI,CAAA;YACb,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;AAC9E,CAAC;AAED,SAAS,QAAQ,CAAC,KAAuB,EAAE,IAAY;IACrD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;IAC3B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE;QAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC;YAAE,OAAO,GAAG,IAAI,IAAI,CAAC,EAAE,CAAA;AAC7E,CAAC;AAED,8GAA8G;AAC9G,SAAS,MAAM,CAAC,KAAkB;IAChC,OAAO,KAAK,CAAC,MAAM,IAAI,eAAe,CAAA;AACxC,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAkB;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC1B,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE;QACxF,OAAO,EAAE,CAAC;QACV,IAAI,EAAE;YACJ,QAAQ,EAAE;gBACR;oBACE,MAAM,EAAE,KAAK,CAAC,IAAI;oBAClB,IAAI,EAAE;wBACJ,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE;wBAC1B,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE;qBAC3B;iBACF;aACF;SACF;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,SAAoB,EAAE,KAAkB,EAAE,IAAkC;IACjG,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAA;IAC/C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACvC,MAAM,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAA;IAC1B,IAAI,OAAQ,CAAC,MAAM,KAAK,KAAK,CAAC,IAAI,IAAI,OAAQ,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IAC/E,MAAM,IAAI,GAAG,OAAQ,CAAC,IAAuC,CAAA;IAC7D,OAAO,CACL,IAAI,CAAC,MAAM,KAAK,CAAC;QACjB,IAAI,CAAC,CAAC,CAAE,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,CAAC,CAAC,CAAE,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,CAAC,CAAC,CAAE,CAAC,KAAK,KAAK,IAAI,CAAC,GAAG;QAC3B,IAAI,CAAC,CAAC,CAAE,CAAC,KAAK,KAAK,IAAI,CAAC,GAAG;QAC3B,SAAS,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG;QACjC,SAAS,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,CAClC,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,KAAkB,EAAE,IAAgB,EAAE,IAA2B;IAC5F,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,EAAE,CAAA;IACjC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,CAAA;IAEhE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAA;QAC3B,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC3C,CAAC;IAED,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAA;QACzC,MAAM,KAAK,GAAgB;YACzB,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,QAAQ,CAAC;YACtE,KAAK,EAAE,IAAI,CAAC,IAAI;YAChB,IAAI;YACJ,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE;YACpE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;SAC3B,CAAA;QACD,IAAI,IAAI,KAAK,YAAY;YAAE,KAAK,CAAC,MAAM,GAAG,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAA;QAEjE,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,CAAA;QACzC,MAAM,KAAK,GAAG,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;QACpG,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAA;QACtC,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QAEzC,OAAO,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,UAAU,EAAE,MAAM,CAAC,EAAE,CAAA;IACtF,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAExC,MAAM,IAAI,GAAgB,EAAE,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAA;IAC/C,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC,MAAM,CAAA;SACxC,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,WAAW,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,EAAE,CAAC;QACtG,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAA;IACvC,CAAC;IAED,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;AAC5C,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,UAAU,CACxB,KAAkB,EAClB,IAAY,EACZ,KAA4E;IAE5E,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;IACjE,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3B,MAAM,IAAI,GAAgB,EAAE,GAAG,QAAQ,EAAE,GAAG,KAAK,EAAE,CAAA;IACnD,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC,MAAM,CAAA;IACtD,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;AAC5C,CAAC;AAED,SAAS,YAAY,CAAC,KAAkB,EAAE,MAAmB,EAAE,KAAkB;IAC/E,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;IACpF,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAC1B,MAAM,WAAW,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,CAAA;IAClF,MAAM,WAAW,GAAG,GAAG,CAAC,GAAG,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAA;IAElF,IAAI,CAAC,WAAW,IAAI,CAAC,WAAW;QAAE,OAAO,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,CAAA;IAE7D,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE;QAC1D,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC;YAAE,OAAO,SAAS,CAAA;QAC5D,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAA;QACtC,OAAO,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAA;IAC3G,CAAC,CAAC,CAAA;IACF,OAAO,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AACzC,CAAC;AAED,SAAS,YAAY,CAAC,KAAkB,EAAE,IAAY;IACpD,MAAM,IAAI,GAAG,CAAgD,GAAM,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAA;IAEvI,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;SACxC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,SAAS,EAAE,IAAI,EAAE,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,CAAc,CAAC;SAC9G,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC1D,MAAM,UAAU,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;SACxC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAmD,CAAC;SAClG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC3C,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;IAExE,MAAM,IAAI,GAAgB,EAAE,GAAG,KAAK,EAAE,CAAA;IACtC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;;QACtC,OAAO,IAAI,CAAC,MAAM,CAAA;IACvB,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS;QAAE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;IAChE,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS;QAAE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;IAChE,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport type { FigureJoint, FigurePart, JointType, Vec3 } from '@hatiolab/figure-model'\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * Editing joints and parents (ADR-0066). Pure functions over the draft and the part list, so the inspector\n * only renders and dispatches.\n *\n * A joint is always created together with the parameter that drives it. Values reach a joint only through\n * `parameters`, and the preview draws a slider per parameter, so a joint without one could be defined but\n * never seen moving.\n */\n\ntype Parameter = NonNullable<FigureDraft['parameters']>[number]\n\n/** The limits a new joint starts with. Revolute in degrees, prismatic in millimetres. */\nconst NEW_LIMITS: Record<Exclude<JointType, 'continuous'>, { min: number; max: number }> = {\n revolute: { min: -90, max: 90 },\n prismatic: { min: 0, max: 500 }\n}\n\n/** The span a parameter driving a continuous joint offers: one turn either way. */\nconst CONTINUOUS_SPAN = { min: -360, max: 360 }\n\n/** The joint whose child is this part, if any. */\nexport function jointOf(draft: FigureDraft | undefined, part: string): FigureJoint | undefined {\n return draft?.joints?.find(joint => joint.child === part)\n}\n\n/**\n * Parts a part may be attached to: every other part except itself and anything attached below it, since\n * either would close a loop the format refuses (`parent-cycle`).\n */\nexport function parentChoices(parts: readonly PartModel[], part: string): string[] {\n const below = new Set<string>([part])\n let grew = true\n while (grew) {\n grew = false\n for (const one of parts) {\n if (one.parent !== undefined && below.has(one.parent) && !below.has(one.name)) {\n below.add(one.name)\n grew = true\n }\n }\n }\n return parts.map(one => one.name).filter(name => !!name && !below.has(name))\n}\n\nfunction freeName(taken: Iterable<string>, stem: string): string {\n const used = new Set(taken)\n if (!used.has(stem)) return stem\n for (let n = 2; ; n++) if (!used.has(`${stem}-${n}`)) return `${stem}-${n}`\n}\n\n/** The span the driving parameter offers for a joint: its limits, or one turn either way when it has none. */\nfunction spanOf(joint: FigureJoint): { min: number; max: number } {\n return joint.limits ?? CONTINUOUS_SPAN\n}\n\nfunction drivingParameter(joint: FigureJoint): Parameter {\n const span = spanOf(joint)\n return {\n name: joint.name,\n range: { unit: joint.type === 'prismatic' ? 'mm' : 'deg', min: span.min, max: span.max },\n default: 0,\n clip: {\n channels: [\n {\n target: joint.name,\n keys: [\n { at: 0, value: span.min },\n { at: 1, value: span.max }\n ]\n }\n ]\n }\n }\n}\n\n/**\n * Whether a parameter is the plain one `setJointType` made for a joint: one channel on that joint, keys at\n * 0 and 1 spanning the given limits. Only such a parameter follows the joint when its limits or type change;\n * one the author reshaped is left as written.\n */\nfunction isPlainDriver(parameter: Parameter, joint: FigureJoint, span: { min: number; max: number }): boolean {\n const channels = parameter.clip?.channels ?? []\n if (channels.length !== 1) return false\n const [channel] = channels\n if (channel!.target !== joint.name || channel!.path !== undefined) return false\n const keys = channel!.keys as { at: number; value: number }[]\n return (\n keys.length === 2 &&\n keys[0]!.at === 0 &&\n keys[1]!.at === 1 &&\n keys[0]!.value === span.min &&\n keys[1]!.value === span.max &&\n parameter.range?.min === span.min &&\n parameter.range?.max === span.max\n )\n}\n\n/**\n * Sets how a part moves relative to its parent, or clears the joint with `undefined`.\n *\n * - From none: adds a joint whose origin is the centre of the part's bottom face, with a vertical axis and\n * starting limits, and a parameter that drives it over those limits.\n * - To none: removes the joint and every channel that drove it; a parameter or clip left with no channel\n * is removed too, because the format refuses an empty one.\n * - Between types: keeps the origin and axis, sets limits for the new type (none for continuous), and\n * moves a plain driving parameter along with them.\n */\nexport function setJointType(draft: FigureDraft, part: FigurePart, type: JointType | undefined): FigureDraft {\n const joints = draft.joints ?? []\n const existing = joints.find(joint => joint.child === part.name)\n\n if (type === undefined) {\n if (!existing) return draft\n return withoutJoint(draft, existing.name)\n }\n\n if (!existing) {\n const { position, size } = part.transform\n const joint: FigureJoint = {\n name: freeName([...joints.map(one => one.name)], `${part.name}-joint`),\n child: part.name,\n type,\n origin: { x: position.x, y: position.y - size.y / 2, z: position.z },\n axis: { x: 0, y: 1, z: 0 }\n }\n if (type !== 'continuous') joint.limits = { ...NEW_LIMITS[type] }\n\n const parameters = draft.parameters ?? []\n const taken = [...parameters.map(one => one.name), ...(draft.animations ?? []).map(one => one.name)]\n const driver = drivingParameter(joint)\n driver.name = freeName(taken, joint.name)\n\n return { ...draft, joints: [...joints, joint], parameters: [...parameters, driver] }\n }\n\n if (existing.type === type) return draft\n\n const next: FigureJoint = { ...existing, type }\n if (type === 'continuous') delete next.limits\n else if (existing.type === 'continuous' || (existing.type === 'prismatic') !== (type === 'prismatic')) {\n next.limits = { ...NEW_LIMITS[type] }\n }\n\n return replaceJoint(draft, existing, next)\n}\n\n/** Changes a joint's origin, axis or limits. A plain driving parameter follows new limits. */\nexport function patchJoint(\n draft: FigureDraft,\n name: string,\n patch: { origin?: Vec3; axis?: Vec3; limits?: { min: number; max: number } }\n): FigureDraft {\n const existing = draft.joints?.find(joint => joint.name === name)\n if (!existing) return draft\n const next: FigureJoint = { ...existing, ...patch }\n if (existing.type === 'continuous') delete next.limits\n return replaceJoint(draft, existing, next)\n}\n\nfunction replaceJoint(draft: FigureDraft, before: FigureJoint, after: FigureJoint): FigureDraft {\n const joints = (draft.joints ?? []).map(joint => (joint === before ? after : joint))\n const was = spanOf(before)\n const unitChanged = (before.type === 'prismatic') !== (after.type === 'prismatic')\n const spanChanged = was.min !== spanOf(after).min || was.max !== spanOf(after).max\n\n if (!unitChanged && !spanChanged) return { ...draft, joints }\n\n const parameters = (draft.parameters ?? []).map(parameter => {\n if (!isPlainDriver(parameter, before, was)) return parameter\n const driver = drivingParameter(after)\n return { ...parameter, range: driver.range, clip: { ...parameter.clip, channels: driver.clip.channels } }\n })\n return { ...draft, joints, parameters }\n}\n\nfunction withoutJoint(draft: FigureDraft, name: string): FigureDraft {\n const keep = <T extends { channels?: { target: string }[] }>(one: T) => (one.channels ?? []).filter(channel => channel.target !== name)\n\n const parameters = (draft.parameters ?? [])\n .map(parameter => ({ ...parameter, clip: { ...parameter.clip, channels: keep(parameter.clip) } }) as Parameter)\n .filter(parameter => parameter.clip.channels.length > 0)\n const animations = (draft.animations ?? [])\n .map(clip => ({ ...clip, channels: keep(clip) }) as NonNullable<FigureDraft['animations']>[number])\n .filter(clip => clip.channels.length > 0)\n const joints = (draft.joints ?? []).filter(joint => joint.name !== name)\n\n const next: FigureDraft = { ...draft }\n if (joints.length > 0) next.joints = joints\n else delete next.joints\n if (draft.parameters !== undefined) next.parameters = parameters\n if (draft.animations !== undefined) next.animations = animations\n return next\n}\n"]}
@@ -12,6 +12,11 @@ export interface ProposalSession {
12
12
  picked: Set<string>;
13
13
  note: string;
14
14
  metrics?: Pick<FigureProposal, 'grade' | 'triangles' | 'groups' | 'attempts' | 'quality'>;
15
+ /**
16
+ * The release-check fix this session was opened from, if any. The page translates the title from it;
17
+ * this module stays free of i18n because node tests import it directly.
18
+ */
19
+ fix?: FigureGateFix;
15
20
  }
16
21
  /** 서버 후보를 검토 세션으로 연다. 저장 형식이 아니면 화면에 후보를 열지 않는다. */
17
22
  export declare function openAiProposal(current: FigureSource | undefined, proposal: FigureProposal): ProposalSession;
@@ -6,7 +6,7 @@ function selectedKeys(current, source) {
6
6
  export function openAiProposal(current, proposal) {
7
7
  const source = JSON.parse(proposal.source);
8
8
  if (!source || !Array.isArray(source.parts)) {
9
- throw new Error('AI 후보의 Figure 형식이 올바르지 않습니다.');
9
+ throw new Error('the AI candidate is not a figure source: parts is missing or not an array');
10
10
  }
11
11
  return {
12
12
  source,
@@ -32,7 +32,8 @@ export function openSizingFix(current, fix) {
32
32
  };
33
33
  return {
34
34
  source,
35
- title: `발행 검사 수정안 · ${fix.part}.anchor.${fix.axis}: ${fix.from ?? 'auto'} → ${fix.to}`,
35
+ title: '',
36
+ fix,
36
37
  picked: selectedKeys(current, source),
37
38
  note: ''
38
39
  };
@@ -1 +1 @@
1
- {"version":3,"file":"proposal-session.js","sourceRoot":"","sources":["../../client/modeller/proposal-session.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigureSource } from '@hatiolab/figure-model'\n\nimport type { FigureGateFix, FigureProposal, ProposalFeedback } from '../types.js'\nimport { diffProposal } from './proposal.js'\n\n/**\n * 한 번의 AI 후보 검토에만 존재하는 상태다.\n *\n * 후보는 정본이 아니다. 후보 원본·선택·메모·측정값을 한 값으로 묶어 두면 화면이\n * 수락/거절 중 일부만 이전 후보에서 남기는 일이 없다.\n */\nexport interface ProposalSession {\n source: FigureSource\n title: string\n picked: Set<string>\n note: string\n metrics?: Pick<FigureProposal, 'grade' | 'triangles' | 'groups' | 'attempts' | 'quality'>\n}\n\nfunction selectedKeys(current: FigureSource | undefined, source: FigureSource): Set<string> {\n return new Set(diffProposal(current, source).map(change => change.key))\n}\n\n/** 서버 후보를 검토 세션으로 연다. 저장 형식이 아니면 화면에 후보를 열지 않는다. */\nexport function openAiProposal(current: FigureSource | undefined, proposal: FigureProposal): ProposalSession {\n const source = JSON.parse(proposal.source) as FigureSource\n if (!source || !Array.isArray(source.parts)) {\n throw new Error('AI 후보의 Figure 형식이 올바르지 않습니다.')\n }\n\n return {\n source,\n title: '',\n picked: selectedKeys(current, source),\n note: '',\n metrics: {\n grade: proposal.grade,\n triangles: proposal.triangles,\n groups: proposal.groups,\n attempts: proposal.attempts,\n quality: proposal.quality\n }\n }\n}\n\n/** 발행 게이트가 측정한 한-앵커 수정도 같은 후보 세션으로 연다. */\nexport function openSizingFix(current: FigureSource, fix: FigureGateFix): ProposalSession | undefined {\n if (!current.parts.some(part => part.name === fix.part)) return undefined\n\n const source: FigureSource = {\n ...current,\n parts: current.parts.map(part =>\n part.name === fix.part ? { ...part, anchor: { ...(part.anchor ?? {}), [fix.axis]: fix.to } } : part\n )\n }\n\n return {\n source,\n title: `발행 검사 수정안 · ${fix.part}.anchor.${fix.axis}: ${fix.from ?? 'auto'} → ${fix.to}`,\n picked: selectedKeys(current, source),\n note: ''\n }\n}\n\n/** 후보 검토 중 사람의 선택을 뒤집는다. 기존 Set을 바꾸지 않아 Lit 상태 경계도 보존한다. */\nexport function toggleProposalChange(session: ProposalSession, key: string): ProposalSession {\n const picked = new Set(session.picked)\n if (picked.has(key)) picked.delete(key)\n else picked.add(key)\n return { ...session, picked }\n}\n\n/** 사람의 결정을, 다음 AI 요청에만 쓰는 제한된 피드백으로 만든다. */\nexport function feedbackFromSession(\n current: FigureSource | undefined,\n session: ProposalSession,\n outcome: ProposalFeedback['outcome']\n): ProposalFeedback | undefined {\n if (!session.metrics) return undefined\n\n return {\n outcome,\n selectedChanges: outcome === 'accepted' ? session.picked.size : 0,\n totalChanges: diffProposal(current, session.source).length,\n note: session.note.trim().slice(0, 500) || undefined,\n grade: session.metrics.grade,\n quality: {\n status: session.metrics.quality.status,\n findingCodes: session.metrics.quality.findings.map(finding => finding.code)\n }\n }\n}\n"]}
1
+ {"version":3,"file":"proposal-session.js","sourceRoot":"","sources":["../../client/modeller/proposal-session.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigureSource } from '@hatiolab/figure-model'\n\nimport type { FigureGateFix, FigureProposal, ProposalFeedback } from '../types.js'\nimport { diffProposal } from './proposal.js'\n\n/**\n * 한 번의 AI 후보 검토에만 존재하는 상태다.\n *\n * 후보는 정본이 아니다. 후보 원본·선택·메모·측정값을 한 값으로 묶어 두면 화면이\n * 수락/거절 중 일부만 이전 후보에서 남기는 일이 없다.\n */\nexport interface ProposalSession {\n source: FigureSource\n title: string\n picked: Set<string>\n note: string\n metrics?: Pick<FigureProposal, 'grade' | 'triangles' | 'groups' | 'attempts' | 'quality'>\n /**\n * The release-check fix this session was opened from, if any. The page translates the title from it;\n * this module stays free of i18n because node tests import it directly.\n */\n fix?: FigureGateFix\n}\n\nfunction selectedKeys(current: FigureSource | undefined, source: FigureSource): Set<string> {\n return new Set(diffProposal(current, source).map(change => change.key))\n}\n\n/** 서버 후보를 검토 세션으로 연다. 저장 형식이 아니면 화면에 후보를 열지 않는다. */\nexport function openAiProposal(current: FigureSource | undefined, proposal: FigureProposal): ProposalSession {\n const source = JSON.parse(proposal.source) as FigureSource\n if (!source || !Array.isArray(source.parts)) {\n throw new Error('the AI candidate is not a figure source: parts is missing or not an array')\n }\n\n return {\n source,\n title: '',\n picked: selectedKeys(current, source),\n note: '',\n metrics: {\n grade: proposal.grade,\n triangles: proposal.triangles,\n groups: proposal.groups,\n attempts: proposal.attempts,\n quality: proposal.quality\n }\n }\n}\n\n/** 발행 게이트가 측정한 한-앵커 수정도 같은 후보 세션으로 연다. */\nexport function openSizingFix(current: FigureSource, fix: FigureGateFix): ProposalSession | undefined {\n if (!current.parts.some(part => part.name === fix.part)) return undefined\n\n const source: FigureSource = {\n ...current,\n parts: current.parts.map(part =>\n part.name === fix.part ? { ...part, anchor: { ...(part.anchor ?? {}), [fix.axis]: fix.to } } : part\n )\n }\n\n return {\n source,\n title: '',\n fix,\n picked: selectedKeys(current, source),\n note: ''\n }\n}\n\n/** 후보 검토 중 사람의 선택을 뒤집는다. 기존 Set을 바꾸지 않아 Lit 상태 경계도 보존한다. */\nexport function toggleProposalChange(session: ProposalSession, key: string): ProposalSession {\n const picked = new Set(session.picked)\n if (picked.has(key)) picked.delete(key)\n else picked.add(key)\n return { ...session, picked }\n}\n\n/** 사람의 결정을, 다음 AI 요청에만 쓰는 제한된 피드백으로 만든다. */\nexport function feedbackFromSession(\n current: FigureSource | undefined,\n session: ProposalSession,\n outcome: ProposalFeedback['outcome']\n): ProposalFeedback | undefined {\n if (!session.metrics) return undefined\n\n return {\n outcome,\n selectedChanges: outcome === 'accepted' ? session.picked.size : 0,\n totalChanges: diffProposal(current, session.source).length,\n note: session.note.trim().slice(0, 500) || undefined,\n grade: session.metrics.grade,\n quality: {\n status: session.metrics.quality.status,\n findingCodes: session.metrics.quality.findings.map(finding => finding.code)\n }\n }\n}\n"]}
@@ -6,28 +6,6 @@ import '../modeller/figure-parts.js';
6
6
  import '../modeller/figure-inspector.js';
7
7
  import { PageView } from '@operato/shell';
8
8
  declare const FigureModellerPageBase: typeof PageView;
9
- /**
10
- * Figure 저작면.
11
- *
12
- * ## 세 칸이다
13
- *
14
- * 왼쪽 도형 팔레트 · 부품 목록 · 속성
15
- * 가운데 미리보기 — `FigureInstance` 를 실제로 세운다
16
- * 오른쪽 판단 — 늘 떠 있다
17
- *
18
- * 미리보기가 씬 위에서 실제로 동작하는 것이 이 도구의 전제다. **편집한 것이 곧 실제 결과물**이라야
19
- * "모델러에서는 되는데 도면에서는 다르다"가 생기지 않는다.
20
- *
21
- * ## 편집 데이터는 씬 모델 하나뿐이다
22
- *
23
- * 이 페이지가 갖는 것은 초안 하나와 부품 목록 — 미리보기에 그대로 세워지는 씬 모델이다.
24
- * 저장 형식(`FigureSource`)으로 바꾸는 것은 **경계에서만** 한다: 저장할 때, 판단할 때,
25
- * AI 후보와 비교할 때. 그 변환은 `figure-source.ts` 한 곳에서만 일어난다.
26
- *
27
- * 팔레트도 속성 편집기도 이 데이터를 직접 수정하지 않는다. 무엇을 하겠다는 것만 알리고,
28
- * 바꾸는 것은 이 페이지다. 두 곳에서 고치면 실행 취소도 저장도 어느 쪽이 맞는지 알 수
29
- * 없게 된다.
30
- */
31
9
  export declare class FigureModellerPage extends FigureModellerPageBase {
32
10
  static styles: import("lit").CSSResult;
33
11
  /** 편집 중인 Figure. 새로 만드는 중이면 `id` 가 없다. */
@@ -368,6 +346,8 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
368
346
  private fillThumbnail;
369
347
  private save;
370
348
  private doSave;
349
+ /** Title of the candidate bar. A candidate opened from a release-check fix is described by translating that fix here. */
350
+ private proposalTitle;
371
351
  private renderSaveModal;
372
352
  }
373
353
  export {};
@@ -13,7 +13,7 @@ import { i18next, localize } from '@operato/i18n';
13
13
  import { navigate, PageView } from '@operato/shell';
14
14
  import { openOverlay } from '@operato/layout';
15
15
  import { requestFigureAI, consumePendingFigureProposal } from '../modeller/figure-ai-target.js';
16
- import { validate } from '@hatiolab/figure-model';
16
+ import { isPlaceholderType, placeholderType, TYPE_LENGTH, TYPE_PATTERN, validate } from '@hatiolab/figure-model';
17
17
  import * as edits from '../modeller/part-edits.js';
18
18
  import * as proposals from '../modeller/proposal.js';
19
19
  import * as proposalSessions from '../modeller/proposal-session.js';
@@ -91,6 +91,17 @@ function startingModel(type) {
91
91
  * 바꾸는 것은 이 페이지다. 두 곳에서 고치면 실행 취소도 저장도 어느 쪽이 맞는지 알 수
92
92
  * 없게 된다.
93
93
  */
94
+ /**
95
+ * Whether this name could be released, by the format's own rules (`type-not-identifier`,
96
+ * `type-is-placeholder`). Saving is not blocked, but the type cannot change after creation, so the
97
+ * author has to hear it now to avoid a draft that can never be released.
98
+ */
99
+ function releasableType(type) {
100
+ return (!isPlaceholderType(type) &&
101
+ TYPE_PATTERN.test(type) &&
102
+ type.length >= TYPE_LENGTH.min &&
103
+ type.length <= TYPE_LENGTH.max);
104
+ }
94
105
  let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase {
95
106
  constructor() {
96
107
  super(...arguments);
@@ -1051,10 +1062,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1051
1062
  const { errors, violations } = validate(picked);
1052
1063
  return html `
1053
1064
  <div decide>
1054
- <span>${this.proposalSession.title || i18next.t('figure.text.assistant-proposed-a-figure')}</span>
1065
+ <span>${this.proposalTitle(this.proposalSession)}</span>
1055
1066
  <div spacer></div>
1056
1067
  <button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
1057
- ${stale ? html `<span role="alert">원본이 변경되었습니다. 후보를 취소하고 AI 지원에서 다시 요청하세요.</span>` : ''}
1068
+ ${stale ? html `<span role="alert">${i18next.t('figure.text.source-changed-ask-again')}</span>` : ''}
1058
1069
  <button take ?disabled=${stale || errors.length > 0 || this.proposalSession.picked.size === 0} @click=${() => this.take()}>
1059
1070
  ${i18next.t('figure.button.take-n-changes', { n: this.proposalSession.picked.size })}
1060
1071
  </button>
@@ -1352,7 +1363,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1352
1363
  this.proposalFeedback = [];
1353
1364
  this.mode = 'edit';
1354
1365
  // 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.
1355
- const draftType = `FIGURE_${Date.now().toString(36).toUpperCase()}`;
1366
+ const draftType = placeholderType();
1356
1367
  const { draft, parts } = startingModel(draftType);
1357
1368
  this.figure = { id: '', type: draftType, name: '' };
1358
1369
  this.draft = draft;
@@ -1616,8 +1627,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1616
1627
  if (!this.figure.id) {
1617
1628
  // 신규 저장: 타입 코드와 표시 이름을 명확히 팝업으로 확인/입력받음
1618
1629
  let defaultType = this.draft?.figureType || this.figure.type || '';
1619
- if (!defaultType || defaultType.startsWith('FIGURE_')) {
1620
- defaultType = 'FIGURE';
1630
+ /*
1631
+ A placeholder is not prefilled. This used to prefill 'FIGURE', inviting the author to confirm a
1632
+ meaningless name that cannot change later and that the release gate refuses.
1633
+ */
1634
+ if (!defaultType || isPlaceholderType(defaultType)) {
1635
+ defaultType = '';
1621
1636
  }
1622
1637
  this.saveModalType = defaultType;
1623
1638
  this.saveModalName = this.figure.name || '';
@@ -1638,7 +1653,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1638
1653
  const typeToSave = (this.saveModalType || this.draft?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase();
1639
1654
  const nameToSave = (this.saveModalName || this.figure.name || typeToSave).trim();
1640
1655
  if (!typeToSave) {
1641
- throw new Error('타입 이름을 입력하세요.');
1656
+ throw new Error(i18next.t('figure.text.enter-a-type-name'));
1642
1657
  }
1643
1658
  const taken = await fetchFigureTypeNames();
1644
1659
  if (taken.includes(typeToSave)) {
@@ -1693,6 +1708,19 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1693
1708
  this.saving = false;
1694
1709
  }
1695
1710
  }
1711
+ /** Title of the candidate bar. A candidate opened from a release-check fix is described by translating that fix here. */
1712
+ proposalTitle(session) {
1713
+ const fix = session.fix;
1714
+ if (fix) {
1715
+ return i18next.t('figure.text.release-check-fix', {
1716
+ part: fix.part,
1717
+ axis: fix.axis.toUpperCase(),
1718
+ from: fix.from ?? 'auto',
1719
+ to: fix.to
1720
+ });
1721
+ }
1722
+ return session.title || i18next.t('figure.text.assistant-proposed-a-figure');
1723
+ }
1696
1724
  renderSaveModal() {
1697
1725
  if (!this.showSaveModal)
1698
1726
  return nothing;
@@ -1700,23 +1728,26 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1700
1728
  <div class="save-modal-backdrop" @click=${(e) => { if (e.target === e.currentTarget)
1701
1729
  this.showSaveModal = false; }}>
1702
1730
  <div class="save-modal-card" role="dialog" aria-modal="true">
1703
- <h3 class="save-modal-title">${i18next.t('figure.title.save-new-figure', { defaultValue: '도형 초안 저장' })}</h3>
1704
- <p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc', { defaultValue: '도면과 캔버스에서 식별할 고유 타입 코드와 이름을 정합니다.' })}</p>
1731
+ <h3 class="save-modal-title">${i18next.t('figure.title.save-new-figure')}</h3>
1732
+ <p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc')}</p>
1705
1733
 
1706
1734
  <div class="save-modal-field">
1707
- <label>${i18next.t('figure.label.figure-type', { defaultValue: '타입 코드 (영문 대문자)' })}</label>
1735
+ <label>${i18next.t('figure.label.figure-type')}</label>
1708
1736
  <input
1709
1737
  .value=${this.saveModalType}
1710
- placeholder="예: OHT, AGV_LIFTER"
1738
+ placeholder=${i18next.t('figure.text.figure-type-example')}
1711
1739
  @input=${(e) => (this.saveModalType = e.target.value.trim().toUpperCase())}
1712
1740
  />
1741
+ ${this.saveModalType && !releasableType(this.saveModalType)
1742
+ ? html `<div class="save-modal-error">${i18next.t('figure.text.type-name-will-not-release')}</div>`
1743
+ : nothing}
1713
1744
  </div>
1714
1745
 
1715
1746
  <div class="save-modal-field">
1716
- <label>${i18next.t('figure.label.name', { defaultValue: '도형 이름' })}</label>
1747
+ <label>${i18next.t('figure.label.name')}</label>
1717
1748
  <input
1718
1749
  .value=${this.saveModalName}
1719
- placeholder="예: 오버헤드 호이스트 트랜스포트"
1750
+ placeholder=${i18next.t('figure.text.figure-name-example')}
1720
1751
  @input=${(e) => (this.saveModalName = e.target.value)}
1721
1752
  />
1722
1753
  </div>
@@ -1725,10 +1756,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1725
1756
 
1726
1757
  <div class="save-modal-actions">
1727
1758
  <button class="btn-cancel" @click=${() => (this.showSaveModal = false)}>
1728
- ${i18next.t('figure.button.cancel', { defaultValue: '취소' })}
1759
+ ${i18next.t('figure.button.cancel')}
1729
1760
  </button>
1730
1761
  <button class="btn-confirm" ?disabled=${this.saving || !this.saveModalType} @click=${() => this.doSave()}>
1731
- ${this.saving ? i18next.t('figure.text.saving-figure', { defaultValue: '저장 중...' }) : i18next.t('figure.button.save', { defaultValue: '저장' })}
1762
+ ${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
1732
1763
  </button>
1733
1764
  </div>
1734
1765
  </div>