@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.
- package/client/graphql/index.ts +4 -1
- package/client/modeller/figure-animations.ts +229 -29
- package/client/modeller/figure-history-panel.ts +2 -2
- package/client/modeller/figure-inspector.ts +151 -5
- package/client/modeller/figure-preview.ts +5 -5
- package/client/modeller/figure-source.ts +33 -45
- package/client/modeller/figure-view.ts +20 -0
- package/client/modeller/joint-edits.ts +201 -0
- package/client/modeller/proposal-session.ts +8 -2
- package/client/pages/figure-modeller-page.ts +50 -15
- package/dist-client/graphql/index.js +4 -1
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +19 -0
- package/dist-client/modeller/figure-animations.js +206 -22
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -2
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +11 -0
- package/dist-client/modeller/figure-inspector.js +124 -4
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +5 -5
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +12 -1
- package/dist-client/modeller/figure-source.js +13 -40
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +16 -0
- package/dist-client/modeller/figure-view.js +15 -0
- package/dist-client/modeller/figure-view.js.map +1 -1
- package/dist-client/modeller/joint-edits.d.ts +29 -0
- package/dist-client/modeller/joint-edits.js +175 -0
- package/dist-client/modeller/joint-edits.js.map +1 -0
- package/dist-client/modeller/proposal-session.d.ts +5 -0
- package/dist-client/modeller/proposal-session.js +3 -2
- package/dist-client/modeller/proposal-session.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +2 -22
- package/dist-client/pages/figure-modeller-page.js +46 -15
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -3
- package/test/ai-proposal-contract.test.ts +3 -2
- package/test/figure-source.test.ts +43 -57
- package/test/i18n-prefix-guard.test.ts +2 -3
- package/test/joint-edits.test.ts +130 -0
- package/test/preview-board-depth.test.ts +27 -0
- package/translations/en.json +119 -80
- package/translations/ja.json +208 -58
- package/translations/ko.json +139 -100
- package/translations/ms.json +206 -56
- 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":"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","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
|
|
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:
|
|
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":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;
|
|
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.
|
|
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"
|
|
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 =
|
|
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
|
-
|
|
1620
|
-
|
|
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'
|
|
1704
|
-
<p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc'
|
|
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'
|
|
1735
|
+
<label>${i18next.t('figure.label.figure-type')}</label>
|
|
1708
1736
|
<input
|
|
1709
1737
|
.value=${this.saveModalType}
|
|
1710
|
-
placeholder
|
|
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'
|
|
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'
|
|
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'
|
|
1762
|
+
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
1732
1763
|
</button>
|
|
1733
1764
|
</div>
|
|
1734
1765
|
</div>
|