@things-factory/figure-ui 10.1.146 → 10.1.149

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 (209) hide show
  1. package/client/bootstrap.ts +10 -2
  2. package/client/graphql/index.ts +20 -0
  3. package/client/modeller/canvas.ts +6 -2
  4. package/client/modeller/carrier-declaration.ts +63 -0
  5. package/client/modeller/editor.ts +62 -27
  6. package/client/modeller/figure-carrier.ts +117 -0
  7. package/client/modeller/figure-cavity-drawing.ts +3 -9
  8. package/client/modeller/figure-first-save.ts +4 -7
  9. package/client/modeller/figure-history-panel.ts +2 -3
  10. package/client/modeller/figure-links.ts +46 -60
  11. package/client/modeller/figure-material-panel.ts +4 -14
  12. package/client/modeller/figure-mesh-choice.ts +68 -0
  13. package/client/modeller/figure-parts.ts +23 -1
  14. package/client/modeller/figure-preview.ts +2 -2
  15. package/client/modeller/figure-properties.ts +5 -3
  16. package/client/modeller/figure-section-editor.ts +12 -33
  17. package/client/modeller/figure-section-panel.ts +6 -7
  18. package/client/modeller/figure-slot-layout-panel.ts +208 -0
  19. package/client/modeller/figure-verdict.ts +21 -31
  20. package/client/modeller/first-save.ts +23 -0
  21. package/client/modeller/links.ts +52 -1
  22. package/client/modeller/material-edit.ts +38 -0
  23. package/client/modeller/maturity.ts +28 -0
  24. package/client/modeller/mesh-import.ts +77 -0
  25. package/client/modeller/modeller-route.ts +9 -0
  26. package/client/modeller/mounting-part.ts +15 -0
  27. package/client/modeller/newest-answer.ts +21 -0
  28. package/client/modeller/preview-model.ts +5 -0
  29. package/client/modeller/refusals.ts +168 -82
  30. package/client/modeller/section-controls.ts +25 -1
  31. package/client/modeller/section-drawing.ts +43 -1
  32. package/client/modeller/slot-layout-host.ts +140 -0
  33. package/client/pages/figure-modeller-page.ts +6 -5
  34. package/client/viewparts/detail-route.ts +7 -0
  35. package/client/viewparts/figure-detail-dock.ts +1 -3
  36. package/dist-client/bootstrap.d.ts +1 -1
  37. package/dist-client/bootstrap.js +9 -2
  38. package/dist-client/bootstrap.js.map +1 -1
  39. package/dist-client/graphql/index.d.ts +5 -0
  40. package/dist-client/graphql/index.js +19 -0
  41. package/dist-client/graphql/index.js.map +1 -1
  42. package/dist-client/modeller/canvas.d.ts +2 -0
  43. package/dist-client/modeller/canvas.js +10 -2
  44. package/dist-client/modeller/canvas.js.map +1 -1
  45. package/dist-client/modeller/carrier-declaration.d.ts +31 -0
  46. package/dist-client/modeller/carrier-declaration.js +53 -0
  47. package/dist-client/modeller/carrier-declaration.js.map +1 -0
  48. package/dist-client/modeller/editor.d.ts +13 -0
  49. package/dist-client/modeller/editor.js +62 -27
  50. package/dist-client/modeller/editor.js.map +1 -1
  51. package/dist-client/modeller/figure-carrier.d.ts +26 -0
  52. package/dist-client/modeller/figure-carrier.js +123 -0
  53. package/dist-client/modeller/figure-carrier.js.map +1 -0
  54. package/dist-client/modeller/figure-cavity-drawing.js +3 -11
  55. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  56. package/dist-client/modeller/figure-first-save.d.ts +4 -0
  57. package/dist-client/modeller/figure-first-save.js +5 -8
  58. package/dist-client/modeller/figure-first-save.js.map +1 -1
  59. package/dist-client/modeller/figure-history-panel.js +2 -4
  60. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  61. package/dist-client/modeller/figure-links.d.ts +4 -1
  62. package/dist-client/modeller/figure-links.js +47 -57
  63. package/dist-client/modeller/figure-links.js.map +1 -1
  64. package/dist-client/modeller/figure-material-panel.d.ts +0 -1
  65. package/dist-client/modeller/figure-material-panel.js +4 -14
  66. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  67. package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
  68. package/dist-client/modeller/figure-mesh-choice.js +75 -0
  69. package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
  70. package/dist-client/modeller/figure-parts.d.ts +1 -1
  71. package/dist-client/modeller/figure-parts.js +22 -0
  72. package/dist-client/modeller/figure-parts.js.map +1 -1
  73. package/dist-client/modeller/figure-preview.js +2 -2
  74. package/dist-client/modeller/figure-preview.js.map +1 -1
  75. package/dist-client/modeller/figure-properties.d.ts +2 -1
  76. package/dist-client/modeller/figure-properties.js +5 -3
  77. package/dist-client/modeller/figure-properties.js.map +1 -1
  78. package/dist-client/modeller/figure-section-editor.d.ts +2 -2
  79. package/dist-client/modeller/figure-section-editor.js +12 -33
  80. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  81. package/dist-client/modeller/figure-section-panel.js +6 -7
  82. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  83. package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
  84. package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
  85. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
  86. package/dist-client/modeller/figure-verdict.d.ts +1 -1
  87. package/dist-client/modeller/figure-verdict.js +19 -37
  88. package/dist-client/modeller/figure-verdict.js.map +1 -1
  89. package/dist-client/modeller/first-save.d.ts +10 -0
  90. package/dist-client/modeller/first-save.js +16 -0
  91. package/dist-client/modeller/first-save.js.map +1 -1
  92. package/dist-client/modeller/links.d.ts +26 -1
  93. package/dist-client/modeller/links.js +43 -0
  94. package/dist-client/modeller/links.js.map +1 -1
  95. package/dist-client/modeller/material-edit.d.ts +11 -0
  96. package/dist-client/modeller/material-edit.js +28 -0
  97. package/dist-client/modeller/material-edit.js.map +1 -0
  98. package/dist-client/modeller/maturity.d.ts +15 -0
  99. package/dist-client/modeller/maturity.js +23 -0
  100. package/dist-client/modeller/maturity.js.map +1 -1
  101. package/dist-client/modeller/mesh-import.d.ts +47 -0
  102. package/dist-client/modeller/mesh-import.js +47 -0
  103. package/dist-client/modeller/mesh-import.js.map +1 -0
  104. package/dist-client/modeller/modeller-route.d.ts +5 -0
  105. package/dist-client/modeller/modeller-route.js +9 -0
  106. package/dist-client/modeller/modeller-route.js.map +1 -1
  107. package/dist-client/modeller/mounting-part.d.ts +14 -0
  108. package/dist-client/modeller/mounting-part.js +10 -0
  109. package/dist-client/modeller/mounting-part.js.map +1 -0
  110. package/dist-client/modeller/newest-answer.d.ts +12 -0
  111. package/dist-client/modeller/newest-answer.js +21 -0
  112. package/dist-client/modeller/newest-answer.js.map +1 -0
  113. package/dist-client/modeller/preview-model.d.ts +2 -0
  114. package/dist-client/modeller/preview-model.js +4 -0
  115. package/dist-client/modeller/preview-model.js.map +1 -1
  116. package/dist-client/modeller/refusals.d.ts +26 -5
  117. package/dist-client/modeller/refusals.js +184 -86
  118. package/dist-client/modeller/refusals.js.map +1 -1
  119. package/dist-client/modeller/section-controls.d.ts +17 -1
  120. package/dist-client/modeller/section-controls.js +18 -0
  121. package/dist-client/modeller/section-controls.js.map +1 -1
  122. package/dist-client/modeller/section-drawing.d.ts +24 -1
  123. package/dist-client/modeller/section-drawing.js +42 -0
  124. package/dist-client/modeller/section-drawing.js.map +1 -1
  125. package/dist-client/modeller/slot-layout-host.d.ts +81 -0
  126. package/dist-client/modeller/slot-layout-host.js +105 -0
  127. package/dist-client/modeller/slot-layout-host.js.map +1 -0
  128. package/dist-client/pages/figure-modeller-page.js +5 -4
  129. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  130. package/dist-client/tsconfig.tsbuildinfo +1 -1
  131. package/dist-client/viewparts/detail-route.d.ts +5 -0
  132. package/dist-client/viewparts/detail-route.js +8 -0
  133. package/dist-client/viewparts/detail-route.js.map +1 -0
  134. package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
  135. package/dist-client/viewparts/figure-detail-dock.js +1 -3
  136. package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
  137. package/package.json +9 -8
  138. package/test/align.test.ts +0 -9
  139. package/test/browser/assistant-stand-in.js +32 -0
  140. package/test/browser/build.mjs +6 -1
  141. package/test/browser/editing-page.js +224 -0
  142. package/test/browser/editing.check.mjs +550 -0
  143. package/test/browser/links-page.js +113 -0
  144. package/test/browser/links.check.mjs +568 -0
  145. package/test/browser/modeller.check.mjs +21 -0
  146. package/test/browser/parts-page.js +120 -0
  147. package/test/browser/parts.check.mjs +328 -0
  148. package/test/browser/places-mesh-page.js +36 -0
  149. package/test/browser/places-mesh.check.mjs +239 -0
  150. package/test/browser/product-page.check.mjs +682 -0
  151. package/test/browser/product-page.js +276 -0
  152. package/test/browser/screen-page.js +112 -0
  153. package/test/browser/screen-product-page.js +62 -0
  154. package/test/browser/screen.check.mjs +1003 -0
  155. package/test/browser/section-page.js +256 -0
  156. package/test/browser/section.check.mjs +719 -0
  157. package/test/browser/serve.mjs +20 -12
  158. package/test/carrier-declaration.test.ts +69 -0
  159. package/test/cavity-drawing.test.ts +2 -18
  160. package/test/clip-ai-proposal.test.ts +1 -5
  161. package/test/clip-consent.test.ts +0 -5
  162. package/test/clips.test.ts +0 -7
  163. package/test/command-preview.test.ts +0 -68
  164. package/test/controls.test.ts +0 -9
  165. package/test/detail-dock.test.ts +6 -25
  166. package/test/face-mapping.test.ts +1 -20
  167. package/test/fasten-drawing.test.ts +0 -23
  168. package/test/fields.test.ts +6 -33
  169. package/test/figure-assistant-surface.test.ts +32 -38
  170. package/test/first-minute.test.ts +3 -55
  171. package/test/first-save-suggest.test.ts +11 -8
  172. package/test/first-save.test.ts +4 -26
  173. package/test/gizmo.test.ts +0 -25
  174. package/test/joint-fan.test.ts +0 -10
  175. package/test/keys.test.ts +0 -7
  176. package/test/layout.test.ts +4 -73
  177. package/test/links.test.ts +48 -67
  178. package/test/live.test.ts +0 -34
  179. package/test/material.test.ts +17 -26
  180. package/test/maturity.test.ts +13 -16
  181. package/test/mesh-import.test.ts +76 -0
  182. package/test/modeller-route.test.ts +3 -19
  183. package/test/package-download.test.ts +6 -7
  184. package/test/parts-wiring.test.ts +9 -51
  185. package/test/preview.test.ts +5 -46
  186. package/test/properties.test.ts +11 -26
  187. package/test/proposal-record.test.ts +2 -19
  188. package/test/recipes.test.ts +0 -15
  189. package/test/refusals.test.ts +257 -151
  190. package/test/release-entry.test.ts +1 -9
  191. package/test/release-panel.test.ts +1 -22
  192. package/test/resize-with-placement.test.ts +0 -32
  193. package/test/revert-choice.test.ts +1 -22
  194. package/test/revert-keeping.test.ts +1 -12
  195. package/test/save-landed.test.ts +16 -14
  196. package/test/scene-canvas.test.ts +2 -13
  197. package/test/section-editor.test.ts +30 -83
  198. package/test/section-panel.test.ts +22 -17
  199. package/test/slot-layout-host.test.ts +117 -0
  200. package/test/theme.test.ts +5 -9
  201. package/test/verdict.test.ts +44 -41
  202. package/test/view.test.ts +5 -12
  203. package/translations/en.json +61 -1
  204. package/translations/es.json +61 -1
  205. package/translations/ja.json +61 -1
  206. package/translations/ko.json +61 -1
  207. package/translations/ms.json +61 -1
  208. package/translations/zh.json +61 -1
  209. package/test/thumbnail.test.ts +0 -29
