@things-factory/figure-ui 10.1.176 → 10.1.177
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/modeller/editor-styles.ts +3 -5
- package/client/modeller/editor.ts +41 -39
- package/client/modeller/figure-links.ts +36 -9
- package/dist-client/modeller/editor-styles.js +3 -5
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +6 -10
- package/dist-client/modeller/editor.js +34 -37
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +7 -2
- package/dist-client/modeller/figure-links.js +40 -8
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/browser/editing-page.js +2 -1
- package/test/browser/links.check.mjs +18 -9
- package/test/browser/modeller.check.mjs +6 -0
- package/test/browser/parts.check.mjs +3 -12
- package/test/browser/places-mesh.check.mjs +4 -9
- package/test/browser/props.check.mjs +27 -10
- package/test/browser/row-preview.check.mjs +4 -0
- package/test/browser/screen.check.mjs +27 -19
- package/test/place-words.test.ts +31 -0
- package/translations/en.json +6 -3
- package/translations/es.json +3 -0
- package/translations/ja.json +3 -0
- package/translations/ko.json +6 -3
- package/translations/ms.json +3 -0
- package/translations/zh.json +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.177",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"@operato/slot-layout-editor": "^10.29.25",
|
|
42
42
|
"@operato/styles": "^10.0.0",
|
|
43
43
|
"@operato/utils": "^10.0.0",
|
|
44
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
44
|
+
"@things-factory/ai-assistant": "^10.1.177",
|
|
45
|
+
"@things-factory/figure-service": "^10.1.177",
|
|
46
46
|
"@things-factory/figure-viewer": "^10.1.176",
|
|
47
47
|
"@things-factory/setting-base": "^10.1.176",
|
|
48
48
|
"three": "^0.185.1"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "91da09c945ec5e77ab960074db8f53b9ea8ac534"
|
|
51
51
|
}
|
|
@@ -94,7 +94,8 @@ window.editing = {
|
|
|
94
94
|
},
|
|
95
95
|
/** The labels of the number fields on the properties tab. */
|
|
96
96
|
propertyInputs() {
|
|
97
|
-
|
|
97
|
+
/* The panel shown: the properties, or the 「반응형」 tab a check pressed. */
|
|
98
|
+
const panel = editor().renderRoot.querySelector('div[panel]:not([off])')
|
|
98
99
|
return [...panel.querySelectorAll('input')].map(i => i.getAttribute('aria-label'))
|
|
99
100
|
},
|
|
100
101
|
/** The page point of the first element a selector finds -- anywhere on the page, or inside the `host` elements -- and what it says. */
|
|
@@ -90,11 +90,6 @@ async function pick(part) {
|
|
|
90
90
|
await until(id => probe.state().selected === id, [part], `${part} selected`)
|
|
91
91
|
}
|
|
92
92
|
const tab = name => read(l => probe.tab(l), `figure.label.tab-short-${name}`).then(point => click(point, `the ${name} tab`))
|
|
93
|
-
/** What the figure does is a folded group of the properties (chief architect 2026-10-16): its fold opened, as a person opens it. */
|
|
94
|
-
async function openCapability() {
|
|
95
|
-
const summary = await handle('section[capability] > details > summary')
|
|
96
|
-
if (!(await summary.evaluate(s => s.parentElement.open))) await summary.click()
|
|
97
|
-
}
|
|
98
93
|
/** A fastening the part has is said in one line; 「바꾸기」 opens its drawing and details. */
|
|
99
94
|
async function changeFastening() {
|
|
100
95
|
await until(() => look.all('button[change-fastening]').length === 1, [], 'the fastening said in one line')
|
|
@@ -211,6 +206,8 @@ describe('a part made to follow the figure size, and let go of it', () => {
|
|
|
211
206
|
a = applyFigureAuthoring(a, { kind: 'declare-instance-size', range: SIZE, at: { x: 400, y: 400, z: 400 } })
|
|
212
207
|
await mount('sized', applyFigureAuthoring(a, { kind: 'link-dimension', part: 'box', axis: 'x', source: { of: 'instance', axis: 'x' } }))
|
|
213
208
|
await pick('box')
|
|
209
|
+
/* How a part answers a size is the 「반응형」 tab's (user 2026-10-17). */
|
|
210
|
+
await tab('response')
|
|
214
211
|
})
|
|
215
212
|
|
|
216
213
|
test('on a figure with a size, follow-size is one step, says nothing of a size declared, and is not offered again', async () => {
|
|
@@ -253,6 +250,8 @@ function sizedPair(turning) {
|
|
|
253
250
|
describe('the sizes a figure may be placed at, and how it grows', () => {
|
|
254
251
|
test('measured again, the declared range becomes the measured one in one step, and the offer goes', async () => {
|
|
255
252
|
await mount('sized-pair', sizedPair(false))
|
|
253
|
+
/* The figure's range is the 「반응형」 tab's. */
|
|
254
|
+
await tab('response')
|
|
256
255
|
const start = await state()
|
|
257
256
|
await click(await at('button', 'figure.button.remeasure-range'), 'remeasure')
|
|
258
257
|
await oneStep(start)
|
|
@@ -270,6 +269,7 @@ describe('the sizes a figure may be placed at, and how it grows', () => {
|
|
|
270
269
|
|
|
271
270
|
test('a figure that turns about a joint is urged to grow by one ratio', async () => {
|
|
272
271
|
await mount('turning', sizedPair(true))
|
|
272
|
+
await tab('response')
|
|
273
273
|
assert.equal((await all('p[hint]', 'figure.text.sizing-uniform-proposed')).length, 1)
|
|
274
274
|
})
|
|
275
275
|
})
|
|
@@ -285,6 +285,8 @@ describe('a row\'s pitch', () => {
|
|
|
285
285
|
test('a pitch that follows the frame\'s length shows as the number it comes to, and the row is said as drawn', async () => {
|
|
286
286
|
await mount('pitch-follows', applyFigureAuthoring(conveyor(), { kind: 'set-repeat', part: 'roller', pitch: { from: { of: 'part', part: 'frame', axis: 'x' }, times: 0.1 } }))
|
|
287
287
|
await pick('roller')
|
|
288
|
+
/* The row is the 「반응형」 tab's. */
|
|
289
|
+
await tab('response')
|
|
288
290
|
assert.deepEqual((await all('input[aria-label="roller.repeat.pitch"]')).map(i => i.value), ['200'], 'a tenth of 2000')
|
|
289
291
|
assert.ok((await all('ul[row-reading] li')).length > 0, 'the row said as it stands')
|
|
290
292
|
})
|
|
@@ -297,6 +299,7 @@ describe('a row\'s pitch', () => {
|
|
|
297
299
|
])
|
|
298
300
|
await mount('published-pitch', published)
|
|
299
301
|
await pick('roller')
|
|
302
|
+
await tab('response')
|
|
300
303
|
const start = await state()
|
|
301
304
|
await type(await handle('input[aria-label="roller.repeat.pitch"]'), '300')
|
|
302
305
|
await oneStep(start)
|
|
@@ -310,7 +313,8 @@ describe('holding places on a part', () => {
|
|
|
310
313
|
test('a place needs kinds it takes; a figure that holds nothing says so, and declaring it holds things is one step', async () => {
|
|
311
314
|
await mount('box')
|
|
312
315
|
await pick('box')
|
|
313
|
-
|
|
316
|
+
/* What the figure does is a tab again (user 2026-10-17). */
|
|
317
|
+
await tab('capability')
|
|
314
318
|
assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, true, 'no kinds: no place')
|
|
315
319
|
await type(await handle('input[placeholder="BOX, TOTE"]'), 'BOX')
|
|
316
320
|
assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, false, 'a kind: a place may be made')
|
|
@@ -333,6 +337,8 @@ describe('a position worked out from the figure size', () => {
|
|
|
333
337
|
a.document.model.nodes.find(n => n.id === 'lamp.pose').args[1] = 'lamp.lift.value'
|
|
334
338
|
await mount('lifted', a)
|
|
335
339
|
await pick('lamp')
|
|
340
|
+
/* A position that follows the figure size is the 「반응형」 tab's. */
|
|
341
|
+
await tab('response')
|
|
336
342
|
assert.equal((await all('div[pair] span', 'figure.label.position-worked-out')).length, 1)
|
|
337
343
|
const start = await state()
|
|
338
344
|
await click(await at('button[act]', 'figure.button.scale-position'), 'scale position')
|
|
@@ -531,13 +537,15 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
|
|
|
531
537
|
|
|
532
538
|
test('a part fastened to nothing: the row is folded under its head and says why', async () => {
|
|
533
539
|
await pick('loose')
|
|
540
|
+
await tab('response')
|
|
534
541
|
assert.deepEqual((await all('details[row-fold] > summary h3')).map(h => h.text), ['figure.label.repeat'])
|
|
535
542
|
assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [false], 'folded')
|
|
536
543
|
assert.deepEqual((await all('p[row-blocked]')).map(p => p.text), ['figure.text.row-blocked-unfastened'])
|
|
537
544
|
})
|
|
538
545
|
|
|
539
|
-
test('the row worked on is the one seen: its copies drawn and said;
|
|
546
|
+
test('the row worked on is the one seen: its copies drawn and said; on the properties tab they are not', async () => {
|
|
540
547
|
await pick('lamp')
|
|
548
|
+
await tab('response')
|
|
541
549
|
assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [false], 'folded')
|
|
542
550
|
await read(() => look.hear('preview-command'))
|
|
543
551
|
/* One click opens it, and it stays open while the row is worked. */
|
|
@@ -549,8 +557,9 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
|
|
|
549
557
|
assert.equal((await heard('preview-command')).filter(Boolean).at(-1).action.kind, 'make-repeat')
|
|
550
558
|
await until(() => look.drawn().filter(n => n.startsWith('__preview')).length > 1, [], 'the copies drawn')
|
|
551
559
|
await until(() => look.all('section[row] [preview-words]', 'figure.text.row-count-pitch').length === 1, [], 'the row said')
|
|
552
|
-
|
|
553
|
-
await
|
|
560
|
+
/* The properties tab shown: what its fastening would do is said, the row's copies are not drawn. */
|
|
561
|
+
await tab('properties')
|
|
562
|
+
await until(() => !look.drawn().some(n => n.startsWith('__preview')), [], 'the copies not drawn on the properties tab')
|
|
554
563
|
})
|
|
555
564
|
})
|
|
556
565
|
|
|
@@ -53,6 +53,8 @@ async function click(point) {
|
|
|
53
53
|
await page().mouse.click(point.x, point.y)
|
|
54
54
|
await sleep(400)
|
|
55
55
|
}
|
|
56
|
+
/* A side tab pressed, by its short name; how a part answers a size is the 「반응형」 tab's (user 2026-10-17). */
|
|
57
|
+
const tab = async name => click(await read(l => probe.tab(l), `figure.label.tab-short-${name}`))
|
|
56
58
|
|
|
57
59
|
const KEYS = { translate: 'KeyW', rotate: 'KeyE', scale: 'KeyR' }
|
|
58
60
|
|
|
@@ -383,6 +385,7 @@ describe('the fastening form belongs to the part it is for', () => {
|
|
|
383
385
|
describe('a figure made in the modeller is given its size from its parts (user 2026-09-25)', () => {
|
|
384
386
|
before(async () => {
|
|
385
387
|
await mount('box')
|
|
388
|
+
await tab('response')
|
|
386
389
|
})
|
|
387
390
|
|
|
388
391
|
test('with nothing picked, the figure offers to declare its size; clicked, its size fields appear', async () => {
|
|
@@ -697,6 +700,7 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
|
|
|
697
700
|
test('one click on a figure with no size: its size is declared from the part, said in a line, and the part follows it', async () => {
|
|
698
701
|
await mount('box')
|
|
699
702
|
await click(await read(() => probe.row('box')))
|
|
703
|
+
await tab('response')
|
|
700
704
|
const at = await page().evaluate(() => {
|
|
701
705
|
const deep = (root = document) => [...root.querySelectorAll('button[follow-size]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
702
706
|
const b = deep()[0]
|
|
@@ -827,6 +831,7 @@ describe('a row\'s pitch made a board\'s to set (ADR-0096)', () => {
|
|
|
827
831
|
test('published in one step with a name and a range; the pitch then sets the input; withdrawn, the pitch stays as it is', async () => {
|
|
828
832
|
await mount('rollers')
|
|
829
833
|
await click(await read(() => probe.row('roller')))
|
|
834
|
+
await tab('response')
|
|
830
835
|
const before = (await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length
|
|
831
836
|
await click(await read(() => probe.labelled('figure.label.board-sets')))
|
|
832
837
|
await typeInto('figure.label.board-name', '롤러 간격')
|
|
@@ -893,6 +898,7 @@ describe('the row section opens on one click', () => {
|
|
|
893
898
|
test('a part that can be made a row: one click on the folded heading opens it, a second folds it', async () => {
|
|
894
899
|
await mount('rollers')
|
|
895
900
|
await click(await read(() => probe.row('mark')))
|
|
901
|
+
await tab('response')
|
|
896
902
|
const summary = await page().evaluateHandle(() => {
|
|
897
903
|
const deep = (root = document) => [...root.querySelectorAll('details[row-fold] > summary')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
898
904
|
return deep()[0] ?? null
|
|
@@ -320,7 +320,8 @@ describe('a row is one part: selected whole, its pitch one value set by the row
|
|
|
320
320
|
|
|
321
321
|
test('the pitch is offered once, and typed it is one step that sets the row again', async () => {
|
|
322
322
|
await pick('roller')
|
|
323
|
-
|
|
323
|
+
/* The row's pitch is the 「반응형」 tab's (user 2026-10-17). */
|
|
324
|
+
await tab('response')
|
|
324
325
|
const fields = await read(() => editing.propertyInputs())
|
|
325
326
|
assert.equal(fields.filter(label => /pitch|repeat/i.test(label ?? '')).length, 1, fields.join())
|
|
326
327
|
const label = fields.find(label => /pitch|repeat/i.test(label ?? ''))
|
|
@@ -344,18 +345,8 @@ describe('the long explanations open from their 「?」 beside the title, closed
|
|
|
344
345
|
await mount('box', 'doors')
|
|
345
346
|
await pick('left')
|
|
346
347
|
const seen = new Set()
|
|
347
|
-
for (const name of ['properties', 'motion']) {
|
|
348
|
+
for (const name of ['properties', 'response', 'motion', 'capability']) {
|
|
348
349
|
await tab(name)
|
|
349
|
-
/* What the figure does is a folded group of the properties now (chief architect 2026-10-16): opened as a hand opens it. */
|
|
350
|
-
if (name === 'properties') {
|
|
351
|
-
const fold = await read(() => {
|
|
352
|
-
const summary = document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details > summary')
|
|
353
|
-
summary.scrollIntoView({ block: 'center' })
|
|
354
|
-
const r = summary.getBoundingClientRect()
|
|
355
|
-
return { x: r.left + 20, y: r.top + r.height / 2 }
|
|
356
|
-
})
|
|
357
|
-
await click(fold, 'the capability fold')
|
|
358
|
-
}
|
|
359
350
|
const helps = await read(() =>
|
|
360
351
|
[...document.querySelector('figure-editor').renderRoot.querySelectorAll('figure-links, figure-clips')].flatMap((element, host) =>
|
|
361
352
|
[...element.shadowRoot.querySelectorAll('button[help]')].map((b, i) => ({ host, i, seen: b.getClientRects().length > 0 })).filter(b => b.seen)
|
|
@@ -198,15 +198,10 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
|
|
|
198
198
|
before(async () => {
|
|
199
199
|
await mount(addFigureBox(figure('Stacker'), { id: 'fork', size: { x: 1200, y: 50, z: 900 }, position: { x: 0, y: 25, z: 0 } }))
|
|
200
200
|
await pick('fork')
|
|
201
|
-
/* What the figure does is a
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const r = summary.getBoundingClientRect()
|
|
206
|
-
return { x: r.left + 20, y: r.top + r.height / 2 }
|
|
207
|
-
})
|
|
208
|
-
await page().mouse.click(fold.x, fold.y)
|
|
209
|
-
await until(() => !!document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details[open]'), [], 'the capability fold open')
|
|
201
|
+
/* What the figure does is a tab of its own (user 2026-10-17). */
|
|
202
|
+
const tab = await read(l => probe.tab(l), 'figure.label.tab-short-capability')
|
|
203
|
+
await page().mouse.click(tab.x, tab.y)
|
|
204
|
+
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-capability', [], 'the capability tab on')
|
|
210
205
|
await settled()
|
|
211
206
|
})
|
|
212
207
|
|
|
@@ -70,8 +70,8 @@ async function press(selector, what) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* The groups of the panel shown, top to bottom, by what each is: the part's name, the size
|
|
74
|
-
*
|
|
73
|
+
* The groups of the panel shown, top to bottom, by what each is: the part's name, the size and the place the links
|
|
74
|
+
* draw, the look and 「더 보기」 on the properties tab; on 「반응형」 the size rules, a position that follows, the row.
|
|
75
75
|
*/
|
|
76
76
|
const groupsInOrder = () =>
|
|
77
77
|
read(() => {
|
|
@@ -80,11 +80,11 @@ const groupsInOrder = () =>
|
|
|
80
80
|
const links = panel.querySelector('figure-links')?.shadowRoot
|
|
81
81
|
const marks = {
|
|
82
82
|
name: panel.querySelector('input[name-field]'),
|
|
83
|
-
size: links?.querySelector('section[size]'),
|
|
83
|
+
size: links?.querySelector('section[size], section[size-response]'),
|
|
84
84
|
place: links?.querySelector('section[fastening]'),
|
|
85
|
+
position: links?.querySelector('section[position-response]'),
|
|
85
86
|
row: panel.querySelector('section[slot=row]') ?? links?.querySelector('section[row]'),
|
|
86
87
|
look: panel.querySelector('figure-material-panel:not([show])'),
|
|
87
|
-
capability: panel.querySelector('section[capability]'),
|
|
88
88
|
more: panel.querySelector('section[more]')
|
|
89
89
|
}
|
|
90
90
|
return Object.entries(marks)
|
|
@@ -93,12 +93,18 @@ const groupsInOrder = () =>
|
|
|
93
93
|
.map(([name]) => name)
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
/* A side tab pressed, by its name. */
|
|
97
|
+
async function tab(name) {
|
|
98
|
+
await press(`aside button[tab][aria-label="figure.label.tab-short-${name}"]`, `the ${name} tab`)
|
|
99
|
+
await read(() => document.querySelector('figure-editor').updateComplete)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('the side: six tabs, the view and the history from the bar', () => {
|
|
97
103
|
before(() => mount('rollers'))
|
|
98
104
|
|
|
99
|
-
test('properties, motion, data and the verdict -- the
|
|
105
|
+
test('properties, responsive, motion, capability, data and the verdict -- the six tabs, in that order (user 2026-10-17)', async () => {
|
|
100
106
|
const tabs = await read(() => [...document.querySelector('figure-editor').shadowRoot.querySelectorAll('aside button[tab]')].map(b => b.getAttribute('aria-label')))
|
|
101
|
-
assert.deepEqual(tabs, ['properties', 'motion', 'data', 'verdict'].map(t => `figure.label.tab-short-${t}`))
|
|
107
|
+
assert.deepEqual(tabs, ['properties', 'response', 'motion', 'capability', 'data', 'verdict'].map(t => `figure.label.tab-short-${t}`))
|
|
102
108
|
})
|
|
103
109
|
|
|
104
110
|
test('the view opens over the work from the bar, pressed while open; Esc closes it', async () => {
|
|
@@ -126,18 +132,27 @@ describe('the side: four tabs, the view and the history from the bar', () => {
|
|
|
126
132
|
describe('a part\'s groups in one order, whatever its state', () => {
|
|
127
133
|
before(() => mount('rollers'))
|
|
128
134
|
|
|
129
|
-
test('a part fastened in a row: name, size, place,
|
|
135
|
+
test('a part fastened in a row: name, size, place, look, more; on 「반응형」 its size rules and its row', async () => {
|
|
130
136
|
await pick('roller')
|
|
131
|
-
|
|
137
|
+
await tab('properties')
|
|
138
|
+
assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'look', 'more'])
|
|
139
|
+
await tab('response')
|
|
140
|
+
assert.deepEqual(await groupsInOrder(), ['size', 'row'])
|
|
141
|
+
await tab('properties')
|
|
132
142
|
})
|
|
133
143
|
|
|
134
144
|
test('a part on the plane, not in a row: the same order, the row folded where a row would be', async () => {
|
|
135
145
|
await pick('bed')
|
|
136
|
-
assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', '
|
|
146
|
+
assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'look', 'more'])
|
|
147
|
+
/* On the plane, its height is worked out from the figure size: the position group stands between. */
|
|
148
|
+
await tab('response')
|
|
149
|
+
assert.deepEqual(await groupsInOrder(), ['size', 'position', 'row'])
|
|
150
|
+
await tab('properties')
|
|
137
151
|
})
|
|
138
152
|
|
|
139
153
|
test('a fastened part\'s place is one line and 「바꾸기」: no locked cells, no drawing, until it is changed', async () => {
|
|
140
154
|
await pick('mark')
|
|
155
|
+
await tab('properties')
|
|
141
156
|
const place = () =>
|
|
142
157
|
read(() => {
|
|
143
158
|
const editor = document.querySelector('figure-editor')
|
|
@@ -187,6 +202,8 @@ describe('a group\'s 「⋯」: its rarer actions, by the mouse or the keyboard'
|
|
|
187
202
|
|
|
188
203
|
test('Esc closes the menu and nothing is done', async () => {
|
|
189
204
|
await pick('roller')
|
|
205
|
+
/* The row is the 「반응형」 tab's. */
|
|
206
|
+
await tab('response')
|
|
190
207
|
const start = await revision()
|
|
191
208
|
await press('section[row] button[group-menu]', 'the row\'s 「⋯」')
|
|
192
209
|
await until(() => document.activeElement?.localName === 'ox-popup-menu', [], 'the menu open, with the focus')
|
|
@@ -60,6 +60,10 @@ describe('a row\'s preview follows its basis', () => {
|
|
|
60
60
|
const at = await read(() => probe.row('bin'))
|
|
61
61
|
await page().mouse.click(at.x, at.y)
|
|
62
62
|
await until(() => probe.state().selected === 'bin', [], 'bin selected')
|
|
63
|
+
/* The row is the 「반응형」 tab's (user 2026-10-17). */
|
|
64
|
+
const tab = await read(() => probe.tab('figure.label.tab-short-response'))
|
|
65
|
+
await page().mouse.click(tab.x, tab.y)
|
|
66
|
+
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-response', [], 'the 반응형 tab on')
|
|
63
67
|
await click('details[row-fold] > summary', {}, 'the row\'s fold')
|
|
64
68
|
})
|
|
65
69
|
|
|
@@ -191,13 +191,13 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
191
191
|
assert.equal(await read(() => getComputedStyle(screenCheck.editor()).containerType), 'inline-size')
|
|
192
192
|
})
|
|
193
193
|
|
|
194
|
-
test('the side\'s
|
|
194
|
+
test('the side\'s six tabs, in order, each saying its name under its icon -- the name it is read by', async () => {
|
|
195
195
|
const tabs = await read(() =>
|
|
196
196
|
[...screenCheck.editor().shadowRoot.querySelectorAll('aside button[tab]')].map(b => ({ label: b.getAttribute('aria-label'), shows: b.querySelector('span[tab-label]')?.textContent.trim() }))
|
|
197
197
|
)
|
|
198
198
|
assert.deepEqual(
|
|
199
199
|
tabs.map(t => t.label),
|
|
200
|
-
['properties', 'motion', 'data', 'verdict'].map(TAB)
|
|
200
|
+
['properties', 'response', 'motion', 'capability', 'data', 'verdict'].map(TAB)
|
|
201
201
|
)
|
|
202
202
|
for (const tab of tabs) assert.equal(tab.shows, tab.label, `${tab.label} shows its name`)
|
|
203
203
|
assert.equal(await read(() => screenCheck.editor().shadowRoot.querySelector('aside').firstElementChild.matches('div[tabs]')), true, 'the tabs are the side\'s first row')
|
|
@@ -206,12 +206,12 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
|
|
|
206
206
|
test('a tab shows its panel and hides the others without taking them down -- the verdict keeps what it holds', async () => {
|
|
207
207
|
const panels = () =>
|
|
208
208
|
read(() => [...screenCheck.editor().shadowRoot.querySelectorAll('aside > div[panel]')].map(p => ({ seen: p.getBoundingClientRect().height > 0, connected: p.isConnected, verdict: !!p.querySelector('slot[name=verdict]') })))
|
|
209
|
-
const tabs = ['properties', 'motion', 'data', 'verdict']
|
|
209
|
+
const tabs = ['properties', 'response', 'motion', 'capability', 'data', 'verdict']
|
|
210
210
|
for (const [at, tab] of tabs.entries()) {
|
|
211
211
|
await pressTab(TAB(tab))
|
|
212
212
|
await until(`${tab} on`, name => screenCheck.editor().shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === name, TAB(tab))
|
|
213
213
|
const now = await panels()
|
|
214
|
-
assert.equal(now.length,
|
|
214
|
+
assert.equal(now.length, 6, 'six panels, all standing')
|
|
215
215
|
assert.deepEqual(now.map(p => p.seen), tabs.map((_, i) => i === at), `${tab}: its panel seen, the rest hidden`)
|
|
216
216
|
assert.ok(now.every(p => p.connected))
|
|
217
217
|
assert.ok(now[tabs.indexOf('verdict')].verdict, 'the verdict panel holds the verdict slot whichever tab is on')
|
|
@@ -329,7 +329,7 @@ describe('the editor is drawn in the shell\'s tokens (2026-09-27)', () => {
|
|
|
329
329
|
describe('a part chosen: each tab in its place, the figure-wide tabs give way, the list\'s root goes back', () => {
|
|
330
330
|
before(() => mount('arm'))
|
|
331
331
|
|
|
332
|
-
test('properties hold the part\'s size
|
|
332
|
+
test('properties hold the part\'s size and place; 「반응형」 how it answers a size; capability its places; motion its motion and controls -- each under the part\'s head', async () => {
|
|
333
333
|
await pressRow('upper-arm')
|
|
334
334
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
335
335
|
const holds = () =>
|
|
@@ -349,16 +349,17 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
|
|
|
349
349
|
assert.ok(now.subject, 'the part\'s head')
|
|
350
350
|
assert.ok(now.links.includes('figure.label.group-size') && now.links.includes('figure.label.group-place'), `the shape's links: ${now.links}`)
|
|
351
351
|
assert.ok(now.material, 'its material')
|
|
352
|
-
/*
|
|
353
|
-
await
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
assert.ok(
|
|
361
|
-
assert.ok(
|
|
352
|
+
/* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」). */
|
|
353
|
+
await pressTab(TAB('response'))
|
|
354
|
+
now = await holds()
|
|
355
|
+
assert.ok(now.subject, 'the part\'s head')
|
|
356
|
+
assert.ok(now.links.includes('figure.label.group-size'), `the response's links: ${now.links}`)
|
|
357
|
+
/* What it does is a tab again. */
|
|
358
|
+
await pressTab(TAB('capability'))
|
|
359
|
+
now = await holds()
|
|
360
|
+
assert.ok(now.subject, 'the part\'s head')
|
|
361
|
+
assert.ok(now.links.length === 1 && now.links[0].startsWith('figure.label.places-on-part'), `the places on the part: ${now.links}`)
|
|
362
|
+
assert.ok(now.capability, 'its capabilities')
|
|
362
363
|
await pressTab(TAB('motion'))
|
|
363
364
|
now = await holds()
|
|
364
365
|
assert.ok(now.subject, 'the part\'s head')
|
|
@@ -812,8 +813,11 @@ describe('a part\'s size row: a rule\'s numbers only where a rule sets every sid
|
|
|
812
813
|
await mount('box')
|
|
813
814
|
await pressRow('box')
|
|
814
815
|
await until('box chosen', () => screenCheck.editor().selected === 'box')
|
|
815
|
-
|
|
816
|
+
/* Following the figure's size is the 「반응형」 tab's; the size row it gives is the properties'. */
|
|
817
|
+
await pressTab(TAB('response'))
|
|
818
|
+
await press('follow the figure\'s size', () => screenCheck.deep('button[follow-size]').find(b => b.checkVisibility()))
|
|
816
819
|
await until('following', () => ['x', 'y', 'z'].every(axis => !screenCheck.editor().session.read().fields.some(f => f.id === `box.size.${axis}`)))
|
|
820
|
+
await pressTab(TAB('properties'))
|
|
817
821
|
assert.deepEqual(await sizeRow(), { held: 3 }, 'three sides, each held by the rule')
|
|
818
822
|
})
|
|
819
823
|
})
|
|
@@ -841,9 +845,12 @@ describe('the side panel is found as it was left, per part', () => {
|
|
|
841
845
|
test('scrolled and a fold opened on one part; another part opens at its top; back, the first is as it was left', async () => {
|
|
842
846
|
await pressRow('upper-arm')
|
|
843
847
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
848
|
+
/* 「더 보기」 starts open on this page: folded first, so the fold opened below is one this part leaves open. */
|
|
849
|
+
if (await read(() => !!screenCheck.shownPanel().querySelector('section[more] > details[open]')))
|
|
850
|
+
await press('the more fold', () => screenCheck.shownPanel().querySelector('section[more] > details > summary'))
|
|
844
851
|
await press('a fold', () => {
|
|
845
852
|
const panel = screenCheck.shownPanel()
|
|
846
|
-
const closed = [...panel.querySelectorAll('figure-links')].flatMap(l => [...l.shadowRoot.querySelectorAll('details')]).find(d => !d.open)
|
|
853
|
+
const closed = [...panel.querySelectorAll('details'), ...[...panel.querySelectorAll('figure-links')].flatMap(l => [...l.shadowRoot.querySelectorAll('details')])].find(d => !d.open && d.checkVisibility())
|
|
847
854
|
return closed?.querySelector('summary')
|
|
848
855
|
})
|
|
849
856
|
const opened = await folds()
|
|
@@ -1165,8 +1172,9 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1165
1172
|
await open('fig-p', 'arm-clip')
|
|
1166
1173
|
await pressRow('upper-arm')
|
|
1167
1174
|
await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
|
|
1168
|
-
/* The part's panel fits the side now
|
|
1169
|
-
|
|
1175
|
+
/* The part's panel fits the side now: its 「더 보기」 open makes it long enough to scroll. */
|
|
1176
|
+
if (await read(() => !!screenCheck.shownPanel().querySelector('section[more] > details:not([open])')))
|
|
1177
|
+
await press('the more fold', () => screenCheck.shownPanel().querySelector('section[more] > details:not([open]) > summary'))
|
|
1170
1178
|
const at = await read(() => {
|
|
1171
1179
|
const r = screenCheck.shownPanel().getBoundingClientRect()
|
|
1172
1180
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Words that send the author somewhere name a place that is there (chief architect's ruling 2026-10-17: six side tabs,
|
|
3
|
+
* 「반응형」 holding size behaviour, the position that follows the size, the row and the range). The modeller once said
|
|
4
|
+
* 「움직임 탭의 치수에서」 and 「「위치와 크기」에서 「여기에 고정」」 after both had moved, and 「성숙도 탭」 for the tab named 검사.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
const table = (path: string) => JSON.parse(readFileSync(new URL(path, import.meta.url), 'utf8')) as Record<string, string>
|
|
11
|
+
const GONE: Record<string, RegExp> = {
|
|
12
|
+
ko: /성숙도 탭|「위치와 크기」|움직임 탭의 (치수|붙이기)/,
|
|
13
|
+
en: /maturity tab|under placement|under size and placement|(sizes|fastening) in the motion tab/
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('no message sends the author to a tab or group the side no longer has', () => {
|
|
17
|
+
for (const lang of ['ko', 'en'] as const)
|
|
18
|
+
for (const path of ['../translations/', '../../figure-service/translations/']) {
|
|
19
|
+
const said = Object.entries(table(`${path}${lang}.json`)).filter(([, words]) => GONE[lang]!.test(words))
|
|
20
|
+
assert.deepEqual(said, [], `${path}${lang}.json`)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('the places named are the tabs as they are called', () => {
|
|
25
|
+
const ko = table('../translations/ko.json')
|
|
26
|
+
const service = table('../../figure-service/translations/ko.json')
|
|
27
|
+
assert.ok(ko['figure.text.gizmo-size-follows']!.includes(`${ko['figure.label.tab-short-response']} 탭`))
|
|
28
|
+
assert.ok(ko['figure.text.preview-blocked-by-errors']!.includes(`${ko['figure.label.tab-short-verdict']} 탭`))
|
|
29
|
+
for (const key of ['figure.gate.parts-split-on-resize.how', 'figure.gate.sizing-not-uniform.how'])
|
|
30
|
+
assert.ok(service[key]!.includes(`${ko['figure.label.tab-short-response']} 탭의 「${ko['figure.label.group-position-response']}」`), key)
|
|
31
|
+
})
|
package/translations/en.json
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"figure.label.group-menu": "More actions",
|
|
21
21
|
"figure.label.group-more": "More",
|
|
22
22
|
"figure.label.group-place": "Place",
|
|
23
|
+
"figure.label.group-position-response": "Position",
|
|
24
|
+
"figure.label.tab-response": "Responsive -- when the size changes",
|
|
25
|
+
"figure.label.tab-short-response": "Responsive",
|
|
23
26
|
"figure.text.sizing-uniform-proposed": "this figure has joints. growing it by one ratio keeps the joints together at any size.",
|
|
24
27
|
"figure.text.sizing-uniform-explained": "grows the whole figure by the geometric mean of the three sizes a board gives it. the feet stay on the floor; room left above the figure stays empty.",
|
|
25
28
|
"figure.button.mirror-x": "across X",
|
|
@@ -75,9 +78,9 @@
|
|
|
75
78
|
"figure.text.gizmo-joint-mimic": "the {joint} joint follows {leader}. move {leader}.",
|
|
76
79
|
"figure.text.gizmo-joint-fixed-turn": "the {joint} joint does not turn. give it a turn in the motion tab.",
|
|
77
80
|
"figure.text.gizmo-joint-turned-slide": "the {joint} joint only slides. use the move handle.",
|
|
78
|
-
"figure.text.gizmo-joint-origin-follows": "the {joint} joint's place follows another part. change it under
|
|
81
|
+
"figure.text.gizmo-joint-origin-follows": "the {joint} joint's place follows another part. change it under place in the properties tab.",
|
|
79
82
|
"figure.text.gizmo-joint-off-axis": "the {joint} joint turns only about {axis}. use the {axis} rotate handle.",
|
|
80
|
-
"figure.text.gizmo-size-follows": "{part}'s {axis} size follows another measure. change it under
|
|
83
|
+
"figure.text.gizmo-size-follows": "{part}'s {axis} size follows another measure. change it under size in the responsive tab.",
|
|
81
84
|
"figure.text.gizmo-fastening-places": "{part} is fastened to a face of another part, which places it. detach it first to move it.",
|
|
82
85
|
"figure.text.instance-range-can-widen": "measured again with the parts as they are: {ranges} mm.",
|
|
83
86
|
"figure.text.size-range-too-small": "below {mm} mm on {axis} the {part} part has no length, yet the figure is declared to be placed smaller, so this change cannot be made. use \"measure again\" under the sizes it can be placed at.",
|
|
@@ -472,7 +475,7 @@
|
|
|
472
475
|
"figure.text.pick-a-shape-above-to-start": "pick a shape above to add the first part.",
|
|
473
476
|
"figure.text.placement-explained": "sets the anchor plane in 3D scene (floor: AGV/conveyor, ceiling: OHT/hoist, center: sensor/drone).",
|
|
474
477
|
"figure.text.pose-as-default-explained": "Writes the pose you have set as the parameters’ defaults, so the figure stands like this when nobody gives it a value.",
|
|
475
|
-
"figure.text.preview-blocked-by-errors": "cannot show the preview because of format errors. see the
|
|
478
|
+
"figure.text.preview-blocked-by-errors": "cannot show the preview because of format errors. see the check tab for the reason.",
|
|
476
479
|
"figure.text.put-on-floor": "Moves the part once so its bottom is on the floor (Y=0). It does not stay there when the size changes; for that, use “Follow size, attached to the floor”.",
|
|
477
480
|
"figure.text.range-needs-room": "the maximum must be above the minimum.",
|
|
478
481
|
"figure.text.release-needs-a-save-first": "there are unsaved changes. save first.",
|
package/translations/es.json
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"figure.label.group-menu": "Más acciones",
|
|
21
21
|
"figure.label.group-more": "Más",
|
|
22
22
|
"figure.label.group-place": "Posición",
|
|
23
|
+
"figure.label.group-position-response": "Posición",
|
|
24
|
+
"figure.label.tab-response": "Adaptable -- cuando cambia el tamaño",
|
|
25
|
+
"figure.label.tab-short-response": "Adaptable",
|
|
23
26
|
"figure.text.sizing-uniform-proposed": "esta figura tiene articulaciones. escalarla con una sola proporción mantiene unidas las articulaciones en cualquier tamaño.",
|
|
24
27
|
"figure.text.sizing-uniform-explained": "escala toda la figura según la media geométrica de los tres tamaños que le asigna un tablero. los pies quedan en el suelo; el espacio que sobra encima de la figura queda vacío.",
|
|
25
28
|
"figure.button.mirror-x": "al otro lado de X",
|
package/translations/ja.json
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"figure.label.group-menu": "その他の操作",
|
|
21
21
|
"figure.label.group-more": "その他",
|
|
22
22
|
"figure.label.group-place": "位置",
|
|
23
|
+
"figure.label.group-position-response": "位置",
|
|
24
|
+
"figure.label.tab-response": "レスポンシブ — サイズが変わるとき",
|
|
25
|
+
"figure.label.tab-short-response": "レスポンシブ",
|
|
23
26
|
"figure.text.sizing-uniform-proposed": "関節のある図形です。一つの比率で拡大すると、どのサイズでも関節が離れません。",
|
|
24
27
|
"figure.text.sizing-uniform-explained": "ボードが与えた 3 軸サイズの幾何平均の比率で図形全体を拡大します。足は床に置き、箱の上の余りは空けたままにします。",
|
|
25
28
|
"figure.button.mirror-x": "X 反対側に",
|
package/translations/ko.json
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"figure.label.group-menu": "다른 동작",
|
|
21
21
|
"figure.label.group-more": "더 보기",
|
|
22
22
|
"figure.label.group-place": "자리",
|
|
23
|
+
"figure.label.group-position-response": "위치",
|
|
24
|
+
"figure.label.tab-response": "반응형 — 크기가 바뀔 때",
|
|
25
|
+
"figure.label.tab-short-response": "반응형",
|
|
23
26
|
"figure.text.sizing-uniform-proposed": "관절이 있는 도형입니다. 한 비율로 키우면 어떤 크기에서도 관절이 벌어지지 않습니다.",
|
|
24
27
|
"figure.text.sizing-uniform-explained": "보드가 준 세 축 크기의 기하평균 비율로 도형 전체를 키웁니다. 발은 바닥에 두고, 상자 위가 남으면 그대로 비워 둡니다.",
|
|
25
28
|
"figure.button.mirror-x": "X 반대편에",
|
|
@@ -75,9 +78,9 @@
|
|
|
75
78
|
"figure.text.gizmo-joint-mimic": "{joint} 관절은 {leader} 관절을 따라 움직입니다. {leader}을(를) 움직이세요.",
|
|
76
79
|
"figure.text.gizmo-joint-fixed-turn": "{joint} 관절은 돌지 않습니다. 돌리려면 움직임 탭에서 회전을 주세요.",
|
|
77
80
|
"figure.text.gizmo-joint-turned-slide": "{joint} 관절은 미끄러지기만 합니다. 이동 손잡이로 움직이세요.",
|
|
78
|
-
"figure.text.gizmo-joint-origin-follows": "{joint} 관절의 위치는 다른 부품을 기준으로 결정됩니다.
|
|
81
|
+
"figure.text.gizmo-joint-origin-follows": "{joint} 관절의 위치는 다른 부품을 기준으로 결정됩니다. 속성 탭의 「자리」에서 바꾸세요.",
|
|
79
82
|
"figure.text.gizmo-joint-off-axis": "{joint} 관절은 {axis}축으로만 돕니다. {axis} 회전 손잡이를 쓰세요.",
|
|
80
|
-
"figure.text.gizmo-size-follows": "{part}의 {axis} 치수는 다른 치수를 따라 정해집니다.
|
|
83
|
+
"figure.text.gizmo-size-follows": "{part}의 {axis} 치수는 다른 치수를 따라 정해집니다. 반응형 탭의 「크기」에서 바꾸세요.",
|
|
81
84
|
"figure.text.gizmo-fastening-places": "{part} 부품은 다른 부품의 면에 부착되어 있습니다. 위치를 직접 변경하려면 부착 관계를 먼저 해제해 주세요.",
|
|
82
85
|
"figure.text.instance-range-can-widen": "지금 부품으로 다시 재면 {ranges} mm 입니다.",
|
|
83
86
|
"figure.text.size-range-too-small": "이 도형은 {axis} 축을 {mm} mm 보다 작게 하면 {part} 부품의 길이가 0 이 되는데, 그보다 작게 놓일 수 있다고 선언되어 있어 이 변경을 할 수 없습니다. 「놓일 수 있는 크기」에서 다시 재기를 누르면 범위가 맞춰집니다.",
|
|
@@ -472,7 +475,7 @@
|
|
|
472
475
|
"figure.text.pick-a-shape-above-to-start": "위에서 도형을 선택해 첫 부품을 추가하세요.",
|
|
473
476
|
"figure.text.placement-explained": "3D 씬에서 객체의 기준면을 정합니다 (바닥: AGV/컨베이어, 천장: OHT/호이스트, 중심: 센서/드론).",
|
|
474
477
|
"figure.text.pose-as-default-explained": "현재 자세를 파라미터의 기본값으로 저장합니다. 값이 지정되지 않으면 도형에 이 자세가 적용됩니다.",
|
|
475
|
-
"figure.text.preview-blocked-by-errors": "형식 오류로 미리보기를 표시할 수 없습니다.
|
|
478
|
+
"figure.text.preview-blocked-by-errors": "형식 오류로 미리보기를 표시할 수 없습니다. 검사 탭에서 사유를 확인하세요.",
|
|
476
479
|
"figure.text.put-on-floor": "지금 위치만 바닥(Y=0)에 맞춥니다. 크기가 바뀌면 따라가지 않습니다. 계속 붙어 있게 하려면 ‘바닥에 붙여 크기 따라가기’를 쓰세요.",
|
|
477
480
|
"figure.text.range-needs-room": "최대가 최소보다 커야 합니다.",
|
|
478
481
|
"figure.text.release-needs-a-save-first": "저장하지 않은 변경이 있습니다. 먼저 저장하세요.",
|
package/translations/ms.json
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"figure.label.group-menu": "Tindakan lain",
|
|
21
21
|
"figure.label.group-more": "Lagi",
|
|
22
22
|
"figure.label.group-place": "Kedudukan",
|
|
23
|
+
"figure.label.group-position-response": "Kedudukan",
|
|
24
|
+
"figure.label.tab-response": "Responsif -- apabila saiz berubah",
|
|
25
|
+
"figure.label.tab-short-response": "Responsif",
|
|
23
26
|
"figure.text.sizing-uniform-proposed": "figura ini mempunyai sendi. membesarkannya dengan satu nisbah memastikan sendi tidak terpisah pada sebarang saiz.",
|
|
24
27
|
"figure.text.sizing-uniform-explained": "membesarkan seluruh figura dengan min geometri tiga saiz yang diberi papan. kaki kekal di lantai; ruang di atas figura dibiarkan kosong.",
|
|
25
28
|
"figure.button.mirror-x": "seberang X",
|
package/translations/zh.json
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"figure.label.group-menu": "更多操作",
|
|
21
21
|
"figure.label.group-more": "更多",
|
|
22
22
|
"figure.label.group-place": "位置",
|
|
23
|
+
"figure.label.group-position-response": "位置",
|
|
24
|
+
"figure.label.tab-response": "响应式 — 尺寸变化时",
|
|
25
|
+
"figure.label.tab-short-response": "响应式",
|
|
23
26
|
"figure.text.sizing-uniform-proposed": "这是带关节的图形。按单一比例放大,任何尺寸下关节都不会分开。",
|
|
24
27
|
"figure.text.sizing-uniform-explained": "按看板给出的三轴尺寸的几何平均比例放大整个图形。底部保持在地面,盒子上方多出的空间保持空着。",
|
|
25
28
|
"figure.button.mirror-x": "X 对侧",
|