@things-factory/figure-service 10.1.44 → 10.1.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.44",
3
+ "version": "10.1.45",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -23,7 +23,7 @@
23
23
  "migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration"
24
24
  },
25
25
  "dependencies": {
26
- "@hatiolab/figure-model": "^0.1.42",
26
+ "@hatiolab/figure-model": "^0.1.47",
27
27
  "@operato/typeorm-history": "^10.0.0",
28
28
  "@things-factory/ai-client-base": "^10.1.39",
29
29
  "@things-factory/auth-base": "^10.1.44",
@@ -33,5 +33,5 @@
33
33
  "graphql-upload": "^15.0.2",
34
34
  "koa-router": "^7.4.0"
35
35
  },
36
- "gitHead": "7c95c4e75e155e51c714409e865d96f317232c23"
36
+ "gitHead": "d72e1fd1285797fcd7839d97a18e31f5bf076ae3"
37
37
  }
@@ -160,13 +160,32 @@ describe('figure-format — 판을 알아보는 문', () => {
160
160
  expect(inspectAnySource(null).findings[0].code).toBe('no-source')
161
161
  })
162
162
 
163
- it('직전 발행판이 있는 V3 재발행은 견주지 못했다고 말한다 — 막지는 않는다', () => {
164
- /* V2 는 지난 발행과 견주어 놓인 인스턴스를 깨는 변경을 잡는다. V3 는 그 비교가 아직 없으니 그 사실을 적는다. */
163
+ it('바뀐 것 없는 V3 재발행은 아무 말도 없이 지난다', () => {
165
164
  const again = inspectAnySource(v3Source(), v3Source())
166
- const warning = again.findings.find(f => f.code === 'released-unreadable')
165
+ expect(again.blocked).toBe(false)
166
+ expect(again.findings.filter(f => f.code !== 'state-range-sampled')).toEqual([])
167
+ })
168
+
169
+ it('놓인 인스턴스가 기대던 것을 없앤 V3 재발행은 막는다 — 없어진 능력을 이름으로 댄다', () => {
170
+ const before = v3Asset()
171
+ before.document.capabilities.names = ['Holdable']
172
+ const inspection = inspectAnySource(v3Source(), serializeV3Asset(before))
173
+ const gone = inspection.findings.find(f => f.code === 'capability-gone')
174
+ expect(gone).toBeDefined()
175
+ expect(gone!.blocking).toBe(true)
176
+ expect(gone!.at).toBe('Holdable')
177
+ expect(gone!.message).toContain('Holdable')
178
+ expect(gone!.how).toBeTruthy()
179
+ expect(inspection.blocked).toBe(true)
180
+ })
181
+
182
+ it('직전 발행판이 V2 면 견주지 못했다고 말한다 — 막지는 않는다', () => {
183
+ /* 전환한 도형의 직전 발행판은 모두 V2 다. 두 판을 견주는 길은 없으니 그 사실을 적는다. */
184
+ const converted = inspectAnySource(v3Source(), v2Source())
185
+ const warning = converted.findings.find(f => f.code === 'released-unreadable')
167
186
  expect(warning).toBeDefined()
168
187
  expect(warning!.blocking).toBe(false)
169
- expect(again.blocked).toBe(false)
188
+ expect(converted.blocked).toBe(false)
170
189
 
171
190
  /* 첫 발행에는 견줄 것이 없으니 그 말도 없다. */
172
191
  expect(inspectAnySource(v3Source()).findings.some(f => f.code === 'released-unreadable')).toBe(false)
@@ -227,6 +246,34 @@ describe('figure-format — 판을 알아보는 문', () => {
227
246
  expect(inspectAnySource(empty).findings.find(f => f.code === 'occupancy-undeclared')!.how).toMatch(/^Add a shape first/)
228
247
  })
229
248
 
249
+ /*
250
+ V2 의 sizing-splits 가 V3 판정에 온 것(2026-09-24). 드라이룸 램프처럼 — 붙어 있던 부품이 도형을 키우면 떨어진다.
251
+ 여기서는 손으로 만든다: 램프의 높이가 도형 높이(size.y)를 따라 올라가고, 몸통은 그대로다.
252
+ */
253
+ const splitting = () => {
254
+ let asset = createV3Asset('Lamp post')
255
+ asset = addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
256
+ asset = addV3Box(asset, { id: 'lamp', size: { x: 40, y: 20, z: 40 }, position: { x: 0, y: 10, z: 0 } })
257
+ const model = asset.document.model
258
+ model.inputs.push({ id: 'size.y', unit: 'mm', min: 50, max: 20000, role: 'design' } as never)
259
+ asset.designInputs['size.y'] = 100
260
+ model.nodes.push({ id: 'lamp.lift', op: 'add@1', args: ['size.y', 'lamp.position.y'], outputs: { value: 'lamp.lift.value' } } as never)
261
+ const pose = model.nodes.find(n => n.id === 'lamp.pose')!
262
+ pose.args[1] = 'lamp.lift.value'
263
+ return serializeV3Asset(asset)
264
+ }
265
+
266
+ it('크기를 바꾸면 떨어지는 부품을 막고, 어느 부품이 어느 축에서인지 · 왜 · 어떻게를 말한다', () => {
267
+ const finding = inspectAnySource(splitting(), null, translatorOf(KO)).findings.find(f => f.code === 'parts-split-on-resize')
268
+
269
+ expect(finding?.blocking).toBe(true)
270
+ expect(finding!.message).toBe("크기를 Y 로 2배로 하면 'lamp' 가 나머지에서 떨어집니다.")
271
+ expect(finding!.why).toMatch(/부서진/)
272
+ expect(finding!.how).toContain("'lamp'")
273
+ expect(finding!.how).toContain('여기에 고정')
274
+ expect(`${finding!.message}${finding!.why}${finding!.how}`).not.toMatch(/come away|piece/)
275
+ })
276
+
230
277
  it('번역이 없으면 영어로 답한다 — 키를 찍지 않는다', () => {
231
278
  const finding = inspectAnySource(outside()).findings.find(f => f.code === 'part-outside-occupancy')!
232
279
 
@@ -20,8 +20,8 @@
20
20
  * No conversion. This door reads what is stored; turning a V2 asset into a V3 one is `convertV2ToV3`, run
21
21
  * deliberately against a recoverable copy (the designer's step 3), never as a side effect of a save.
22
22
  */
23
- import { compile, compileV3Asset, inspectV3Asset, isPlaceholderType, parseV3Asset, serializeV3Asset, v3CostOf } from '@hatiolab/figure-model'
24
- import type { V3ContractError, V3Violation } from '@hatiolab/figure-model'
23
+ import { compile, compileV3Asset, inspectV3Asset, isPlaceholderType, parseV3Asset, serializeV3Asset, v3ContractChanges, v3CostOf } from '@hatiolab/figure-model'
24
+ import type { V3Asset, V3ContractChange, V3ContractError, V3Violation } from '@hatiolab/figure-model'
25
25
 
26
26
  import type { GateFinding, Inspection } from './figure-gate.js'
27
27
  import { inspectSource } from './figure-gate.js'
@@ -159,6 +159,22 @@ function v3Wording(violation: V3Violation, say: Say, hasParts: boolean): Pick<Ga
159
159
  why: say(key('why'), {}, 'A tilted face cannot lie on the plane, so how the figure stands is not settled.'),
160
160
  how: say(key('how'), { part }, `Choose a mounting face parallel to the plane, or turn '${part}' upright.`)
161
161
  }
162
+ case 'PARTS_SPLIT_ON_RESIZE': {
163
+ /*
164
+ What comes off is every piece but the largest — the figure as the author sees it is the big piece, and the
165
+ small ones are what they will see hanging in the air (V2's sizing-splits, carried to V3 on 2026-09-24).
166
+ */
167
+ const resize = violation.resize!
168
+ const pieces = [...resize.pieces].sort((a, b) => b.length - a.length)
169
+ const parts = pieces.slice(1).flat().map(one => `'${one}'`).join(', ')
170
+ const axes = resize.axes.map(one => one.toUpperCase()).join('·')
171
+ const factor = resize.factor
172
+ return {
173
+ message: say(key('message'), { parts, axes, factor }, `At ${factor}x on ${axes}, ${parts} come away from the rest of the figure.`),
174
+ why: say(key('why'), {}, 'A figure placed at another size shows those parts hanging in the air — to a person, it looks broken.'),
175
+ how: say(key('how'), { parts }, `Fasten ${parts} to a face of the part they rest on, or press "fix here" under placement, so they stay put when the figure is resized.`)
176
+ }
177
+ }
162
178
  }
163
179
  }
164
180
 
@@ -187,9 +203,10 @@ export function inspectAnySource(
187
203
  if (format === 'v2') return inspectSource(sourceJson, releasedJson)
188
204
 
189
205
  let inspection
206
+ let asset: V3Asset | undefined
190
207
  let hasParts = true
191
208
  try {
192
- const asset = parseV3Asset(sourceJson)
209
+ asset = parseV3Asset(sourceJson)
193
210
  inspection = inspectV3Asset(asset)
194
211
  const cost = v3CostOf(asset)
195
212
  hasParts = cost.groups + cost.hidden > 0
@@ -235,23 +252,160 @@ export function inspectAnySource(
235
252
  blocking: false
236
253
  })
237
254
  }
238
- /*
239
- V2 compares a release with the one before it and names the changes that break placed instances (ADR-0063
240
- decision 3). Nothing does that for a V3 asset yet, and a V2→V3 conversion has no V3 release to compare with.
241
- The gate says so with the finding V2 uses when it cannot compare, rather than passing in silence — a
242
- release that was not checked against the last one is a different fact from one that was and was found safe.
243
- */
244
- if (releasedJson) {
245
- findings.push({
246
- code: 'released-unreadable',
247
- message: say('figure.gate.v3-released-not-compared.message', {}, 'Not compared with the last release — V3 figures have no comparison between releases yet.'),
248
- why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),
249
- blocking: false
250
- })
251
- }
255
+ if (releasedJson) findings.push(...v3ContractFindings(asset!, releasedJson, say))
252
256
  return { blocked: findings.some(finding => finding.blocking), findings }
253
257
  }
254
258
 
259
+ /**
260
+ * The last release against this one — what the drawings that already hold this type lean on (ADR-0063 decision 3).
261
+ *
262
+ * figure-model names the changes and whether each blocks (`v3ContractChanges`); the sentence is made here. When
263
+ * the last release cannot be compared — it was a V2 source, which is what every converted figure's last release
264
+ * is, or it no longer parses — the gate says so rather than passing in silence: a release that was not checked
265
+ * against the last one is a different fact from one that was checked and found safe.
266
+ */
267
+ function v3ContractFindings(asset: V3Asset, releasedJson: string, say: Say): GateFinding[] {
268
+ let before: V3Asset
269
+ try {
270
+ if (formatOf(releasedJson) === 'v2') {
271
+ return [
272
+ {
273
+ code: 'released-unreadable',
274
+ message: say('figure.gate.v3-released-was-v2.message', {}, 'The last release is a V2 figure, so this release was not compared with it.'),
275
+ why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),
276
+ blocking: false
277
+ }
278
+ ]
279
+ }
280
+ before = parseV3Asset(releasedJson)
281
+ } catch (e) {
282
+ return [
283
+ {
284
+ code: 'released-unreadable',
285
+ message: say('figure.gate.v3-released-not-compared.message', { reason: (e as Error).message }, `The last release could not be read, so this release was not compared with it: ${(e as Error).message}`),
286
+ why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),
287
+ blocking: false
288
+ }
289
+ ]
290
+ }
291
+ return v3ContractChanges(before, asset).map(change => ({
292
+ code: change.code,
293
+ ...v3ContractWording(change, say),
294
+ at: change.subject,
295
+ blocking: change.blocking
296
+ }))
297
+ }
298
+
299
+ /** One contract change in the words of the release panel. The English line is the fallback for a missing key. */
300
+ function v3ContractWording(change: V3ContractChange, say: Say): Pick<GateFinding, 'message' | 'why' | 'how'> {
301
+ const v = change.values
302
+ const key = (field: string) => `figure.gate.contract.${change.code}.${field}`
303
+ const gone = {
304
+ how: say('figure.gate.contract.gone.how', {}, 'Put it back as it was, or publish this as a new figure type so the placed ones keep the old one.')
305
+ }
306
+ switch (change.code) {
307
+ case 'placement-changed':
308
+ return {
309
+ message: say(key('message'), v, `Placement changed from ${v.before} to ${v.after}.`),
310
+ why: say(key('why'), {}, 'Placement sets the height of every placed instance. They all move with it.'),
311
+ ...gone
312
+ }
313
+ case 'size-inputs-gone':
314
+ return {
315
+ message: say(key('message'), v, 'The figure no longer takes its size from the place it is given.'),
316
+ why: say(key('why'), {}, 'Placed instances were sized on the board. After this release they all draw at the authored size.'),
317
+ ...gone
318
+ }
319
+ case 'size-inputs-added':
320
+ return {
321
+ message: say(key('message'), v, 'The figure now takes its size from the place it is given.'),
322
+ why: say(key('why'), {}, 'Placed instances now follow their own size on the board.')
323
+ }
324
+ case 'size-range-narrowed':
325
+ return {
326
+ message: say(key('message'), { ...v, input: change.subject ?? '' }, `The size range of ${change.subject} narrowed from ${v.before} to ${v.after}.`),
327
+ why: say(key('why'), {}, 'An instance placed at a size outside the new range will not draw. Which sizes are placed is not recorded.')
328
+ }
329
+ case 'authored-size-changed':
330
+ return {
331
+ message: say(key('message'), v, `The authored size changed from ${v.before} to ${v.after}.`),
332
+ why: say(key('why'), {}, 'Placed instances keep their own size. An instance with no depth of its own takes its depth from this.')
333
+ }
334
+ case 'state-input-gone':
335
+ return {
336
+ message: say(key('message'), v, `The control '${v.id}' is gone.`),
337
+ why: say(key('why'), {}, 'Placed instances and connectors give values under this name. After this release those values do nothing, and nothing on screen says why.'),
338
+ ...gone
339
+ }
340
+ case 'state-unit-changed':
341
+ return {
342
+ message: say(key('message'), v, `The unit of the control '${v.id}' changed from ${v.before} to ${v.after}.`),
343
+ why: say(key('why'), {}, 'Stored values keep their number. With another unit the same number means something else.'),
344
+ ...gone
345
+ }
346
+ case 'state-range-narrowed':
347
+ return {
348
+ message: say(key('message'), v, `The range of the control '${v.id}' narrowed from ${v.before} to ${v.after}.`),
349
+ why: say(key('why'), {}, 'A value outside the new range may already be stored on a placed instance.')
350
+ }
351
+ case 'state-input-added':
352
+ return {
353
+ message: say(key('message'), v, `The control '${v.id}' is new.`),
354
+ why: say(key('why'), {}, 'Placed instances that give no value stand at its default.')
355
+ }
356
+ case 'clip-gone':
357
+ return {
358
+ message: say(key('message'), v, `The clip '${v.clip}' is gone.`),
359
+ why: say(key('why'), {}, 'Placed instances play clips by name. After this release that one plays nothing.'),
360
+ ...gone
361
+ }
362
+ case 'clip-added':
363
+ return {
364
+ message: say(key('message'), v, `The clip '${v.clip}' is new.`),
365
+ why: say(key('why'), {}, 'It stays still until an instance plays it.')
366
+ }
367
+ case 'binding-gone':
368
+ return {
369
+ message: say(key('message'), v, `The place '${v.binding}' is gone.`),
370
+ why: say(key('why'), {}, 'What sits in it on a placed instance loses its place.'),
371
+ ...gone
372
+ }
373
+ case 'binding-role-changed':
374
+ return {
375
+ message: say(key('message'), v, `The place '${v.binding}' changed from ${v.before} to ${v.after}.`),
376
+ why: say(key('why'), {}, 'What sits in it was placed for the old role.'),
377
+ ...gone
378
+ }
379
+ case 'binding-capacity-reduced':
380
+ return {
381
+ message: say(key('message'), v, `The place '${v.binding}' holds ${v.after} instead of ${v.before}.`),
382
+ why: say(key('why'), {}, 'What sits in the places that went has nowhere to go.'),
383
+ ...gone
384
+ }
385
+ case 'binding-accepts-narrowed':
386
+ return {
387
+ message: say(key('message'), v, `The place '${v.binding}' no longer accepts ${v.types}.`),
388
+ why: say(key('why'), {}, 'What already sits there stays. The next one of that type is refused.')
389
+ }
390
+ case 'binding-added':
391
+ return {
392
+ message: say(key('message'), v, `The place '${v.binding}' is new.`),
393
+ why: say(key('why'), {}, 'Placed instances do not know it yet and stand as they are.')
394
+ }
395
+ case 'capability-gone':
396
+ return {
397
+ message: say(key('message'), v, `The capability '${v.name}' is gone.`),
398
+ why: say(key('why'), {}, 'Scenes attach behaviour by this name. Placed instances lose what it did.'),
399
+ ...gone
400
+ }
401
+ case 'capability-added':
402
+ return {
403
+ message: say(key('message'), v, `The capability '${v.name}' is new.`),
404
+ why: say(key('why'), {}, 'It adds what the figure can do. Placed instances stand as they are.')
405
+ }
406
+ }
407
+ }
408
+
255
409
  /**
256
410
  * What a consumer is handed for a released figure.
257
411
  *
@@ -11,6 +11,9 @@
11
11
  "figure.gate.parts-off-placement-face.how": "Move '{part}' {mm} mm on Y so the face meets the plane, or choose a different mounting face.",
12
12
  "figure.gate.parts-off-placement-face.message": "The mounting face of '{part}' is {mm} mm off the plane the figure stands on.",
13
13
  "figure.gate.parts-off-placement-face.why": "The figure is declared to stand on that face. If the face does not reach the plane, the figure floats on the board by that much, and whatever is placed on it floats too.",
14
+ "figure.gate.parts-split-on-resize.how": "Fasten {parts} to a face of the part they rest on, or press “fix here” under placement, so they stay put when the figure is resized.",
15
+ "figure.gate.parts-split-on-resize.message": "At {factor}x on {axes}, {parts} come away from the rest of the figure.",
16
+ "figure.gate.parts-split-on-resize.why": "A figure placed at another size shows those parts hanging in the air — to a person, it looks broken.",
14
17
  "figure.gate.placement-face-tilted.how": "Choose a mounting face parallel to the plane, or turn '{part}' upright.",
15
18
  "figure.gate.placement-face-tilted.message": "The mounting face of '{part}' is tilted {degrees}° from the plane the figure stands on.",
16
19
  "figure.gate.placement-face-tilted.why": "A tilted face cannot lie on the plane, so how the figure stands is not settled.",
@@ -21,5 +24,43 @@
21
24
  "figure.gate.type-is-placeholder.message": "'{type}' is a temporary type given when the figure was first saved.",
22
25
  "figure.gate.type-is-placeholder.why": "Boards call a figure by its type, and it cannot change once the figure is published.",
23
26
  "figure.gate.v3-released-not-compared.message": "Not compared with the last release. V3 figures have no comparison between releases yet.",
24
- "figure.gate.v3-released-not-compared.why": "Whether this release breaks the instances already placed has not been checked."
27
+ "figure.gate.v3-released-not-compared.why": "Whether this release breaks the instances already placed has not been checked.",
28
+ "figure.gate.v3-released-was-v2.message": "The last release is a V2 figure, so this release was not compared with it.",
29
+ "figure.gate.contract.gone.how": "Put it back as it was, or publish this as a new figure type so the placed ones keep the old one.",
30
+ "figure.gate.contract.placement-changed.message": "Placement changed from {before} to {after}.",
31
+ "figure.gate.contract.placement-changed.why": "Placement sets the height of every placed instance. They all move with it.",
32
+ "figure.gate.contract.size-inputs-gone.message": "The figure no longer takes its size from the place it is given.",
33
+ "figure.gate.contract.size-inputs-gone.why": "Placed instances were sized on the board. After this release they all draw at the authored size.",
34
+ "figure.gate.contract.size-inputs-added.message": "The figure now takes its size from the place it is given.",
35
+ "figure.gate.contract.size-inputs-added.why": "Placed instances now follow their own size on the board.",
36
+ "figure.gate.contract.size-range-narrowed.message": "The size range of {input} narrowed from {before} to {after}.",
37
+ "figure.gate.contract.size-range-narrowed.why": "An instance placed at a size outside the new range will not draw. Which sizes are placed is not recorded.",
38
+ "figure.gate.contract.authored-size-changed.message": "The authored size changed from {before} to {after}.",
39
+ "figure.gate.contract.authored-size-changed.why": "Placed instances keep their own size. An instance with no depth of its own takes its depth from this.",
40
+ "figure.gate.contract.state-input-gone.message": "The control '{id}' is gone.",
41
+ "figure.gate.contract.state-input-gone.why": "Placed instances and connectors give values under this name. After this release those values do nothing, and nothing on screen says why.",
42
+ "figure.gate.contract.state-unit-changed.message": "The unit of the control '{id}' changed from {before} to {after}.",
43
+ "figure.gate.contract.state-unit-changed.why": "Stored values keep their number. With another unit the same number means something else.",
44
+ "figure.gate.contract.state-range-narrowed.message": "The range of the control '{id}' narrowed from {before} to {after}.",
45
+ "figure.gate.contract.state-range-narrowed.why": "A value outside the new range may already be stored on a placed instance.",
46
+ "figure.gate.contract.state-input-added.message": "The control '{id}' is new.",
47
+ "figure.gate.contract.state-input-added.why": "Placed instances that give no value stand at its default.",
48
+ "figure.gate.contract.clip-gone.message": "The clip '{clip}' is gone.",
49
+ "figure.gate.contract.clip-gone.why": "Placed instances play clips by name. After this release that one plays nothing.",
50
+ "figure.gate.contract.clip-added.message": "The clip '{clip}' is new.",
51
+ "figure.gate.contract.clip-added.why": "It stays still until an instance plays it.",
52
+ "figure.gate.contract.binding-gone.message": "The place '{binding}' is gone.",
53
+ "figure.gate.contract.binding-gone.why": "What sits in it on a placed instance loses its place.",
54
+ "figure.gate.contract.binding-role-changed.message": "The place '{binding}' changed from {before} to {after}.",
55
+ "figure.gate.contract.binding-role-changed.why": "What sits in it was placed for the old role.",
56
+ "figure.gate.contract.binding-capacity-reduced.message": "The place '{binding}' holds {after} instead of {before}.",
57
+ "figure.gate.contract.binding-capacity-reduced.why": "What sits in the places that went has nowhere to go.",
58
+ "figure.gate.contract.binding-accepts-narrowed.message": "The place '{binding}' no longer accepts {types}.",
59
+ "figure.gate.contract.binding-accepts-narrowed.why": "What already sits there stays. The next one of that type is refused.",
60
+ "figure.gate.contract.binding-added.message": "The place '{binding}' is new.",
61
+ "figure.gate.contract.binding-added.why": "Placed instances do not know it yet and stand as they are.",
62
+ "figure.gate.contract.capability-gone.message": "The capability '{name}' is gone.",
63
+ "figure.gate.contract.capability-gone.why": "Scenes attach behaviour by this name. Placed instances lose what it did.",
64
+ "figure.gate.contract.capability-added.message": "The capability '{name}' is new.",
65
+ "figure.gate.contract.capability-added.why": "It adds what the figure can do. Placed instances stand as they are."
25
66
  }
@@ -11,6 +11,9 @@
11
11
  "figure.gate.parts-off-placement-face.how": "'{part}' 을 Y 로 {mm}mm 옮겨 그 면을 평면에 맞추거나, 장착면을 다른 면으로 다시 정하세요.",
12
12
  "figure.gate.parts-off-placement-face.message": "장착면으로 정한 '{part}' 부품의 면이 놓이는 평면에서 {mm}mm 떨어져 있습니다.",
13
13
  "figure.gate.parts-off-placement-face.why": "도형은 그 면으로 놓인다고 선언돼 있습니다. 닿지 않으면 도면에서 그만큼 떠 보이고, 자리에 놓인 물건도 함께 뜹니다.",
14
+ "figure.gate.parts-split-on-resize.how": "{parts} 를 올려 둔 부품의 면에 붙이거나, 부품의 「위치와 크기」에서 「여기에 고정」을 누르면 크기를 바꿔도 제자리에 있습니다.",
15
+ "figure.gate.parts-split-on-resize.message": "크기를 {axes} 로 {factor}배로 하면 {parts} 가 나머지에서 떨어집니다.",
16
+ "figure.gate.parts-split-on-resize.why": "다른 크기로 놓으면 그 부품이 공중에 떠 보입니다. 보는 사람에게는 부서진 도형입니다.",
14
17
  "figure.gate.placement-face-tilted.how": "평면과 나란한 면을 장착면으로 다시 정하거나, '{part}' 을 바로 세우세요.",
15
18
  "figure.gate.placement-face-tilted.message": "장착면으로 정한 '{part}' 부품의 면이 놓이는 평면에서 {degrees}° 기울어 있습니다.",
16
19
  "figure.gate.placement-face-tilted.why": "기운 면은 평면에 닿을 수 없어서, 도형이 어떤 자세로 놓이는지 정해지지 않습니다.",
@@ -21,5 +24,43 @@
21
24
  "figure.gate.type-is-placeholder.message": "'{type}' 은 처음 저장할 때 붙은 임시 타입입니다.",
22
25
  "figure.gate.type-is-placeholder.why": "보드는 이 타입으로 도형을 부르고, 발행한 뒤에는 바꿀 수 없습니다.",
23
26
  "figure.gate.v3-released-not-compared.message": "직전 발행판과 견주지 못했습니다. V3 도형은 발행 사이의 비교가 아직 없습니다.",
24
- "figure.gate.v3-released-not-compared.why": "이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았습니다."
27
+ "figure.gate.v3-released-not-compared.why": "이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았습니다.",
28
+ "figure.gate.v3-released-was-v2.message": "직전 발행판이 V2 도형이라 이번 발행과 견주지 않았습니다.",
29
+ "figure.gate.contract.gone.how": "원래대로 되돌리거나, 새 도형 타입으로 발행해서 놓인 것들이 옛 도형을 그대로 쓰게 하세요.",
30
+ "figure.gate.contract.placement-changed.message": "배치 기준이 {before} 에서 {after} 로 바뀌었습니다.",
31
+ "figure.gate.contract.placement-changed.why": "놓인 인스턴스의 높이를 배치 기준이 정합니다. 놓인 것이 모두 함께 옮겨 갑니다.",
32
+ "figure.gate.contract.size-inputs-gone.message": "도형이 더 이상 도면에서 받은 크기를 따르지 않습니다.",
33
+ "figure.gate.contract.size-inputs-gone.why": "놓인 인스턴스는 도면에서 크기를 맞춰 두었습니다. 이 발행 뒤에는 모두 저작한 크기로 그려집니다.",
34
+ "figure.gate.contract.size-inputs-added.message": "도형이 이제 도면에서 받은 크기를 따릅니다.",
35
+ "figure.gate.contract.size-inputs-added.why": "놓인 인스턴스가 이제 도면의 제 크기를 따릅니다.",
36
+ "figure.gate.contract.size-range-narrowed.message": "{input} 의 크기 범위가 {before} 에서 {after} 로 좁아졌습니다.",
37
+ "figure.gate.contract.size-range-narrowed.why": "새 범위 밖의 크기로 놓인 인스턴스는 그려지지 않습니다. 어떤 크기로 놓였는지는 기록되지 않습니다.",
38
+ "figure.gate.contract.authored-size-changed.message": "저작한 크기가 {before} 에서 {after} 로 바뀌었습니다.",
39
+ "figure.gate.contract.authored-size-changed.why": "놓인 인스턴스는 제 크기를 그대로 둡니다. 깊이를 따로 정하지 않은 인스턴스는 이 크기에서 깊이를 받습니다.",
40
+ "figure.gate.contract.state-input-gone.message": "조작값 '{id}' 가 없어졌습니다.",
41
+ "figure.gate.contract.state-input-gone.why": "놓인 인스턴스와 커넥터가 이 이름으로 값을 줍니다. 이 발행 뒤에는 그 값이 아무 일도 하지 않고, 화면에는 까닭이 나오지 않습니다.",
42
+ "figure.gate.contract.state-unit-changed.message": "조작값 '{id}' 의 단위가 {before} 에서 {after} 로 바뀌었습니다.",
43
+ "figure.gate.contract.state-unit-changed.why": "저장된 값은 수를 그대로 가집니다. 단위가 바뀌면 같은 수가 다른 뜻이 됩니다.",
44
+ "figure.gate.contract.state-range-narrowed.message": "조작값 '{id}' 의 범위가 {before} 에서 {after} 로 좁아졌습니다.",
45
+ "figure.gate.contract.state-range-narrowed.why": "놓인 인스턴스에 새 범위 밖의 값이 이미 저장돼 있을 수 있습니다.",
46
+ "figure.gate.contract.state-input-added.message": "조작값 '{id}' 가 새로 생겼습니다.",
47
+ "figure.gate.contract.state-input-added.why": "값을 주지 않은 인스턴스는 기본값에 섭니다.",
48
+ "figure.gate.contract.clip-gone.message": "클립 '{clip}' 이 없어졌습니다.",
49
+ "figure.gate.contract.clip-gone.why": "놓인 인스턴스는 이름으로 클립을 재생합니다. 이 발행 뒤에는 그 클립이 아무것도 재생하지 않습니다.",
50
+ "figure.gate.contract.clip-added.message": "클립 '{clip}' 이 새로 생겼습니다.",
51
+ "figure.gate.contract.clip-added.why": "인스턴스가 재생하기 전에는 멈춰 있습니다.",
52
+ "figure.gate.contract.binding-gone.message": "자리 '{binding}' 이 없어졌습니다.",
53
+ "figure.gate.contract.binding-gone.why": "놓인 인스턴스에서 그 자리에 든 것이 갈 곳을 잃습니다.",
54
+ "figure.gate.contract.binding-role-changed.message": "자리 '{binding}' 의 역할이 {before} 에서 {after} 로 바뀌었습니다.",
55
+ "figure.gate.contract.binding-role-changed.why": "그 자리에 든 것은 옛 역할에 맞춰 놓였습니다.",
56
+ "figure.gate.contract.binding-capacity-reduced.message": "자리 '{binding}' 이 {before} 개 대신 {after} 개를 받습니다.",
57
+ "figure.gate.contract.binding-capacity-reduced.why": "없어진 칸에 든 것이 갈 곳을 잃습니다.",
58
+ "figure.gate.contract.binding-accepts-narrowed.message": "자리 '{binding}' 이 {types} 를 더 받지 않습니다.",
59
+ "figure.gate.contract.binding-accepts-narrowed.why": "이미 든 것은 그대로 있고, 그 타입의 다음 것부터 거절됩니다.",
60
+ "figure.gate.contract.binding-added.message": "자리 '{binding}' 이 새로 생겼습니다.",
61
+ "figure.gate.contract.binding-added.why": "놓인 인스턴스는 아직 이 자리를 모르고 그대로 서 있습니다.",
62
+ "figure.gate.contract.capability-gone.message": "능력 '{name}' 이 없어졌습니다.",
63
+ "figure.gate.contract.capability-gone.why": "씬이 이 이름으로 동작을 붙입니다. 놓인 인스턴스가 그 동작을 잃습니다.",
64
+ "figure.gate.contract.capability-added.message": "능력 '{name}' 이 새로 생겼습니다.",
65
+ "figure.gate.contract.capability-added.why": "도형이 할 수 있는 일이 늘어납니다. 놓인 인스턴스는 그대로 서 있습니다."
25
66
  }