@things-factory/figure-ui 10.1.151 → 10.1.153
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/ai-proposal.ts +14 -13
- package/client/modeller/command-preview-controller.ts +210 -0
- package/client/modeller/editor-styles.ts +307 -0
- package/client/modeller/editor.ts +65 -634
- package/client/modeller/fasten-geometry.ts +32 -0
- package/client/modeller/figure-first-save.ts +9 -1
- package/client/modeller/figure-links.ts +32 -14
- package/client/modeller/mesh-import.ts +0 -10
- package/client/modeller/modeller-keys.ts +60 -0
- package/client/modeller/pane-fold.ts +61 -0
- package/client/modeller/panel-memory.ts +53 -0
- package/client/modeller/refusal-teller.ts +93 -0
- package/client/modeller/refusals.ts +32 -2
- package/dist-client/modeller/ai-proposal.d.ts +6 -5
- package/dist-client/modeller/ai-proposal.js +9 -9
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/command-preview-controller.d.ts +75 -0
- package/dist-client/modeller/command-preview-controller.js +178 -0
- package/dist-client/modeller/command-preview-controller.js.map +1 -0
- package/dist-client/modeller/editor-styles.d.ts +2 -0
- package/dist-client/modeller/editor-styles.js +307 -0
- package/dist-client/modeller/editor-styles.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +5 -56
- package/dist-client/modeller/editor.js +76 -656
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +13 -0
- package/dist-client/modeller/fasten-geometry.js +29 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +1 -0
- package/dist-client/modeller/figure-first-save.js +8 -1
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -2
- package/dist-client/modeller/figure-links.js +35 -14
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +0 -2
- package/dist-client/modeller/mesh-import.js +0 -11
- package/dist-client/modeller/mesh-import.js.map +1 -1
- package/dist-client/modeller/modeller-keys.d.ts +22 -0
- package/dist-client/modeller/modeller-keys.js +43 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -0
- package/dist-client/modeller/pane-fold.d.ts +15 -0
- package/dist-client/modeller/pane-fold.js +60 -0
- package/dist-client/modeller/pane-fold.js.map +1 -0
- package/dist-client/modeller/panel-memory.d.ts +15 -0
- package/dist-client/modeller/panel-memory.js +54 -0
- package/dist-client/modeller/panel-memory.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +4 -28
- package/dist-client/modeller/refusal-teller.d.ts +36 -0
- package/dist-client/modeller/refusal-teller.js +81 -0
- package/dist-client/modeller/refusal-teller.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +12 -1
- package/dist-client/modeller/refusals.js +26 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/browser/build.mjs +7 -2
- package/test/browser/editing.check.mjs +14 -0
- package/test/browser/fix-alignment.check.mjs +112 -0
- package/test/browser/links.check.mjs +69 -13
- package/test/browser/mesh-select.check.mjs +91 -0
- package/test/browser/modeller-page.js +36 -0
- package/test/browser/modeller-revision.check.mjs +18 -6
- package/test/browser/modeller.check.mjs +25 -0
- package/test/browser/part-ids.check.mjs +24 -6
- package/test/browser/parts-page.js +9 -1
- package/test/browser/parts.check.mjs +61 -2
- package/test/browser/places-mesh.check.mjs +25 -9
- package/test/browser/product-page.check.mjs +42 -2
- package/test/browser/row-preview.check.mjs +81 -0
- package/test/browser/screen.check.mjs +35 -1
- package/test/clip-ai-proposal.test.ts +4 -1
- package/test/clip-consent.test.ts +9 -2
- package/test/fields.test.ts +5 -9
- package/test/fix-alignment.test.ts +32 -0
- package/test/layout.test.ts +3 -1
- package/test/mesh-import.test.ts +1 -8
- package/test/parts-wiring.test.ts +1 -5
- package/test/refusals.test.ts +46 -10
- package/test/theme.test.ts +5 -15
- package/translations/en.json +8 -0
- package/translations/es.json +8 -0
- package/translations/ja.json +8 -0
- package/translations/ko.json +8 -0
- package/translations/ms.json +8 -0
- package/translations/zh.json +8 -0
|
@@ -202,6 +202,65 @@ describe('a removal is told: what it let go of as a notice, a refusal as an erro
|
|
|
202
202
|
})
|
|
203
203
|
})
|
|
204
204
|
|
|
205
|
+
describe('a part renamed in its name field: one step, still selected; a name refused is said why', () => {
|
|
206
|
+
before(() => mount('box', 'three'))
|
|
207
|
+
const name = () => read(() => editing.fieldValue('figure.label.name'))
|
|
208
|
+
async function rename(to) {
|
|
209
|
+
await tab('properties')
|
|
210
|
+
const at = await read(() => probe.labelled('figure.label.name'))
|
|
211
|
+
assert.ok(at, 'the name field')
|
|
212
|
+
await page().mouse.click(at.x, at.y, { clickCount: 3 })
|
|
213
|
+
await page().keyboard.type(to)
|
|
214
|
+
await page().keyboard.press('Enter')
|
|
215
|
+
await sleep(400)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
test('renamed: the list has the new name, it stays selected, in one step', async () => {
|
|
219
|
+
await pick('a')
|
|
220
|
+
const before = await revision()
|
|
221
|
+
await rename('left')
|
|
222
|
+
assert.deepEqual(await ids(), ['left', 'b', 'c'])
|
|
223
|
+
assert.equal((await state()).selected, 'left')
|
|
224
|
+
assert.equal(await revision(), before + 1)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('a name another part has is refused: nothing written, the field shows the name it had, and why is said', async () => {
|
|
228
|
+
await pick('left')
|
|
229
|
+
await read(() => editing.told())
|
|
230
|
+
const before = await revision()
|
|
231
|
+
await rename('b')
|
|
232
|
+
assert.equal(await revision(), before, 'nothing written')
|
|
233
|
+
assert.deepEqual(await ids(), ['left', 'b', 'c'])
|
|
234
|
+
assert.equal(await name(), 'left')
|
|
235
|
+
const told = await read(() => editing.told())
|
|
236
|
+
assert.deepEqual(told.map(t => t.level), ['error'], JSON.stringify(told))
|
|
237
|
+
/* Said from the kernel's details (ADR-0127): the page loads no translations, so the words are the key. */
|
|
238
|
+
assert.equal(told[0].message, 'figure.text.part-name-taken')
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('a part in the declared space removed: at once, the space fitted to the parts left, and said so (2026-09-27)', () => {
|
|
243
|
+
test('one step, nothing asked, the space still declared, and one notice that it was fitted again', async () => {
|
|
244
|
+
await mount('box', 'gate')
|
|
245
|
+
await pick('stray')
|
|
246
|
+
const before = await revision()
|
|
247
|
+
const space = () => read(() => JSON.parse(document.querySelector('figure-editor').session.source()).occupancy ?? null)
|
|
248
|
+
const was = await space()
|
|
249
|
+
assert.ok(was, 'declared to begin with')
|
|
250
|
+
await click(await read(() => parts.op('remove-part')), 'remove')
|
|
251
|
+
assert.deepEqual(await ids(), ['housing'])
|
|
252
|
+
assert.equal(await revision(), before + 1, 'one step')
|
|
253
|
+
assert.equal(await read(() => editing.prompt()), null, 'nothing asked')
|
|
254
|
+
const now = await space()
|
|
255
|
+
assert.ok(now, 'the space is still declared')
|
|
256
|
+
assert.notDeepEqual(now, was, 'fitted to the part left')
|
|
257
|
+
const told = await read(() => editing.told())
|
|
258
|
+
assert.deepEqual(told.map(t => t.level), ['info'], JSON.stringify(told))
|
|
259
|
+
/* The page loads no translations: the words are the key. */
|
|
260
|
+
assert.ok(told[0].message.includes('figure.text.removal-space-refitted'), told[0].message)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
205
264
|
describe('the list narrowed: the selected row keeps its name and its buttons in sight', () => {
|
|
206
265
|
before(() => mount('box', 'three'))
|
|
207
266
|
after(() => read(() => parts.narrow(0)))
|
|
@@ -221,11 +280,11 @@ describe('the list narrowed: the selected row keeps its name and its buttons in
|
|
|
221
280
|
})
|
|
222
281
|
|
|
223
282
|
describe('the palette: the starting shapes, locked at the detail level\'s limit', () => {
|
|
224
|
-
test('nine starting shapes, in order; a rounded box or a drum is made from the box', async () => {
|
|
283
|
+
test('nine starting shapes, in order, then a mesh brought from a file; a rounded box or a drum is made from the box', async () => {
|
|
225
284
|
await mount('box', 'three')
|
|
226
285
|
assert.deepEqual(
|
|
227
286
|
(await read(() => parts.palette())).map(b => b.label),
|
|
228
|
-
['cube', 'cylinder', 'sphere', 'frustum', 'polygon', 'pyramid', 'plate', 'wall', 'portal'].map(k => `figure.label.primitive-${k}`)
|
|
287
|
+
['cube', 'cylinder', 'sphere', 'frustum', 'polygon', 'pyramid', 'plate', 'wall', 'portal'].map(k => `figure.label.primitive-${k}`).concat('figure.button.import-mesh')
|
|
229
288
|
)
|
|
230
289
|
})
|
|
231
290
|
|
|
@@ -42,24 +42,38 @@ async function until(fn, args, what, ms = 8000) {
|
|
|
42
42
|
await sleep(50)
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
/*
|
|
46
|
+
* Every element the check reads has drawn what it was last told: the editor, and the places panel and the shared slot
|
|
47
|
+
* layout editor inside it, which draw after the editor does -- awaited until none of them has a draw pending, so a read
|
|
48
|
+
* never meets the one before.
|
|
49
|
+
*/
|
|
50
|
+
const settled = () =>
|
|
51
|
+
read(async () => {
|
|
52
|
+
const deep = (s, root = document) => [...root.querySelectorAll(s)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(s, e.shadowRoot)))
|
|
53
|
+
const drawing = () => [document.querySelector('figure-editor'), ...deep('figure-links, figure-slot-layout-panel, ox-slot-layout-editor')].filter(Boolean)
|
|
54
|
+
for (let round = 0; round < 20; round++) {
|
|
55
|
+
await Promise.all(drawing().map(e => e.updateComplete))
|
|
56
|
+
if (!drawing().some(e => e.isUpdatePending)) return
|
|
57
|
+
}
|
|
58
|
+
})
|
|
46
59
|
|
|
47
60
|
async function mount(asset) {
|
|
48
61
|
await read(text => harness.mount('given', text), serializeFigureAsset(asset))
|
|
49
62
|
await until(() => look.drawn().length > 0, [], 'the board is drawn')
|
|
50
63
|
}
|
|
51
|
-
/** The element `findDeep` finds,
|
|
52
|
-
|
|
64
|
+
/** The element `findDeep` finds, once it is on the page -- waited for, not looked up once -- as a handle to act on. */
|
|
65
|
+
async function handle(selector, options = {}, what = selector) {
|
|
66
|
+
await until((s, o) => !!findDeep(s, o), [selector, options], `${what} on the page`)
|
|
67
|
+
return page().evaluateHandle((s, o) => findDeep(s, o), selector, options)
|
|
68
|
+
}
|
|
53
69
|
async function click(selector, options = {}, what = selector) {
|
|
54
|
-
const element = await handle(selector, options)
|
|
55
|
-
assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
|
|
70
|
+
const element = await handle(selector, options, what)
|
|
56
71
|
await element.evaluate(e => e.scrollIntoView({ block: 'center' }))
|
|
57
72
|
await element.click()
|
|
58
73
|
await settled()
|
|
59
74
|
}
|
|
60
75
|
async function type(selector, options, text) {
|
|
61
76
|
const field = await handle(selector, options)
|
|
62
|
-
assert.ok(await field.evaluate(e => !!e), `${selector} is on the page`)
|
|
63
77
|
await field.evaluate(e => e.scrollIntoView({ block: 'center' }))
|
|
64
78
|
await field.click({ clickCount: 3 })
|
|
65
79
|
await page().keyboard.type(text)
|
|
@@ -68,7 +82,6 @@ async function type(selector, options, text) {
|
|
|
68
82
|
}
|
|
69
83
|
async function choose(selector, options, value) {
|
|
70
84
|
const select = await handle(selector, options)
|
|
71
|
-
assert.ok(await select.evaluate(e => !!e), `${selector} is on the page`)
|
|
72
85
|
await select.select(value)
|
|
73
86
|
await settled()
|
|
74
87
|
}
|
|
@@ -120,7 +133,8 @@ describe('a GLB made into a mesh part', () => {
|
|
|
120
133
|
})
|
|
121
134
|
|
|
122
135
|
test('the file chooser takes GLB files', async () => {
|
|
123
|
-
|
|
136
|
+
const input = await handle('input[mesh-file]', {}, 'the palette\'s file input')
|
|
137
|
+
assert.equal(await input.evaluate(i => i.accept), '.glb,model/gltf-binary')
|
|
124
138
|
})
|
|
125
139
|
|
|
126
140
|
test('a file with one node: uploaded, added at once as one step, named by the file, selected', async () => {
|
|
@@ -142,7 +156,7 @@ describe('a GLB made into a mesh part', () => {
|
|
|
142
156
|
await read(a => meshServer.answer({ upload: a }), crane)
|
|
143
157
|
const start = await state()
|
|
144
158
|
await importFile()
|
|
145
|
-
await until(() =>
|
|
159
|
+
await until(() => (findDeep('figure-mesh-choice')?.shadowRoot?.querySelectorAll('li button').length ?? 0) === 3, [], 'the question and its three choices')
|
|
146
160
|
const choices = await read(() => [...findDeep('figure-mesh-choice').shadowRoot.querySelectorAll('li button')].map(b => b.textContent.replace(/\s+/g, ' ').trim()))
|
|
147
161
|
assert.equal(choices.length, 3)
|
|
148
162
|
assert.match(choices[0], /^figure\.text\.mesh-whole-file/)
|
|
@@ -186,6 +200,7 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
|
|
|
186
200
|
await pick('fork')
|
|
187
201
|
const tab = await read(l => probe.tab(l), 'figure.label.tab-short-capability')
|
|
188
202
|
await page().mouse.click(tab.x, tab.y)
|
|
203
|
+
await until(() => look.tab() === 'figure.label.tab-short-capability', [], 'the capability tab on')
|
|
189
204
|
await settled()
|
|
190
205
|
})
|
|
191
206
|
|
|
@@ -193,6 +208,7 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
|
|
|
193
208
|
assert.equal(await has('figure-slot-layout-panel'), false)
|
|
194
209
|
await click('md-filter-chip[data-value="rule"]', {}, 'the by-a-rule choice')
|
|
195
210
|
await until(() => !!findDeep('ox-slot-layout-editor'), [], 'the editor')
|
|
211
|
+
await settled()
|
|
196
212
|
assert.equal(await has('button[data-action="use-default"]'), false, 'no default: the figure\'s layout is the only one')
|
|
197
213
|
})
|
|
198
214
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { test, describe, before, after } from 'node:test'
|
|
11
11
|
import assert from 'node:assert/strict'
|
|
12
12
|
|
|
13
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
14
|
+
|
|
13
15
|
import { buildPage } from './build.mjs'
|
|
14
16
|
import { openPage } from './serve.mjs'
|
|
15
17
|
|
|
@@ -203,7 +205,9 @@ describe('the first save of a new figure', () => {
|
|
|
203
205
|
await until(() => !product.deep('figure-first-save').length, 'the question closed')
|
|
204
206
|
await press('button[save]', 'save')
|
|
205
207
|
await until(() => product.deep('figure-first-save')[0]?.shadowRoot.querySelector('#type'), 'asked again')
|
|
206
|
-
|
|
208
|
+
/* Asked again, the name is focused again: the browser honours `autofocus` once per document. */
|
|
209
|
+
await until(() => { let a = document.activeElement; while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement; return a?.id === 'name' }, 'the name focused again')
|
|
210
|
+
assert.deepEqual(await firstSave(), { first: 'name', type: 'my-lift', focused: 'name' })
|
|
207
211
|
await read(() => {
|
|
208
212
|
const name = product.deep('figure-first-save >>> #name')[0]
|
|
209
213
|
name.focus()
|
|
@@ -566,6 +570,42 @@ describe('the page around the figure: the shell, the dock, the properties', () =
|
|
|
566
570
|
})
|
|
567
571
|
})
|
|
568
572
|
|
|
573
|
+
describe('a placement the figure\'s parts refuse, chosen on the properties tab (wall placement, 09-25)', () => {
|
|
574
|
+
/* A post standing on the floor's plane by its bottom: a wall takes the back face, so the wall is refused. */
|
|
575
|
+
const post = () => {
|
|
576
|
+
const a = addFigureBox(createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 500, z: 0 } })
|
|
577
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'attach', part: 'post', face: 'bottom', to: { plane: 'mounting-plane' } }))
|
|
578
|
+
}
|
|
579
|
+
const placement = () =>
|
|
580
|
+
read(() => {
|
|
581
|
+
const select = product.deep('figure-modeller-page >>> figure-properties >>> select').find(s => s.previousElementSibling?.textContent.trim() === 'figure.label.figure-placement')
|
|
582
|
+
return select ? select.value : null
|
|
583
|
+
})
|
|
584
|
+
const revision = () => read(() => product.page().session.read().revision)
|
|
585
|
+
|
|
586
|
+
before(async () => {
|
|
587
|
+
await fresh()
|
|
588
|
+
await read(text => product.seed({ id: 'p', type: 'post', source: text }), post())
|
|
589
|
+
await open('p')
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
test('refused: nothing written, the reason said in words beside the tabs, and the list back on the placement it has', async () => {
|
|
593
|
+
assert.equal(await placement(), 'floor')
|
|
594
|
+
const start = await revision()
|
|
595
|
+
await read(() => {
|
|
596
|
+
const select = product.deep('figure-modeller-page >>> figure-properties >>> select').find(s => s.previousElementSibling?.textContent.trim() === 'figure.label.figure-placement')
|
|
597
|
+
select.value = 'wall'
|
|
598
|
+
select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
599
|
+
})
|
|
600
|
+
const said = await until(() => product.deep('figure-modeller-page >>> p[fit-error]')[0]?.textContent.trim(), 'the refusal said')
|
|
601
|
+
/* Said from the kernel's details (ADR-0127): the page loads no translations, so the words are the key with its values. */
|
|
602
|
+
assert.match(said, /^figure\.text\./, said)
|
|
603
|
+
assert.ok(said.includes('post') && said.includes('back'), `names the part and the face the wall takes: ${said}`)
|
|
604
|
+
assert.equal(await revision(), start, 'nothing written')
|
|
605
|
+
assert.equal(await placement(), 'floor', 'the list says the placement the figure has')
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
569
609
|
describe('AI proposals reaching the page', () => {
|
|
570
610
|
before(async () => {
|
|
571
611
|
await fresh()
|
|
@@ -593,7 +633,7 @@ describe('AI proposals reaching the page', () => {
|
|
|
593
633
|
test('a proposal that would stop a clip is refused whole, the clip named', async () => {
|
|
594
634
|
const before = await read(() => product.view())
|
|
595
635
|
const refused = await offer({ steps: [{ kind: 'remove-part', target: 'leaf' }], create: false, context: before.context, revision: before.revision })
|
|
596
|
-
assert.deepEqual({ applied: refused.applied, code: refused.code, clips: refused.clips }, { applied: false, code: 'CLIPS_DROPPED', clips: ['swing'] })
|
|
636
|
+
assert.deepEqual({ applied: refused.applied, code: refused.code, clips: refused.clips?.map(c => c.clip) }, { applied: false, code: 'CLIPS_DROPPED', clips: ['swing'] })
|
|
597
637
|
const after = await read(() => product.view())
|
|
598
638
|
assert.deepEqual({ source: after.source, revision: after.revision }, { source: before.source, revision: before.revision })
|
|
599
639
|
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A row's preview follows the row as it is set: a pitch refused, then 개수를 정함 chosen -- the pitch's refusal goes at
|
|
3
|
+
* once, not after the count is worked out (on :3300, 2026-10-11, it stayed under the count, as if the count were
|
|
4
|
+
* refused), and the count's own result is said.
|
|
5
|
+
*
|
|
6
|
+
* Run: node --test test/browser/row-preview.check.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { test, describe, before, after } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
|
|
11
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
12
|
+
|
|
13
|
+
import { buildPage } from './build.mjs'
|
|
14
|
+
import { openPage } from './serve.mjs'
|
|
15
|
+
|
|
16
|
+
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
17
|
+
|
|
18
|
+
let opened
|
|
19
|
+
before(async () => {
|
|
20
|
+
opened = await openPage(await buildPage('places-mesh-page.js'))
|
|
21
|
+
})
|
|
22
|
+
after(async () => {
|
|
23
|
+
await opened?.close()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const page = () => opened.page
|
|
27
|
+
const read = (fn, ...args) => page().evaluate(fn, ...args)
|
|
28
|
+
async function until(fn, args, what, ms = 8000) {
|
|
29
|
+
const deadline = Date.now() + ms
|
|
30
|
+
for (;;) {
|
|
31
|
+
if (await read(fn, ...args)) return
|
|
32
|
+
if (Date.now() > deadline) assert.fail(`waited for ${what}`)
|
|
33
|
+
await sleep(50)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const settled = () => read(() => document.querySelector('figure-editor').updateComplete)
|
|
37
|
+
const handle = (selector, options = {}) => page().evaluateHandle((s, o) => findDeep(s, o), selector, options)
|
|
38
|
+
async function click(selector, options = {}, what = selector) {
|
|
39
|
+
const element = await handle(selector, options)
|
|
40
|
+
assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
|
|
41
|
+
await element.evaluate(e => e.scrollIntoView({ block: 'center' }))
|
|
42
|
+
await element.click()
|
|
43
|
+
await settled()
|
|
44
|
+
}
|
|
45
|
+
/** What the row's preview says now, line by line. */
|
|
46
|
+
const said = () => read(() => [...(findDeep('section[row]')?.querySelectorAll('[preview-words], [preview-words] li') ?? [])].map(e => e.textContent.trim()))
|
|
47
|
+
|
|
48
|
+
/* A 400 mm board, a 3 mm bin fastened on it: a row of it along X. */
|
|
49
|
+
function board() {
|
|
50
|
+
let a = addFigureBox(createFigureAsset('Board'), { id: 'board', size: { x: 400, y: 50, z: 1000 }, position: { x: 0, y: 25, z: 0 } })
|
|
51
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'board', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
52
|
+
a = addFigureBox(a, { id: 'bin', size: { x: 3, y: 100, z: 100 }, position: { x: 0, y: 100, z: 0 } })
|
|
53
|
+
return applyFigureAuthoring(a, { kind: 'attach', part: 'bin', face: 'bottom', to: { part: 'board', face: 'top' } })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('a row\'s preview follows its basis', () => {
|
|
57
|
+
before(async () => {
|
|
58
|
+
await read(text => harness.mount('given', text), serializeFigureAsset(board()))
|
|
59
|
+
await until(() => look.drawn().length > 0, [], 'the board is drawn')
|
|
60
|
+
const at = await read(() => probe.row('bin'))
|
|
61
|
+
await page().mouse.click(at.x, at.y)
|
|
62
|
+
await until(() => probe.state().selected === 'bin', [], 'bin selected')
|
|
63
|
+
await click('details[row-fold] > summary', {}, 'the row\'s fold')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('a 1 mm pitch: refused, said under the row', async () => {
|
|
67
|
+
const pitch = await handle('section[row] input[type=number]')
|
|
68
|
+
await pitch.evaluate(i => i.scrollIntoView({ block: 'center' }))
|
|
69
|
+
await pitch.click({ clickCount: 3 })
|
|
70
|
+
await page().keyboard.type('1')
|
|
71
|
+
await page().keyboard.press('Tab')
|
|
72
|
+
await until(() => [...(findDeep('section[row]')?.querySelectorAll('[preview-words]') ?? [])].some(e => e.textContent.includes('figure.text.preview-refused')), [], 'the pitch refused')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('개수를 정함 chosen: the pitch\'s refusal is gone at once, and the count is said', async () => {
|
|
76
|
+
await click('section[row] input[type=radio]', { index: 1 }, 'by count')
|
|
77
|
+
/* Read before the count is worked out (it settles 150 ms after it is chosen). */
|
|
78
|
+
assert.ok(!(await said()).some(line => line.includes('preview-refused')), `not the pitch's: ${JSON.stringify(await said())}`)
|
|
79
|
+
await until(() => [...(findDeep('section[row]')?.querySelectorAll('[preview-words] li') ?? [])].some(e => e.textContent.includes('figure.text.row-count-pitch')), [], 'the count said')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -287,6 +287,39 @@ describe('the frame: parts · canvas · one side of six tabs, and the bar over t
|
|
|
287
287
|
})
|
|
288
288
|
})
|
|
289
289
|
|
|
290
|
+
describe('the editor is drawn in the shell\'s tokens (2026-09-27)', () => {
|
|
291
|
+
/* Set apart from any theme, so a colour read back can only have come from the token named. */
|
|
292
|
+
const TOKENS = { '--md-sys-color-secondary-container': 'rgb(1, 2, 3)', '--md-sys-color-on-secondary-container': 'rgb(4, 5, 6)', '--md-sys-color-primary': 'rgb(7, 8, 9)' }
|
|
293
|
+
before(async () => {
|
|
294
|
+
await mount('box')
|
|
295
|
+
await read(tokens => Object.entries(tokens).forEach(([name, value]) => screenCheck.editor().style.setProperty(name, value)), TOKENS)
|
|
296
|
+
})
|
|
297
|
+
after(() => read(tokens => Object.keys(tokens).forEach(name => screenCheck.editor().style.removeProperty(name)), TOKENS))
|
|
298
|
+
|
|
299
|
+
test('a pressed button is drawn in the container the shell matched for both themes', async () => {
|
|
300
|
+
await pressRow('box')
|
|
301
|
+
await press('the move tool', () => screenCheck.editor().shadowRoot.querySelector('button[tool][aria-label="figure.button.gizmo-translate"]'))
|
|
302
|
+
await until('the move tool pressed', () => screenCheck.editor().shadowRoot.querySelector('button[tool][aria-pressed="true"]'))
|
|
303
|
+
/* Off the button: under the pointer it is drawn as hovered. */
|
|
304
|
+
await page().mouse.move(0, 0)
|
|
305
|
+
const drawn = await read(() => {
|
|
306
|
+
const root = screenCheck.editor().shadowRoot
|
|
307
|
+
const tool = getComputedStyle(root.querySelector('button[tool][aria-pressed="true"]'))
|
|
308
|
+
const mode = getComputedStyle(root.querySelector('button[mode][aria-pressed="true"]'))
|
|
309
|
+
return { tool: { background: tool.backgroundColor, color: tool.color }, mode: { color: mode.color, border: mode.borderTopColor } }
|
|
310
|
+
})
|
|
311
|
+
assert.deepEqual(drawn, { tool: { background: 'rgb(1, 2, 3)', color: 'rgb(4, 5, 6)' }, mode: { color: 'rgb(4, 5, 6)', border: 'rgb(7, 8, 9)' } })
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('the side draws no edge of its own and stacks over nothing: the frame draws the line between panes', async () => {
|
|
315
|
+
const aside = await read(() => {
|
|
316
|
+
const style = getComputedStyle(screenCheck.editor().shadowRoot.querySelector('aside'))
|
|
317
|
+
return { zIndex: style.zIndex, borderLeft: style.borderLeftWidth }
|
|
318
|
+
})
|
|
319
|
+
assert.deepEqual(aside, { zIndex: 'auto', borderLeft: '0px' })
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
290
323
|
describe('a part chosen: each tab in its place, the figure-wide tabs give way, the list\'s root goes back', () => {
|
|
291
324
|
before(() => mount('arm'))
|
|
292
325
|
|
|
@@ -648,7 +681,8 @@ describe('the preview: registered as a board registers it, posed and played, the
|
|
|
648
681
|
await mode('preview')
|
|
649
682
|
await until('the preview stands', () => !!screenCheck.deep('figure-preview')[0]?.shadowRoot?.querySelector('div[clips]'))
|
|
650
683
|
const type = await read(() => screenCheck.previewTypeOf(screenCheck.editor().session.read().context))
|
|
651
|
-
|
|
684
|
+
/* The preview made earlier stands hidden and is shown again first: registered once it has the figure as it is now. */
|
|
685
|
+
await until(`registered as ${type}`, type => screenCheck.registered(type), type)
|
|
652
686
|
assert.equal(await read(() => screenCheck.registered('figure-preview-new') && screenCheck.editor().session.read().context !== ''), false, 'not under another figure\'s type')
|
|
653
687
|
await boardsUp()
|
|
654
688
|
await read(() => {
|
|
@@ -10,6 +10,7 @@ import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart,
|
|
|
10
10
|
|
|
11
11
|
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
12
12
|
import { runFigureProposal, takeFigureOffer, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
|
|
13
|
+
import { clipLossOf } from '../client/modeller/refusals.js'
|
|
13
14
|
|
|
14
15
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
15
16
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
@@ -45,7 +46,7 @@ test('a proposal that would stop a clip is refused whole: nothing written, nothi
|
|
|
45
46
|
const out = await runFigureProposal({ steps, context: context!, revision: revision! }, pageOf(s), say)
|
|
46
47
|
assert.equal(out.verdict, 'refused')
|
|
47
48
|
assert.equal(out.errorCode, 'CLIPS_DROPPED')
|
|
48
|
-
assert.
|
|
49
|
+
assert.equal(out.note, say('figure.text.proposal-would-drop-clips', { clips: 'swing' }), 'said as the clips it would stop, not the kernel\'s English')
|
|
49
50
|
assert.equal(s.source(), before, 'not even the rename before it: refused whole')
|
|
50
51
|
assert.equal(s.read().revision, revision)
|
|
51
52
|
assert.equal(s.read().dirty, false, 'nothing to save')
|
|
@@ -93,6 +94,8 @@ test('an AI proposal made while a person\'s consent is held is refused; clips, f
|
|
|
93
94
|
s.withConsent({ dropClips: ['swing/open'], context: context!, revision: revision! }, () => takeFigureOffer(s, detail, { onSavedFigure: false, say }))
|
|
94
95
|
assert.equal(detail.applied, false)
|
|
95
96
|
assert.equal(detail.code, 'CLIPS_DROPPED')
|
|
97
|
+
/* Recorded as figure-model refused it, and read where every refusal is read. */
|
|
98
|
+
assert.deepEqual(clipLossOf(detail)?.map(c => `${c.clip} ${c.why}`), ['swing gone'])
|
|
96
99
|
assert.equal(s.source(), before)
|
|
97
100
|
assert.equal(s.read().revision, revision)
|
|
98
101
|
assert.equal(s.read().dirty, false)
|
|
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
10
10
|
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, parseFigureAsset, serializeFigureAsset, type FigureAsset } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
13
|
+
import { clipLossOf } from '../client/modeller/refusals.js'
|
|
13
14
|
|
|
14
15
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
15
16
|
|
|
@@ -37,9 +38,15 @@ test('refused and named first; agreed, exactly those go in one step; undo brings
|
|
|
37
38
|
const s = createFigureProductSession(store)
|
|
38
39
|
await s.open('doors')
|
|
39
40
|
const revision = s.read().revision
|
|
40
|
-
assert.throws(() => s.command(fixed(s, 'left')), (e: { code?: string; clips?: Array<{ id: string; clip: string; why: string }> }) =>
|
|
41
|
-
e.code === 'CLIPS_DROPPED' && e.clips?.map(c => `${c.clip} ${c.why}`).join() === 'left-swing gone')
|
|
41
|
+
assert.throws(() => s.command(fixed(s, 'left')), (e: { code?: string; details?: { reason?: string; clips?: Array<{ id: string; clip: string; why: string }> } }) =>
|
|
42
|
+
e.code === 'CLIPS_DROPPED' && e.details?.reason === 'clips-dropped' && e.details.clips?.map(c => `${c.clip} ${c.why}`).join() === 'left-swing gone')
|
|
42
43
|
assert.equal(s.read().revision, revision, 'nothing written')
|
|
44
|
+
/* The screen reads the refusal in one place: the clips to ask about, and nothing for any other refusal. */
|
|
45
|
+
const refusalOf = (run: () => unknown) => { try { run() } catch (error) { return error } }
|
|
46
|
+
assert.deepEqual(clipLossOf(refusalOf(() => s.command(fixed(s, 'left'))))?.map(c => `${c.clip} ${c.why}`), ['left-swing gone'])
|
|
47
|
+
const other = refusalOf(() => s.edit({ kind: 'remove-part', target: 'nowhere' }))
|
|
48
|
+
assert.ok((other as { code?: string })?.code, 'another refusal')
|
|
49
|
+
assert.equal(clipLossOf(other), null)
|
|
43
50
|
assert.deepEqual(clipsOf(s), ['left-swing', 'right-swing'])
|
|
44
51
|
const ids = ['left-swing/left-open']
|
|
45
52
|
const at = () => ({ context: s.read().context!, revision: s.read().revision! })
|
package/test/fields.test.ts
CHANGED
|
@@ -76,12 +76,8 @@ test('pitch 는 한 곳에서 하나의 값이고, 늘 set-repeat 로 바꿔 최
|
|
|
76
76
|
}
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/* Which of the two places it is said from the refusal's details (refusals.test.ts makes both with the kernel, ADR-0127). */
|
|
85
|
-
assert.ok(moved.includes('placedRefusalOf(error, partLabelOf(id)'), moved)
|
|
86
|
-
})
|
|
87
|
-
|
|
79
|
+
/*
|
|
80
|
+
* A gizmo drag on a part a rule places is written as the rule's nudge, growing by a ratio on a scale drag
|
|
81
|
+
* (modeller.check.mjs, the gizmo on a part placed by worked-out rules); one a joint places is refused in words from the
|
|
82
|
+
* kernel's details (editing.check.mjs).
|
|
83
|
+
*/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 정렬·간격 고치기 (fasten-geometry `insideAlignmentOf`): the alignment that brings a part inside the face it is fastened
|
|
3
|
+
* on -- the least move on each side it is out, the centre when it is wider -- or why there is nothing to move.
|
|
4
|
+
*/
|
|
5
|
+
import { test } from 'node:test'
|
|
6
|
+
import assert from 'node:assert/strict'
|
|
7
|
+
|
|
8
|
+
import { insideAlignmentOf, type FaceView } from '../client/modeller/fasten-geometry.js'
|
|
9
|
+
|
|
10
|
+
/* A 400 × 1000 top, a 100 × 100 footprint on it; edges offered on X, a distance only on Z. */
|
|
11
|
+
const view = (part = { x: 100, y: 100, z: 100 }): FaceView => ({ axes: ['x', 'z'], face: { x: 400, z: 1000 }, part, edges: { x: true, y: true, z: false } })
|
|
12
|
+
|
|
13
|
+
test('out past the low edge: moved to that edge from inside, the other axis left as it was', () => {
|
|
14
|
+
const fixed = insideAlignmentOf(view(), { x: 'mm', z: 'centre' }, { x: -1000 })
|
|
15
|
+
assert.deepEqual(fixed, { align: { x: 'min', z: 'centre' }, alignMm: { x: -1000 }, moved: ['x'] })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('out past the high edge where edges are not offered: that edge as a distance', () => {
|
|
19
|
+
const fixed = insideAlignmentOf(view(), { x: 'centre', z: 'mm' }, { z: 900 })
|
|
20
|
+
assert.deepEqual(fixed, { align: { x: 'centre', z: 'mm' }, alignMm: { z: 450 }, moved: ['z'] })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('wider than the face: centred, the most of it on the face', () => {
|
|
24
|
+
const fixed = insideAlignmentOf(view({ x: 600, y: 100, z: 100 }), { x: 'max' }, {})
|
|
25
|
+
assert.deepEqual(fixed, { align: { x: 'centre' }, alignMm: {}, moved: ['x'] })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('nothing of it out: inside -- what is out is fastened on it; no face size: unmeasured', () => {
|
|
29
|
+
assert.deepEqual(insideAlignmentOf(view(), { x: 'max', z: 'centre' }, {}), { cannot: 'inside' })
|
|
30
|
+
assert.deepEqual(insideAlignmentOf(undefined, {}, {}), { cannot: 'unmeasured' })
|
|
31
|
+
assert.deepEqual(insideAlignmentOf({ ...view(), face: { x: 400 } }, {}, {}), { cannot: 'unmeasured' })
|
|
32
|
+
})
|
package/test/layout.test.ts
CHANGED
|
@@ -16,7 +16,9 @@ const renderBody = editor.slice(editor.indexOf(' render() {'), editor.indexOf('
|
|
|
16
16
|
test('띠 — 칸 접기 · 되돌리기 · 전체 보기 · 저장 상태 · 저장, 말은 번역에서', () => {
|
|
17
17
|
/* The bar itself -- its folds, undo · redo · fit, the status and save -- is the browser check's (test/browser/screen.check.mjs). */
|
|
18
18
|
assert.doesNotMatch(renderBody, /[가-힣]/, '편집기의 화면 말에 한국어가 박혀 있지 않다')
|
|
19
|
-
|
|
19
|
+
/* The editor's words, and those of the parts it hands its work to. */
|
|
20
|
+
const said = [editor, ...['pane-fold', 'refusal-teller', 'command-preview-controller'].map(name => read(`../client/modeller/${name}.ts`))].join('\n')
|
|
21
|
+
const keys = [...said.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
20
22
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
|
|
21
23
|
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
22
24
|
for (const key of keys) assert.ok(table[key], `${lang}: ${key}`)
|
package/test/mesh-import.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import assert from 'node:assert/strict'
|
|
|
8
8
|
import { readFileSync } from 'node:fs'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
|
|
11
|
-
import { meshChoicesOf,
|
|
11
|
+
import { meshChoicesOf, meshPartOf, type MeshUpload } from '../client/modeller/mesh-import.js'
|
|
12
12
|
|
|
13
13
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
14
14
|
const HASH = 'c'.repeat(64)
|
|
@@ -59,13 +59,6 @@ test('a node: its own bounds and counts, standing and turned where the file had
|
|
|
59
59
|
assert.throws(() => meshPartOf(upload, { node: 3 }, 'x'), 'a node that cannot be a part is not made one')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
test("a part's id from a node's name: letter-led and clean, another number when taken", () => {
|
|
63
|
-
assert.equal(meshPartIdOf('Hoist Unit #2', new Set()), 'Hoist-Unit-2')
|
|
64
|
-
assert.equal(meshPartIdOf('2nd beam', new Set()), 'nd-beam')
|
|
65
|
-
assert.equal(meshPartIdOf('###', new Set()), 'mesh')
|
|
66
|
-
assert.equal(meshPartIdOf('hoist', new Set(['hoist', 'hoist-2'])), 'hoist-3')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
62
|
/* The screen itself -- the palette's button, the upload, the question, the step added -- is driven in browser/places-mesh.check.mjs. */
|
|
70
63
|
test('the mesh words are in every language', () => {
|
|
71
64
|
for (const lang of ['en', 'es', 'ja', 'ko', 'ms', 'zh']) {
|
|
@@ -11,7 +11,6 @@ import { readFileSync } from 'node:fs'
|
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
|
|
13
13
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
14
|
-
const editor = read('../client/modeller/editor.ts')
|
|
15
14
|
|
|
16
15
|
/*
|
|
17
16
|
* Each event the panel sends answered by the editor -- picked, moved, duplicated, removed, added, hidden -- as one step
|
|
@@ -37,10 +36,7 @@ test('팔레트는 시작 모양만 낸다 — 둥근 상자와 통은 상자의
|
|
|
37
36
|
})
|
|
38
37
|
|
|
39
38
|
test('부품 이름은 figure-model 의 rename-part 로 바꾸고, 거절은 까닭별로 사람의 말로 한다', () => {
|
|
40
|
-
|
|
41
|
-
assert.match(editor, /this\.selected = name/, '바꾼 이름의 부품이 그대로 골라져 있다')
|
|
42
|
-
/* The reason is the kernel's refusal (ADR-0127): the screen does not check the name again (refusals.test.ts makes all three). */
|
|
43
|
-
assert.match(editor, /this\.error = partNameRefusalOf\(error, /)
|
|
39
|
+
/* Renamed in one step and still selected, and a refused name said from the kernel's details: parts.check.mjs. */
|
|
44
40
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
|
|
45
41
|
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
46
42
|
for (const key of ['figure.text.part-name-taken', 'figure.text.part-name-invalid']) assert.ok(table[key]?.includes('{name}'), `${lang}: ${key}`)
|
package/test/refusals.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
13
13
|
import * as fm from '@hatiolab/figure-model'
|
|
14
14
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
15
15
|
import {
|
|
16
|
+
clipLossOf,
|
|
16
17
|
editRefusalOf,
|
|
17
18
|
inDeclaredRoom,
|
|
18
19
|
inputRangeRefusalOf,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
placedRefusalOf,
|
|
23
24
|
placementRefusalOf,
|
|
24
25
|
recipeRefusalOf,
|
|
26
|
+
refusalRecordOf,
|
|
25
27
|
removalNoticeOf,
|
|
26
28
|
repeatPitchRefusalOf,
|
|
27
29
|
sizeRangeRefusalOf
|
|
@@ -133,11 +135,7 @@ test('공간 선언에 든 부품 — 한 번 눌러 지우고, 공간은 남은
|
|
|
133
135
|
const after = fm.editFigurePart(a, { kind: 'remove-part', target: 'stray', refitRoom: true })
|
|
134
136
|
assert.equal(after.document.model.nodes.some(n => n.id === 'stray'), false)
|
|
135
137
|
assert.ok(after.occupancy?.bounds, '공간은 남은 부품으로 다시 선언된다')
|
|
136
|
-
|
|
137
|
-
const remove = editor.slice(editor.indexOf(' private removePart('), editor.indexOf(' private toggleHiddenPart('))
|
|
138
|
-
assert.match(remove, /if \(!refitRoom && inDeclaredRoom\(error\)\) \{\s*this\.removePart\(id, true\)/, '거절 없이 곧바로 다시 맞춰 지운다')
|
|
139
|
-
assert.match(remove, /figure\.text\.removal-space-refitted/, '그렇다고 한 줄')
|
|
140
|
-
assert.doesNotMatch(remove, /this\.retry = \{/, '묻거나 권하지 않는다')
|
|
138
|
+
/* Removed in one click, nothing asked, the space fitted to the parts left and said so: parts.check.mjs. */
|
|
141
139
|
assert.ok(ko['figure.text.removal-space-refitted'])
|
|
142
140
|
})
|
|
143
141
|
|
|
@@ -153,11 +151,7 @@ test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평
|
|
|
153
151
|
assert.ok(said.includes('post (bottom)') && said.includes('벽 기반') && said.includes('back'), said)
|
|
154
152
|
assert.equal(editRefusalOf(refused, say), said, '편집 거절의 말도 같다')
|
|
155
153
|
assert.equal(placementRefusalOf(refusalOf(() => act(box(), declaring({ min: 0, max: 4000 }))), say), null)
|
|
156
|
-
/* The placement list sends its command through the page
|
|
157
|
-
const page = readFileSync(fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url)), 'utf8')
|
|
158
|
-
assert.ok(page.includes('editRefusalOf(error, (key, params) => i18next.t(key, params))'), '페이지 경로도 사람의 말로')
|
|
159
|
-
const props = readFileSync(fileURLToPath(new URL('../client/modeller/figure-properties.ts', import.meta.url)), 'utf8')
|
|
160
|
-
assert.ok(props.includes('select.value = current'), '거절되면 목록이 원래 배치로 돌아온다')
|
|
154
|
+
/* The placement list sends its command through the page: said there in words, and the list back on the placement it has (product-page.check.mjs). */
|
|
161
155
|
})
|
|
162
156
|
|
|
163
157
|
test('범위 밖 값은 받을 수 있는 범위를 숫자로 말한다 — 커널의 영어가 아니다 (Figure 레인 :3300, 2026-09-26)', () => {
|
|
@@ -210,12 +204,54 @@ test('줄이 한도를 넘으면 몇 개가 되는지와 한도를 말한다 —
|
|
|
210
204
|
assert.ok(bodies.includes('19,402') && bodies.includes('10,000'), bodies)
|
|
211
205
|
})
|
|
212
206
|
|
|
207
|
+
test('일상에서 코드로만 보이던 거절 넷 — 줄이 안 들어감 · 같은 축의 줄 안의 줄 · 기준면이 걸린 부품의 줄 · 쓰인 부품 id (:3300, 2026-10-11)', () => {
|
|
208
|
+
const shelf = () => {
|
|
209
|
+
let a = fm.addFigureBox(fm.createFigureAsset('Shelf'), { id: 'board', size: { x: 400, y: 50, z: 1000 }, position: { x: 0, y: 0, z: 0 } })
|
|
210
|
+
a = fm.addFigureBox(a, { id: 'bin', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 0, z: 0 } })
|
|
211
|
+
a = act(a, { kind: 'attach', part: 'board', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
212
|
+
return act(a, { kind: 'attach', part: 'bin', face: 'bottom', to: { part: 'board', face: 'top' } })
|
|
213
|
+
}
|
|
214
|
+
/* Five 100 mm bins asked for on 400 mm. */
|
|
215
|
+
const fit = editRefusalOf(refusalOf(() => act(shelf(), { kind: 'make-repeat', part: 'bin', axis: 'x', pitch: 1, count: 5 })), keyed)
|
|
216
|
+
assert.match(fit, /row-does-not-fit .*"count":"5".*"length":"400".*"extent":"100"/)
|
|
217
|
+
/* A cap on the roller made a row along X, as the roller's row is. */
|
|
218
|
+
let capped = fm.addFigureBox(conveyor(), { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
219
|
+
capped = act(capped, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'roller', face: 'top' } })
|
|
220
|
+
assert.match(editRefusalOf(refusalOf(() => act(capped, { kind: 'make-repeat', part: 'cap', axis: 'x', pitch: 10 })), keyed), /row-same-axis .*"part":"cap","outer":"roller","axis":"X"/)
|
|
221
|
+
/* The bin's top holds the slot layout: made a row, the layout would stand on no one bin. */
|
|
222
|
+
const slotted = act(shelf(), {
|
|
223
|
+
kind: 'set-slot-layout',
|
|
224
|
+
label: 'Bin',
|
|
225
|
+
role: 'seat',
|
|
226
|
+
anchors: [{ id: 'bin', part: 'bin', face: 'top' }],
|
|
227
|
+
layout: { tiers: [{ tierNo: 1, anchorId: 'bin', mount: 'ON', position: { x: 0, y: 0, z: 0 }, rotation: 0, layout: { pattern: 'SINGLE', slot: { allowed: { width: 100, depth: 100, height: 100 }, accepts: ['TOTE'] } } }] }
|
|
228
|
+
})
|
|
229
|
+
const held = editRefusalOf(refusalOf(() => act(slotted, { kind: 'make-repeat', part: 'bin', axis: 'x', pitch: 150 })), say)
|
|
230
|
+
assert.ok(held.includes('bin') && held.includes('자리 배치') && !held.includes('SURFACE_PROVENANCE'), held)
|
|
231
|
+
/* A new part named as the row's part is (its copies drawn under ids of their own): the same words as a rename. */
|
|
232
|
+
assert.match(editRefusalOf(refusalOf(() => fm.addFigureBox(conveyor(), { id: 'roller', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } })), keyed), /part-name-taken .*"name":"roller"/)
|
|
233
|
+
})
|
|
234
|
+
|
|
213
235
|
test('기즈모로 옮길 수 없는 부품 — 관절이 자리를 정하는지, 붙은 면이 정하는지', () => {
|
|
214
236
|
assert.match(placedRefusalOf(refusalOf(() => act(conveyor(), { kind: 'nudge-part', part: 'roller', by: { x: 10 } })), '롤러', keyed)!, /gizmo-joint-places .*"part":"롤러"/)
|
|
215
237
|
assert.match(placedRefusalOf(refusalOf(() => act(conveyor(), { kind: 'scale-position', part: 'roller', axis: 'x' })), '롤러', keyed)!, /gizmo-fastening-places .*"part":"롤러"/)
|
|
216
238
|
assert.equal(placedRefusalOf(refusalOf(() => act(conveyor(), { kind: 'set-repeat', part: 'frame', pitch: 10 })), '프레임', keyed), null, '다른 EDIT_TARGET 은 이 말이 아니다')
|
|
217
239
|
})
|
|
218
240
|
|
|
241
|
+
test('옮겨 적은 거절 — 기록은 커널의 거절과 같은 모양이라 같은 말로 읽힌다 (ADR-0127)', () => {
|
|
242
|
+
const a = act(fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } }), {
|
|
243
|
+
kind: 'attach',
|
|
244
|
+
part: 'post',
|
|
245
|
+
face: 'bottom',
|
|
246
|
+
to: { plane: 'mounting-plane' }
|
|
247
|
+
})
|
|
248
|
+
const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
|
|
249
|
+
const record = refusalRecordOf(refused)
|
|
250
|
+
assert.ok(record.details, 'its details kept')
|
|
251
|
+
assert.equal(editRefusalOf(record, say), editRefusalOf(refused, say))
|
|
252
|
+
assert.equal(clipLossOf(record), null, 'no clips to ask about')
|
|
253
|
+
})
|
|
254
|
+
|
|
219
255
|
test('부품 이름 — 이름 규칙은 커널 한 벌이고, 화면은 거절의 값으로 말한다 (ADR-0127)', () => {
|
|
220
256
|
const rename = (asset: FigureAsset, target: string, name: string) => refusalOf(() => fm.editFigurePart(asset, { kind: 'rename-part', target, name } as never))
|
|
221
257
|
assert.match(partNameRefusalOf(rename(conveyor(), 'frame', '1x'), keyed)!, /part-name-invalid .*"name":"1x"/)
|