@@ -0,0 +1,117 @@
1
+ /*
2
+ * The Figure modeller's slot layout (ADR-0118): the shared editor hosted, the modeller owning the anchors -- a part's top
3
+ * or bottom -- and sending the whole layout as one command; refusals said from their details (ADR-0127); the unfolded
4
+ * slots drawn where they fall. No override of any kind: the figure's layout is the only one (ADR-0118 decision 8).
5
+ */
6
+ import { test } from 'node:test'
7
+ import assert from 'node:assert/strict'
8
+ import { readFileSync } from 'node:fs'
9
+ import { fileURLToPath } from 'node:url'
10
+ import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset, type SlotLayout } from '@hatiolab/figure-model'
11
+
12
+ import {
13
+ anchorFacesOf,
14
+ anchorIdFor,
15
+ figureSlotIssues,
16
+ slotLayoutCommandOf,
17
+ slotLayoutFormOf,
18
+ slotMarksOf,
19
+ slotPresetsOf,
20
+ SLOT_MARK
21
+ } from '../client/modeller/slot-layout-host.js'
22
+ import { editRefusalOf, slotLayoutRefusalOf } from '../client/modeller/refusals.js'
23
+
24
+ const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
25
+ const say = (key: string, params: Record<string, string>) => `${key}${JSON.stringify(params)}`
26
+
27
+ /* A stacker's carriage: a 1200 × 50 × 900 fork, its top at y = 50. */
28
+ const stacker = () => addFigureBox(createFigureAsset('Stacker'), { id: 'fork', size: { x: 1200, y: 50, z: 900 }, position: { x: 0, y: 25, z: 0 } })
29
+ const twoAcross = (accepts: string[] = ['PALLET']): SlotLayout => ({
30
+ tiers: [{ tierNo: 1, anchorId: 'fork', mount: 'ON', position: { x: 0, y: 0, z: 0 }, rotation: 0, layout: { pattern: 'ROW', count: 2, pitch: 600, axis: 'X', slot: { allowed: { width: 500, depth: 800, height: 1000 }, accepts } } }]
31
+ })
32
+
33
+ test("no layout yet: a form on the selected part's top, and the equipment presets laid out on its anchors", () => {
34
+ const asset = stacker()
35
+ const { form, layout } = slotLayoutFormOf(asset, 'fork')
36
+ assert.deepEqual(form, { label: '', role: 'seat', anchors: [{ id: 'fork', part: 'fork', face: 'top' }] })
37
+ assert.equal(layout, undefined)
38
+ const faces = anchorFacesOf(form.anchors, compileFigureAsset(asset).evaluate().geometry as never)
39
+ assert.deepEqual(faces, { fork: { width: 1200, depth: 900 } })
40
+ const say = (key: string) => key
41
+ const presets = slotPresetsOf(form.anchors, say)
42
+ assert.deepEqual(presets.map(p => p.key)[0], 'single', 'a single slot first: where an author starts')
43
+ assert.equal(presets.length, 9)
44
+ const size = { ...faces.fork!, slotHeight: 1000 }
45
+ const single = presets[0]!.build(size, {})
46
+ assert.deepEqual(single.tiers[0]!, { tierNo: 1, anchorId: 'fork', mount: 'ON', position: { x: 0, y: 0, z: 0 }, rotation: 0, layout: { pattern: 'SINGLE', slot: { allowed: { width: 1200, depth: 900, height: 1000 } } } })
47
+ assert.deepEqual(presets.find(p => p.key === 'floorBlock')!.params, [
48
+ { key: 'rows', label: 'figure.label.slot-preset-param-rows', integer: true },
49
+ { key: 'cols', label: 'figure.label.slot-preset-param-cols', integer: true },
50
+ { key: 'stackLimit', label: 'figure.label.slot-preset-param-stackLimit', integer: true }
51
+ ])
52
+ /* Two anchors: the twin fork stands one tier on each. A bottom anchor hangs its single slot. */
53
+ const twin = slotPresetsOf([{ id: 'left', part: 'a', face: 'top' }, { id: 'right', part: 'b', face: 'top' }], say)
54
+ assert.deepEqual(twin.find(p => p.key === 'craneTwinFork')!.build(size, {}).tiers.map(t => t.anchorId), ['left', 'right'])
55
+ assert.equal(slotPresetsOf([{ id: 'under', part: 'fork', face: 'bottom' }], say)[0]!.build(size, {}).tiers[0]!.mount, 'HANG')
56
+ })
57
+
58
+ test('the editor is told what figure-model asks beyond its own checks: every slot names a type', () => {
59
+ assert.deepEqual(figureSlotIssues(twoAcross()), [])
60
+ assert.deepEqual(figureSlotIssues(twoAcross([])), [{ code: 'ACCEPTS_REQUIRED', path: 'tiers[0].layout.slot.accepts' }])
61
+ const custom: SlotLayout = { tiers: [{ ...twoAcross().tiers[0]!, layout: { pattern: 'CUSTOM', slots: [{ slotNo: 1, offset: { x: 0, z: 0 }, allowed: { width: 1, depth: 1, height: 1 } }] } }] }
62
+ assert.deepEqual(figureSlotIssues(custom), [{ code: 'ACCEPTS_REQUIRED', path: 'tiers[0].layout.slots[0].accepts' }])
63
+ })
64
+
65
+ test('applied, it is one command that figure-model takes, and the figure reads back as the form it came from', () => {
66
+ const form = { label: ' Forks ', role: 'seat' as const, anchors: [{ id: 'fork', part: 'fork', face: 'top' as const }] }
67
+ const command = slotLayoutCommandOf(form, twoAcross(), 'slot layout')
68
+ assert.equal(command.kind, 'set-slot-layout')
69
+ assert.equal(command.label, 'Forks')
70
+ const asset = applyFigureAuthoring(stacker(), command as never)
71
+ assert.deepEqual(slotLayoutFormOf(asset, 'fork'), { form: { ...form, label: 'Forks' }, layout: twoAcross() })
72
+ assert.equal(slotLayoutCommandOf({ ...form, label: ' ' }, twoAcross(), 'slot layout').label, 'slot layout')
73
+ assert.equal(anchorIdFor('fork', ['fork', 'fork_2']), 'fork_3')
74
+ })
75
+
76
+ test('the unfolded slots are drawn as boxes of what they allow, on the fork, never picked', () => {
77
+ const asset = applyFigureAuthoring(stacker(), slotLayoutCommandOf({ label: 'Forks', role: 'seat', anchors: [{ id: 'fork', part: 'fork', face: 'top' }] }, twoAcross(), '') as never)
78
+ const evaluation = compileFigureAsset(asset).evaluate()
79
+ const marks = slotMarksOf(evaluation.bindings as never, twoAcross())
80
+ assert.deepEqual(marks.map(m => m.id), [`${SLOT_MARK}:t1_s1`, `${SLOT_MARK}:t1_s2`])
81
+ const [first, second] = marks as any[]
82
+ assert.deepEqual(first.shape.dimensions, { width: 500, height: 1000, depth: 800 })
83
+ assert.equal(first.shape.extrusion.start, 0)
84
+ assert.equal(first.material.token, SLOT_MARK)
85
+ /* On the fork's top, 600 apart along X. */
86
+ assert.equal(Math.round(first.pose.t[1]), 50)
87
+ assert.equal(Math.round(second.pose.t[0] - first.pose.t[0]), 600)
88
+ /* A hung tier hangs its boxes under the face. */
89
+ const hung = slotMarksOf(evaluation.bindings as never, { tiers: [{ ...twoAcross().tiers[0]!, mount: 'HANG' }] }) as any[]
90
+ assert.equal(hung[0].shape.extrusion.end, 0)
91
+ assert.equal(hung[0].shape.extrusion.start, -1000)
92
+ assert.deepEqual(slotMarksOf(evaluation.bindings as never, undefined), [])
93
+ })
94
+
95
+ test('refusals are said from their details, never the kernel\'s sentence', () => {
96
+ const refusal = (details: object) => Object.assign(new Error('KERNEL ENGLISH'), { code: 'X', details })
97
+ assert.equal(slotLayoutRefusalOf(refusal({ reason: 'slot-anchor-not-flat', anchor: 'a', part: 'fork', face: 'front' }), say), 'figure.text.slot-anchor-not-flat{"anchor":"a","part":"fork"}')
98
+ assert.equal(slotLayoutRefusalOf(refusal({ reason: 'slot-accepts-empty', slot: 't1_s1' }), say), 'figure.text.slot-accepts-empty{"slot":"t1_s1"}')
99
+ assert.equal(slotLayoutRefusalOf(refusal({ reason: 'slot-layout-issues', issues: [{ code: 'POSITIVE_NUMBER_REQUIRED', path: 'tiers[0].layout.pitch' }] }), say), 'figure.text.slot-layout-issues{"path":"tiers[0].layout.pitch"}')
100
+ assert.equal(slotLayoutRefusalOf(refusal({ reason: 'name-taken', name: 'x' }), say), null)
101
+ /* What the editor shows for any refusal: the slot's words, never the kernel's sentence. */
102
+ assert.equal(editRefusalOf(Object.assign(refusal({ reason: 'slot-id-taken', slot: 't1_s1' }), { code: 'SLOT_LAYOUT' }), say), 'figure.text.slot-id-taken{"slot":"t1_s1"}')
103
+ })
104
+
105
+ test('the slot layout panel starts from presets that keep where they started as a record', () => {
106
+ const [single] = slotPresetsOf([{ id: 'fork', part: 'fork', face: 'top' }], k => k)
107
+ assert.deepEqual(single!.build({ width: 1200, depth: 900, slotHeight: 1000 }, {}).preset, { key: 'single', size: { width: 1200, depth: 900, slotHeight: 1000 }, params: {} })
108
+ })
109
+
110
+ /* The screen itself -- the places choice, the shared editor, Apply, the outlines -- is driven in browser/places-mesh.check.mjs. */
111
+ test('the places words are in every language', () => {
112
+ for (const lang of ['en', 'es', 'ja', 'ko', 'ms', 'zh']) {
113
+ const t = JSON.parse(read(`../translations/${lang}.json`))
114
+ for (const key of ['figure.label.places-one', 'figure.label.places-by-rule', 'figure.text.slot-accepts-required', 'figure.text.slot-anchor-not-flat', 'figure.text.slot-id-taken', 'figure.button.remove-slot-layout'])
115
+ assert.ok(t[key], `${lang} ${key}`)
116
+ }
117
+ })
@@ -10,8 +10,9 @@ import assert from 'node:assert/strict'
10
10
  import { readFileSync } from 'node:fs'
11
11
  import { fileURLToPath } from 'node:url'
12
12
 
13
+ import { DEFAULT_VIEW, WORKBENCH } from '../client/modeller/figure-view.ts'
14
+
13
15
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
14
- const canvas = read('../client/modeller/canvas.ts')
15
16
  const editor = read('../client/modeller/editor.ts')
16
17
 
17
18
  /** Hex colours that are not a `var(--token, #fallback)` fallback. */
@@ -25,9 +26,8 @@ test('편집기에 박힌 색이 없다 — 토큰의 대체값만 남는다', (
25
26
  })
26
27
 
27
28
  test('캔버스 바탕은 작업대 회색 — 판의 하늘색이라 환경 탭에서 바꾼다', () => {
28
- assert.ok(canvas.includes('background-color: ${unsafeCSS(WORKBENCH)}'), '씬이 서기 전 · 2D 쪽도 같은 색')
29
- assert.ok(canvas.includes("sky: 'color'") && canvas.includes('...sceneLook(this.view ?? DEFAULT_VIEW)'), '3D 바탕은 보기 설정의 skyColor')
30
- assert.match(read('../client/modeller/figure-view.ts'), /skyColor: WORKBENCH/)
29
+ /* The canvas painted in it before the scene stands, and the scene's sky following the tab, are the browser check's (test/browser/screen.check.mjs). */
30
+ assert.equal(DEFAULT_VIEW.skyColor, WORKBENCH, '3D 바탕의 처음 값도 작업대 회색')
31
31
  })
32
32
 
33
33
  test('눌린 단추는 셸이 두 테마 대비를 맞춘 칸을 쓴다 — 오류는 앱의 알림이 그린다 (2026-09-27)', () => {
@@ -35,21 +35,17 @@ test('눌린 단추는 셸이 두 테마 대비를 맞춘 칸을 쓴다 — 오
35
35
  assert.match(pressed, /--md-sys-color-secondary-container\)/)
36
36
  assert.match(pressed, /--md-sys-color-on-secondary-container\)/)
37
37
  assert.match(pressed, /border-color: var\(--md-sys-color-primary\)/)
38
- assert.equal(editor.includes('.error {'), false, '모델러가 따로 칠하는 오류 칸이 없다')
39
38
  })
40
39
 
41
40
  test('편집기는 창이 아니라 자기 폭으로 접는다 — 레일이 폭을 먼저 가져간다', () => {
42
41
  /* The widths - 200 | 1fr | 340, 160 | 300 under 860px, one column with the canvas first under 560px - are
43
42
  ox-three-pane's, measured in @operato/layout's own test. The editor must not write the grid again. */
44
- assert.match(editor, /container-type: inline-size/)
45
43
  assert.doesNotMatch(editor, /@media \(max-width/, '창 폭 기준이 남아 있다 — 레일을 펼치면 편집기가 넘친다')
46
- assert.match(editor, /<ox-three-pane>/, '세 칸은 공용 틀')
47
44
  assert.doesNotMatch(editor, /grid-template-columns/, '편집기가 격자를 다시 쓰지 않는다')
48
45
  })
49
46
 
50
47
  test('양쪽 칸은 띠에서 접는다 — 접으면 캔버스가 그 폭을 갖고, 오른쪽 칸은 무엇도 가리지 않는다', () => {
51
- assert.ok(editor.includes('.pane=${this.panes}'), '띠가 그 틀을 접는다')
52
- assert.match(editor, /querySelector<OxThreePane>\('ox-three-pane'\)/, '띠에 건네는 틀은 편집기의 세 칸')
48
+ /* The bar folding the editor's own frame, and the canvas taking the width, are the browser check's (test/browser/screen.check.mjs). */
53
49
  const aside = editor.slice(editor.indexOf(' aside {'), editor.indexOf('}', editor.indexOf(' aside {')))
54
50
  assert.doesNotMatch(aside, /z-index/)
55
51
  assert.doesNotMatch(aside, /border-left/, '칸 경계선은 틀이 긋는다 — 두 줄이 되지 않게')
@@ -5,7 +5,7 @@
5
5
  * 까닭을 저장한 뒤 발행 패널에서야 알았다. 판정 탭이 편집 중인 원본으로 둘을 답한다.
6
6
  *
7
7
  * 세 축을 본다: 개수가 목록 카드와 같은 셈인가, 판정이 발행 패널과 같은 문 · 같은 문장인가, 화면이 탭과 조치를
8
- * 실제로 잇는가.
8
+ * 실제로 잇는가. 화면에 서는 쪽(탭 · 조치 · 서버 문)은 브라우저 검사(`test/browser/screen.check.mjs`)가 본다.
9
9
  */
10
10
  import { test } from 'node:test'
11
11
  import assert from 'node:assert/strict'
@@ -14,12 +14,13 @@ import { fileURLToPath } from 'node:url'
14
14
 
15
15
  import { addFigureBox, createFigureAsset, editFigurePart, figureCostOf } from '@hatiolab/figure-model'
16
16
 
17
+ import { budgetOf, figureReportOf, verdictMeasureOf } from '../client/modeller/maturity.ts'
18
+ import { newestAnswer, type FigureOutcome } from '../client/modeller/newest-answer.ts'
17
19
  import { capacityFrom, SCENE_BUDGET } from '../client/modeller/scene-budget.ts'
20
+ import { moving } from './joints.ts'
18
21
 
19
22
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
20
23
  const verdict = read('../client/modeller/figure-verdict.ts')
21
- const page = read('../client/pages/figure-modeller-page.ts')
22
- const graphql = read('../client/graphql/index.ts')
23
24
 
24
25
  test('개수는 목록 카드와 같은 셈이다 — 원가를 같은 capacityFrom 에 넣는다', () => {
25
26
  let asset = createFigureAsset('Probe')
@@ -31,43 +32,41 @@ test('개수는 목록 카드와 같은 셈이다 — 원가를 같은 capacityF
31
32
 
32
33
  assert.equal(cost.groups, 2, '전제: 그려지는 부품 둘이 묶음 둘')
33
34
  assert.equal(capacityFrom(cost.groups, cost.triangles)?.instances, Math.floor(SCENE_BUDGET.groupsOnScreen / 2))
34
- const maturity = read('../client/modeller/maturity.ts')
35
- assert.ok(/capacityFrom\(cost\.groups, cost\.triangles\)/.test(maturity), '판정 탭이 카드와 같은 함수로 센다')
36
- assert.ok(/report = asset \? figureReportOf\(asset\) : undefined/.test(verdict), '탭은 그 셈을 부르고 보여 줄 뿐이다')
37
- assert.ok(/instances: report\?\.capacity\.instances/.test(verdict), '탭 이름표의 개수도 같은 셈에서 온다')
35
+ const measured = verdictMeasureOf(asset)
36
+ assert.deepEqual(measured.report, figureReportOf(asset), '탭은 그 셈을 부르고 보여 줄 뿐이다')
37
+ assert.equal(measured.told.instances, capacityFrom(cost.groups, cost.triangles)!.instances, '탭 이름표의 개수도 같은 셈에서 온다')
38
+ assert.deepEqual(measured.told.budget, budgetOf(figureReportOf(asset)), '부품 목록 머리의 줄도 같은 보고서에서 온다')
39
+ assert.deepEqual(verdictMeasureOf(undefined), { report: undefined, bodies: 0, told: { instances: undefined, budget: undefined } }, '연 도형이 없으면 잴 것도 알릴 개수도 없다')
38
40
  })
39
41
 
40
42
  test('판정은 발행 패널과 같은 서버 문에서 온다 — 화면이 따로 판정하지 않는다', () => {
41
- assert.ok(verdict.includes('inspectFigureDraft(source, this.figureId)'), '편집 중인 원본을 보낸다')
42
43
  assert.ok(!/inspectFigureAsset/.test(verdict), '화면이 제 판정을 가지면 발행 패널과 다른 말을 한다')
43
-
44
- const fields = (name: string) => {
45
- const start = graphql.indexOf(`${name}(`, graphql.indexOf(`query ${name[0].toUpperCase()}${name.slice(1)}`))
46
- return graphql.slice(start, graphql.indexOf('`', start)).replace(/\s+/g, ' ').replace(/^[^{]*/, '')
47
- }
48
- assert.equal(fields('inspectFigureDraft'), fields('inspectFigure'), '두 질의가 같은 칸을 받는다 — 한쪽만 why·how 를 잃지 않게')
49
44
  })
50
45
 
51
- test('늦게 온 옛 답이 새 답을 덮지 않는다', () => {
52
- const guards = verdict.match(/question !== this\.asked/g) ?? []
53
- assert.ok(guards.length >= 2, '성공과 실패 둘 다 가장 새 물음의 답인지 본다')
54
- })
55
-
56
- test('판정 탭은 발행 패널과 같은 두 판정에 「부품에 맞추기」를 낸다', () => {
57
- const panel = read('../client/modeller/figure-history-panel.ts')
58
- const codes = (text: string) => [...text.matchAll(/'(part-outside-occupancy|occupancy-undeclared)'/g)].map(m => m[1]).sort()
59
- assert.deepEqual(codes(verdict), codes(panel.slice(panel.indexOf('private offersFit'))))
60
- assert.ok(/<figure-verdict[\s\S]*?@fit-base-to-parts=\$\{this\.fitToParts\}/.test(page), '누르면 공간 선언으로 이어진다')
61
- })
62
-
63
- test('판정과 이력은 편집기의 탭에 서고, 감추되 떼지 않는다', () => {
64
- const editor = read('../client/modeller/editor.ts')
65
- assert.ok(/<figure-verdict[\s\S]*?\.revision=\$\{this\.session\.revisionNow\(\)\}/.test(page), '편집 중인 원본을 넘긴다')
66
- assert.ok(/<figure-verdict[\s\S]*?\.figureType=/.test(page), '타입을 바꾸면 다시 판정한다')
67
- assert.ok(/<figure-verdict\s+slot="verdict"/.test(page) && /<figure-history-panel\s+slot="history"/.test(page))
68
- assert.ok(editor.includes(`<div panel flush ?off=\${tab !== 'verdict'}><slot name="verdict"></slot></div>`), '고르지 않은 탭은 감춘다 — 판정은 마지막 답을 지킨다')
69
- assert.ok(/@capacity-changed=/.test(page) && page.includes('.instances=${this.instances}'), '개수를 편집기에 넘긴다')
70
- assert.ok(editor.includes("this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'), this.instances)"), '개수가 탭 이름표에 남는다')
46
+ test('늦게 온 옛 답이 새 답을 덮지 않는다', async () => {
47
+ /* A question whose answer comes when the test says. */
48
+ type Held = { promise: Promise<string>; ok: (v: string) => void; no: (e: Error) => void }
49
+ const held = (): Held => {
50
+ const hand = {} as Held
51
+ hand.promise = new Promise<string>((ok, no) => Object.assign(hand, { ok, no }))
52
+ return hand
53
+ }
54
+ /* Two questions out at once, the older asked first; `settle` answers them, the newer first. */
55
+ const twice = async (settle: (older: Held, newer: Held) => void) => {
56
+ const ask = newestAnswer<string>()
57
+ const older = held()
58
+ const newer = held()
59
+ const landed: FigureOutcome<string>[] = []
60
+ const asked = [ask(() => older.promise, o => landed.push(o)), ask(() => newer.promise, o => landed.push(o))]
61
+ settle(older, newer)
62
+ await Promise.all(asked)
63
+ return landed
64
+ }
65
+ assert.deepEqual(await twice((older, newer) => (newer.ok('new'), older.no(Error('old')))), [{ answer: 'new' }], '늦게 온 옛 실패가 새 답을 지우지 않는다')
66
+ const failed = await twice((older, newer) => (newer.no(Error('down')), older.ok('old')))
67
+ assert.equal(failed.length, 1, '늦게 온 옛 답이 새 실패를 덮지 않는다')
68
+ assert.equal((failed[0] as { failure: Error }).failure.message, 'down')
69
+ assert.deepEqual(await twice((older, newer) => (older.ok('old'), newer.ok('new'))), [{ answer: 'new' }], '차례대로 와도 새 답 하나만')
71
70
  })
72
71
 
73
72
  test('판정 탭의 말은 다섯 말 모두에 있다 — 없으면 키가 그대로 찍힌다', () => {
@@ -80,11 +79,15 @@ test('판정 탭의 말은 다섯 말 모두에 있다 — 없으면 키가 그
80
79
  }
81
80
  })
82
81
 
83
- test('부품이 없는 도형에는 「부품에 맞추기」를 내지 않고, 실패해도 편집기를 지우지 않는다', () => {
84
- /* 새 도형은 부품 없이도 「공간 선언 없음」으로 막힌다. 그때 맞추기는 OCCUPANCY_NO_PARTS 로 던진다. */
85
- assert.ok(/if \(!this\.bodies\) return nothing/.test(verdict), '잴 부품이 없으면 단추를 내지 않는다')
86
- assert.ok(/bodies = cost \? cost\.groups \+ cost\.hidden : 0/.test(verdict), '감춘 부품도 잴 수 있는 부품이다')
87
- const fit = page.slice(page.indexOf('private fitToParts'), page.indexOf('private released'))
88
- assert.ok(!/this\.error\s*=/.test(fit), '맞추기 실패가 화면 전체를 오류 한 줄로 바꾸지 않는다')
89
- assert.ok(fit.includes("i18next.t('figure.text.fit-needs-parts')"), '부품이 없다는 것을 사람의 말로 한다')
82
+ test('감춘 부품도 「부품에 맞추기」가 잴 수 있는 부품이다', () => {
83
+ /* 새 도형은 부품 없이도 「공간 선언 없음」으로 막힌다. 그때 맞추기는 OCCUPANCY_NO_PARTS 로 던진다 — 단추는 잴 부품이 있을 때만. */
84
+ let asset = createFigureAsset('Probe')
85
+ asset = addFigureBox(asset, { id: 'base', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
86
+ asset = addFigureBox(asset, { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 200, y: 50, z: 0 } })
87
+ asset = moving(asset, 'base', { kind: 'turn', axis: 'y', id: 'base.turn', min: 0, max: 90 })
88
+ asset = editFigurePart(asset, { kind: 'set-visibility', target: 'lamp', shownWhen: { input: 'base.turn', atLeast: 80 } })
89
+ const cost = figureCostOf(asset)
90
+ assert.equal(cost.hidden, 1, '전제: 등은 지금 감춰져 있다')
91
+ assert.equal(verdictMeasureOf(asset).bodies, 2, '그려지는 하나와 감춘 하나')
92
+ assert.equal(verdictMeasureOf(createFigureAsset('Empty')).bodies, 0, '부품이 없으면 잴 것이 없다')
90
93
  })
package/test/view.test.ts CHANGED
@@ -1,20 +1,13 @@
1
1
  /*
2
2
  * **보기 — 작업대의 설정** (「환경」 탭). 도형에 저장되지 않고, 보는 사람의 브라우저에 남는다.
3
+ * 탭에서 바꾸면 판이 따르고 브라우저에 남으며 도형은 그대로인 것은 브라우저 검사(`test/browser/screen.check.mjs`)가 본다.
3
4
  */
4
5
  import { test } from 'node:test'
5
6
  import assert from 'node:assert/strict'
6
- import { readFileSync } from 'node:fs'
7
- import { fileURLToPath } from 'node:url'
8
7
 
8
+ import { ENVIRONMENTS } from '../client/modeller/figure-view.ts'
9
9
 
10
- const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
11
-
12
- test('환경 탭 — 설정은 판의 상태 이름이고, 도형이 아니라 브라우저에 둔다', () => {
13
- const editor = read('../client/modeller/editor.ts')
14
- assert.ok(editor.includes('<figure-settings .view=${this.view} @update-view=${(e: CustomEvent) => this.setView(e.detail.view)}></figure-settings>'))
15
- assert.ok(editor.includes("localStorage.setItem('figure-view'"))
16
- assert.doesNotMatch(editor.slice(editor.indexOf('private setView'), editor.indexOf('private setView') + 400), /session/, '설정은 세션(도형)에 닿지 않는다')
17
- const view = read('../client/modeller/figure-view.ts')
18
- assert.ok(view.includes("'studio',") && view.includes("'rainy'"), '환경 여덟')
19
- assert.ok(read('../client/modeller/figure-settings.ts').includes("i18next.t('figure.text.view-settings-are-not-saved')"))
10
+ test('환경 탭 — 씬이 받는 환경 여덟을 모두 고를 수 있다', () => {
11
+ assert.equal(ENVIRONMENTS.length, 8, '환경 여덟')
12
+ assert.ok(ENVIRONMENTS.includes('studio') && ENVIRONMENTS.includes('rainy'))
20
13
  })
@@ -56,6 +56,9 @@
56
56
  "figure.text.repeat-fixed-count": "a repeat of a fixed count; its pitch cannot be set yet.",
57
57
  "figure.text.repeat-what-pitch-gives": "{count} at a length of {length} mm · placed up to {longest} mm long ({limit} in a row).",
58
58
  "figure.text.repeat-pitch-too-small": "the pitch must be at least {min} mm: at the authored length of {length} mm it would make more than {limit}.",
59
+ "figure.text.row-copies-over-limit": "At this pitch the row has {count} copies. A row takes up to {limit}. Widen the pitch.",
60
+ "figure.text.row-geometry-over-limit": "Laid out, the row would draw {count} bodies. A figure draws up to {limit}.",
61
+ "figure.text.row-copies-out-of-range": "A row has {min} to {max} copies. Given: {count}",
59
62
  "figure.text.gizmo-joint-places": "a joint places {part}. move it with its control in the motion tab.",
60
63
  "figure.label.motion-continuous": "turn without end",
61
64
  "figure.text.fasten-before-moving": "fasten it to another part or the floor first; then it can move.",
@@ -768,6 +771,10 @@
768
771
  "figure.text.removal-mounting-face": "The mounting face is released.",
769
772
  "figure.text.edit-refused": "This edit cannot be made. ({code})",
770
773
  "figure.text.edit-refused-node-limit": "The figure is too large to add more. It holds up to {max} nodes.",
774
+ "figure.text.document-too-large": "The figure is {at} KiB, over the {max} KiB it can be saved at.",
775
+ "figure.text.document-too-large-unmeasured": "The figure is over the {max} KiB it can be saved at.",
776
+ "figure.text.document-nesting-too-deep": "The figure data is nested {at} levels deep. Up to {max} are taken.",
777
+ "figure.text.part-name-has-nested": "The part '{nested}' is named under this part. Rename that part first.",
771
778
  "figure.text.recipe-step-refused": "Stopped at step {step} of the recipe. {why} ({code})",
772
779
  "figure.text.removal-space-refitted": "The space is fitted to the parts left.",
773
780
  "figure.text.placement-intent-floor": "Floor placement: the bottom keeps its current distance from the floor (Y=0) as the size changes. Its place on the floor (X, Z) stays.",
@@ -987,5 +994,58 @@
987
994
  "figure.text.detail-playing": "Playing",
988
995
  "figure.label.detail-summary": "Drawing cost of the selected model",
989
996
  "figure.text.detail-paused": "Paused",
990
- "figure.button.download-package": "download package"
997
+ "figure.button.download-package": "download package",
998
+ "figure.label.group-carrier": "Carrier",
999
+ "figure.label.carrier-type": "Carrier type",
1000
+ "figure.text.carrier-type-placeholder": "CARRIERTYPE",
1001
+ "figure.label.carrier-size": "Physical size",
1002
+ "figure.label.carrier-width": "Width",
1003
+ "figure.label.carrier-length": "Length",
1004
+ "figure.label.carrier-height": "Height",
1005
+ "figure.button.declare-carrier": "Declare carrier",
1006
+ "figure.button.clear-carrier": "Clear",
1007
+ "figure.text.carrier-type-required": "Enter the carrier type code.",
1008
+ "figure.text.carrier-type-too-long": "A carrier type code is at most {max} characters.",
1009
+ "figure.text.carrier-length-positive": "{axis} must be greater than 0.",
1010
+ "figure.button.import-mesh": "import GLB",
1011
+ "figure.text.import-mesh-hint": "brings a GLB file in as a part: the whole file, or one part of it.",
1012
+ "figure.text.mesh-whole-file": "whole file",
1013
+ "figure.text.mesh-triangles": "{n} triangles",
1014
+ "figure.title.choose-mesh-part": "part to bring in",
1015
+ "figure.label.face": "face",
1016
+ "figure.label.places-one": "one place",
1017
+ "figure.label.places-by-rule": "many by rule",
1018
+ "figure.label.slot-layout": "slot layout",
1019
+ "figure.label.slot-layout-name": "name",
1020
+ "figure.label.slot-layout-role": "kind of place",
1021
+ "figure.label.slot-anchors": "anchors",
1022
+ "figure.label.slot-anchor-id": "anchor name",
1023
+ "figure.button.remove-slot-anchor": "remove anchor",
1024
+ "figure.button.add-slot-anchor-top": "top of {part} as an anchor",
1025
+ "figure.button.add-slot-anchor-bottom": "bottom of {part} as an anchor",
1026
+ "figure.text.slot-anchor-pick": "pick a part on the canvas to add its top or bottom as an anchor. slots on a moving part move with it.",
1027
+ "figure.button.apply-slot-anchors": "apply name and anchors",
1028
+ "figure.button.remove-slot-layout": "remove slot layout",
1029
+ "figure.text.slot-accepts-required": "each slot must name at least one type it takes.",
1030
+ "figure.text.slot-layout-label-missing": "the slot layout needs a name.",
1031
+ "figure.text.slot-anchors-missing": "add at least one anchor.",
1032
+ "figure.text.slot-anchor-duplicate": "the anchor name {anchor} is used twice.",
1033
+ "figure.text.slot-anchor-not-flat": "anchor {anchor} must be the top or the bottom of {part}.",
1034
+ "figure.text.slot-anchor-no-part": "the part {part} of anchor {anchor} is not in this figure.",
1035
+ "figure.text.slot-layout-issues": "the value at {path} in the slot layout is not valid.",
1036
+ "figure.text.slot-accepts-empty": "slot {slot} takes no type.",
1037
+ "figure.text.slot-id-taken": "{slot} is already the name of a place in this figure.",
1038
+ "figure.label.slot-preset-single": "single slot",
1039
+ "figure.label.slot-preset-agvTandem": "AGV tandem (front/rear)",
1040
+ "figure.label.slot-preset-agvParallel": "AGV parallel (left/right)",
1041
+ "figure.label.slot-preset-agv2x2": "AGV 2×2",
1042
+ "figure.label.slot-preset-bufferPort2x2": "buffer port 2×2",
1043
+ "figure.label.slot-preset-multiBufferPort": "multi-buffer port (4)",
1044
+ "figure.label.slot-preset-dualTierConveyor": "dual-tier conveyor",
1045
+ "figure.label.slot-preset-craneTwinFork": "crane twin fork",
1046
+ "figure.label.slot-preset-floorBlock": "floor block",
1047
+ "figure.label.slot-preset-param-upperElevation": "upper tier elevation (mm)",
1048
+ "figure.label.slot-preset-param-rows": "rows",
1049
+ "figure.label.slot-preset-param-cols": "columns",
1050
+ "figure.label.slot-preset-param-stackLimit": "stack limit"
991
1051
  }
@@ -56,6 +56,9 @@
56
56
  "figure.text.repeat-fixed-count": "una repetición de cantidad fija; su pitch aún no se puede definir.",
57
57
  "figure.text.repeat-what-pitch-gives": "{count} con una longitud de {length} mm · se colocan hasta {longest} mm de longitud ({limit} en una fila).",
58
58
  "figure.text.repeat-pitch-too-small": "el pitch debe ser de al menos {min} mm: con la longitud definida de {length} mm se superarían {limit}.",
59
+ "figure.text.row-copies-over-limit": "Con este paso la fila tiene {count} copias. Una fila admite hasta {limit}. Amplíe el paso.",
60
+ "figure.text.row-geometry-over-limit": "Desplegada, la fila dibujaría {count} cuerpos. Una figura dibuja hasta {limit}.",
61
+ "figure.text.row-copies-out-of-range": "Una fila tiene de {min} a {max} copias. Valor dado: {count}",
59
62
  "figure.text.gizmo-joint-places": "una articulación coloca {part}. muévala con su control en la pestaña de movimiento.",
60
63
  "figure.label.motion-continuous": "girar sin fin",
61
64
  "figure.text.fasten-before-moving": "primero sujétela a otra pieza o al suelo; después podrá moverse.",
@@ -768,6 +771,10 @@
768
771
  "figure.text.removal-mounting-face": "La cara de montaje se ha liberado.",
769
772
  "figure.text.edit-refused": "No se puede realizar esta edición. ({code})",
770
773
  "figure.text.edit-refused-node-limit": "La figura es demasiado grande para añadir más. Admite hasta {max} nodos.",
774
+ "figure.text.document-too-large": "La figura ocupa {at} KiB, más que los {max} KiB con los que se puede guardar.",
775
+ "figure.text.document-too-large-unmeasured": "La figura supera los {max} KiB con los que se puede guardar.",
776
+ "figure.text.document-nesting-too-deep": "Los datos de la figura están anidados {at} niveles. Se admiten hasta {max}.",
777
+ "figure.text.part-name-has-nested": "La pieza '{nested}' tiene su nombre bajo esta pieza. Cambie primero el nombre de esa pieza.",
771
778
  "figure.text.recipe-step-refused": "Se detuvo en el paso {step} de la receta. {why} ({code})",
772
779
  "figure.text.removal-space-refitted": "El espacio se ha ajustado a las piezas restantes.",
773
780
  "figure.text.placement-intent-floor": "Colocación en el suelo: la base mantiene su distancia actual al suelo (Y=0) al cambiar el tamaño. Su posición en el suelo (X, Z) se mantiene.",
@@ -987,5 +994,58 @@
987
994
  "figure.text.detail-playing": "Reproduciendo",
988
995
  "figure.label.detail-summary": "Coste de dibujo del modelo seleccionado",
989
996
  "figure.text.detail-paused": "En pausa",
990
- "figure.button.download-package": "descargar paquete"
997
+ "figure.button.download-package": "descargar paquete",
998
+ "figure.label.group-carrier": "Portador",
999
+ "figure.label.carrier-type": "Tipo de portador",
1000
+ "figure.text.carrier-type-placeholder": "CARRIERTYPE",
1001
+ "figure.label.carrier-size": "Tamaño físico",
1002
+ "figure.label.carrier-width": "Ancho",
1003
+ "figure.label.carrier-length": "Largo",
1004
+ "figure.label.carrier-height": "Alto",
1005
+ "figure.button.declare-carrier": "Declarar portador",
1006
+ "figure.button.clear-carrier": "Quitar",
1007
+ "figure.text.carrier-type-required": "Introduzca el código del tipo de portador.",
1008
+ "figure.text.carrier-type-too-long": "El código del tipo de portador tiene como máximo {max} caracteres.",
1009
+ "figure.text.carrier-length-positive": "{axis} debe ser mayor que 0.",
1010
+ "figure.button.import-mesh": "importar GLB",
1011
+ "figure.text.import-mesh-hint": "trae un archivo GLB como pieza: el archivo entero o una parte de él.",
1012
+ "figure.text.mesh-whole-file": "archivo entero",
1013
+ "figure.text.mesh-triangles": "{n} triángulos",
1014
+ "figure.title.choose-mesh-part": "parte a traer",
1015
+ "figure.label.face": "cara",
1016
+ "figure.label.places-one": "un lugar",
1017
+ "figure.label.places-by-rule": "varios por regla",
1018
+ "figure.label.slot-layout": "disposición de huecos",
1019
+ "figure.label.slot-layout-name": "nombre",
1020
+ "figure.label.slot-layout-role": "tipo de lugar",
1021
+ "figure.label.slot-anchors": "caras de referencia",
1022
+ "figure.label.slot-anchor-id": "nombre de la cara de referencia",
1023
+ "figure.button.remove-slot-anchor": "quitar cara de referencia",
1024
+ "figure.button.add-slot-anchor-top": "cara superior de {part} como referencia",
1025
+ "figure.button.add-slot-anchor-bottom": "cara inferior de {part} como referencia",
1026
+ "figure.text.slot-anchor-pick": "elija una pieza en el lienzo para añadir su cara superior o inferior como referencia. los huecos sobre una pieza móvil se mueven con ella.",
1027
+ "figure.button.apply-slot-anchors": "aplicar nombre y caras de referencia",
1028
+ "figure.button.remove-slot-layout": "quitar disposición de huecos",
1029
+ "figure.text.slot-accepts-required": "cada hueco debe nombrar al menos un tipo que acepta.",
1030
+ "figure.text.slot-layout-label-missing": "la disposición de huecos necesita un nombre.",
1031
+ "figure.text.slot-anchors-missing": "añada al menos una cara de referencia.",
1032
+ "figure.text.slot-anchor-duplicate": "el nombre de cara de referencia {anchor} se usa dos veces.",
1033
+ "figure.text.slot-anchor-not-flat": "la cara de referencia {anchor} debe ser la cara superior o inferior de {part}.",
1034
+ "figure.text.slot-anchor-no-part": "la pieza {part} de la cara de referencia {anchor} no está en esta figura.",
1035
+ "figure.text.slot-layout-issues": "el valor en {path} de la disposición de huecos no es válido.",
1036
+ "figure.text.slot-accepts-empty": "el hueco {slot} no acepta ningún tipo.",
1037
+ "figure.text.slot-id-taken": "{slot} ya es el nombre de un lugar en esta figura.",
1038
+ "figure.label.slot-preset-single": "un hueco",
1039
+ "figure.label.slot-preset-agvTandem": "AGV en tándem (delante/detrás)",
1040
+ "figure.label.slot-preset-agvParallel": "AGV en paralelo (izquierda/derecha)",
1041
+ "figure.label.slot-preset-agv2x2": "AGV 2×2",
1042
+ "figure.label.slot-preset-bufferPort2x2": "puerto de búfer 2×2",
1043
+ "figure.label.slot-preset-multiBufferPort": "puerto multibúfer (4)",
1044
+ "figure.label.slot-preset-dualTierConveyor": "transportador de dos niveles",
1045
+ "figure.label.slot-preset-craneTwinFork": "horquilla doble de grúa",
1046
+ "figure.label.slot-preset-floorBlock": "bloque en suelo",
1047
+ "figure.label.slot-preset-param-upperElevation": "altura del nivel superior (mm)",
1048
+ "figure.label.slot-preset-param-rows": "filas",
1049
+ "figure.label.slot-preset-param-cols": "columnas",
1050
+ "figure.label.slot-preset-param-stackLimit": "límite de apilado"
991
1051
  }
@@ -56,6 +56,9 @@
56
56
  "figure.text.repeat-fixed-count": "個数が固定された繰り返しのため、間隔はまだ変更できません。",
57
57
  "figure.text.repeat-what-pitch-gives": "長さ {length} mm で {count} 個 · 最大 {longest} mm まで置けます (1 列 {limit} 個)。",
58
58
  "figure.text.repeat-pitch-too-small": "pitch は {min} mm 以上にしてください。作成した長さ {length} mm で {limit} 個を超えるためです。",
59
+ "figure.text.row-copies-over-limit": "この間隔では列が {count} 個になります。1 列は {limit} 個までです。間隔を広げてください。",
60
+ "figure.text.row-geometry-over-limit": "列を展開すると形状が {count} 個になります。図形は {limit} 個まで描きます。",
61
+ "figure.text.row-copies-out-of-range": "列の個数は {min}~{max} 個です。入力値: {count}",
59
62
  "figure.text.gizmo-joint-places": "{part} は関節に付いていて、関節が位置を決めます。動きタブの操作値で動かしてください。",
60
63
  "figure.label.motion-continuous": "無限回転",
61
64
  "figure.text.fasten-before-moving": "先に他の部品か床に取り付けると、動きを付けられます。",
@@ -768,6 +771,10 @@
768
771
  "figure.text.removal-mounting-face": "取付面が外れました。",
769
772
  "figure.text.edit-refused": "この編集はできません。({code})",
770
773
  "figure.text.edit-refused-node-limit": "図形が大きいためこれ以上追加できません。ノードは {max} 個までです。",
774
+ "figure.text.document-too-large": "図形が {at} KiB で、保存できる上限 {max} KiB を超えています。",
775
+ "figure.text.document-too-large-unmeasured": "図形が保存できる上限 {max} KiB を超えています。",
776
+ "figure.text.document-nesting-too-deep": "図形データの入れ子が {at} 段あります。{max} 段まで受け付けます。",
777
+ "figure.text.part-name-has-nested": "部品 '{nested}' がこの部品の名前の下にあります。先にその部品の名前を変えてください。",
771
778
  "figure.text.recipe-step-refused": "レシピの {step} 番目の手順で止まりました。{why} ({code})",
772
779
  "figure.text.removal-space-refitted": "空間を残りの部品に合わせ直しました。",
773
780
  "figure.text.placement-intent-floor": "床配置: 底面は床(Y=0)からの今の間隔を保ったまま、サイズに追従します。床の上の位置(X・Z)はそのままです。",
@@ -987,5 +994,58 @@
987
994
  "figure.text.detail-playing": "再生中",
988
995
  "figure.label.detail-summary": "選択したモデルの描画コスト",
989
996
  "figure.text.detail-paused": "一時停止",
990
- "figure.button.download-package": "パッケージをダウンロード"
997
+ "figure.button.download-package": "パッケージをダウンロード",
998
+ "figure.label.group-carrier": "キャリア",
999
+ "figure.label.carrier-type": "キャリアタイプ",
1000
+ "figure.text.carrier-type-placeholder": "CARRIERTYPE",
1001
+ "figure.label.carrier-size": "実寸",
1002
+ "figure.label.carrier-width": "幅",
1003
+ "figure.label.carrier-length": "長さ",
1004
+ "figure.label.carrier-height": "高さ",
1005
+ "figure.button.declare-carrier": "キャリアとして宣言",
1006
+ "figure.button.clear-carrier": "宣言を解除",
1007
+ "figure.text.carrier-type-required": "キャリアタイプのコードを入力してください。",
1008
+ "figure.text.carrier-type-too-long": "キャリアタイプのコードは {max} 文字までです。",
1009
+ "figure.text.carrier-length-positive": "{axis} は 0 より大きい値にしてください。",
1010
+ "figure.button.import-mesh": "GLB を取り込む",
1011
+ "figure.text.import-mesh-hint": "GLB ファイルを部品として取り込みます。ファイル全体か、その中の 1 つを選びます。",
1012
+ "figure.text.mesh-whole-file": "ファイル全体",
1013
+ "figure.text.mesh-triangles": "三角形 {n} 個",
1014
+ "figure.title.choose-mesh-part": "取り込む部分",
1015
+ "figure.label.face": "面",
1016
+ "figure.label.places-one": "場所を 1 つ",
1017
+ "figure.label.places-by-rule": "規則で複数",
1018
+ "figure.label.slot-layout": "場所の配置",
1019
+ "figure.label.slot-layout-name": "名前",
1020
+ "figure.label.slot-layout-role": "場所の種類",
1021
+ "figure.label.slot-anchors": "基準面",
1022
+ "figure.label.slot-anchor-id": "基準面の名前",
1023
+ "figure.button.remove-slot-anchor": "基準面を外す",
1024
+ "figure.button.add-slot-anchor-top": "{part} の上面を基準面に",
1025
+ "figure.button.add-slot-anchor-bottom": "{part} の下面を基準面に",
1026
+ "figure.text.slot-anchor-pick": "画面で部品を選ぶと、その部品の上面か下面を基準面として加えます。動く部品の基準面なら、場所も一緒に動きます。",
1027
+ "figure.button.apply-slot-anchors": "名前・基準面を適用",
1028
+ "figure.button.remove-slot-layout": "場所の配置を削除",
1029
+ "figure.text.slot-accepts-required": "場所ごとに受け取る種類を 1 つ以上書く必要があります。",
1030
+ "figure.text.slot-layout-label-missing": "場所の配置に名前が必要です。",
1031
+ "figure.text.slot-anchors-missing": "基準面を 1 つ以上加える必要があります。",
1032
+ "figure.text.slot-anchor-duplicate": "基準面の名前 {anchor} が 2 回使われています。",
1033
+ "figure.text.slot-anchor-not-flat": "基準面 {anchor} は {part} の上面か下面でなければなりません。",
1034
+ "figure.text.slot-anchor-no-part": "基準面 {anchor} の部品 {part} がこの図形にありません。",
1035
+ "figure.text.slot-layout-issues": "場所の配置の {path} の値が正しくありません。",
1036
+ "figure.text.slot-accepts-empty": "場所 {slot} が受け取る種類がありません。",
1037
+ "figure.text.slot-id-taken": "{slot} はこの図形ですでに使われている場所の名前です。",
1038
+ "figure.label.slot-preset-single": "場所 1 つ",
1039
+ "figure.label.slot-preset-agvTandem": "AGV 前後 2 箇所",
1040
+ "figure.label.slot-preset-agvParallel": "AGV 左右 2 箇所",
1041
+ "figure.label.slot-preset-agv2x2": "AGV 2×2",
1042
+ "figure.label.slot-preset-bufferPort2x2": "バッファポート 2×2",
1043
+ "figure.label.slot-preset-multiBufferPort": "マルチバッファポート (4)",
1044
+ "figure.label.slot-preset-dualTierConveyor": "2 段コンベヤ",
1045
+ "figure.label.slot-preset-craneTwinFork": "クレーン ツインフォーク",
1046
+ "figure.label.slot-preset-floorBlock": "平置きブロック",
1047
+ "figure.label.slot-preset-param-upperElevation": "上段の高さ (mm)",
1048
+ "figure.label.slot-preset-param-rows": "行",
1049
+ "figure.label.slot-preset-param-cols": "列",
1050
+ "figure.label.slot-preset-param-stackLimit": "段積み数"
991
1051
  }