@things-factory/figure-ui 10.1.181 → 10.1.185
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 +28 -9
- package/client/modeller/editor.ts +105 -12
- package/client/modeller/fasten-form.ts +207 -0
- package/client/modeller/fasten-geometry.ts +5 -1
- package/client/modeller/figure-facts.ts +4 -35
- package/client/modeller/figure-fasten-dialog.ts +457 -0
- package/client/modeller/figure-fasten-drawing.ts +32 -11
- package/client/modeller/figure-links.ts +56 -300
- package/client/modeller/figure-properties.ts +23 -70
- package/client/modeller/modeller-keys.ts +4 -4
- package/dist-client/modeller/clips.d.ts +2 -2
- package/dist-client/modeller/editor-styles.js +28 -9
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +23 -2
- package/dist-client/modeller/editor.js +121 -12
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +69 -0
- package/dist-client/modeller/fasten-form.js +131 -0
- package/dist-client/modeller/fasten-form.js.map +1 -0
- package/dist-client/modeller/fasten-geometry.d.ts +3 -1
- package/dist-client/modeller/fasten-geometry.js +6 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-facts.d.ts +2 -13
- package/dist-client/modeller/figure-facts.js +2 -29
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +69 -0
- package/dist-client/modeller/figure-fasten-dialog.js +433 -0
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -0
- package/dist-client/modeller/figure-fasten-drawing.d.ts +6 -3
- package/dist-client/modeller/figure-fasten-drawing.js +36 -10
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +14 -38
- package/dist-client/modeller/figure-links.js +58 -301
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +12 -12
- package/dist-client/modeller/figure-properties.js +23 -72
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +4 -4
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/browser/links-page.js +4 -4
- package/test/browser/links.check.mjs +196 -46
- package/test/browser/modeller-page.js +23 -0
- package/test/browser/modeller.check.mjs +19 -20
- package/test/browser/namespace-page.js +2 -29
- package/test/browser/popup-stand-in.js +48 -0
- package/test/browser/product-page.check.mjs +60 -9
- package/test/browser/product-page.js +30 -33
- package/test/browser/property-section.check.mjs +3 -3
- package/test/browser/props.check.mjs +28 -7
- package/test/browser/screen-page.js +7 -2
- package/test/browser/screen.check.mjs +28 -23
- package/test/browser/section-page.js +13 -41
- package/test/browser/section.check.mjs +39 -30
- package/test/fasten-form.test.ts +77 -0
- package/test/fasten-geometry.test.ts +2 -0
- package/test/first-minute.test.ts +7 -4
- package/test/properties.test.ts +11 -19
- package/translations/en.json +5 -12
- package/translations/es.json +5 -12
- package/translations/ja.json +5 -12
- package/translations/ko.json +5 -12
- package/translations/ms.json +5 -12
- package/translations/zh.json +5 -12
- package/client/modeller/mounting-part.ts +0 -15
|
@@ -265,8 +265,8 @@ describe('opening another figure', () => {
|
|
|
265
265
|
|
|
266
266
|
const PANEL = 'figure-modeller-page >>> figure-history-panel'
|
|
267
267
|
const TAB = name => `figure-editor >>> button[tab][aria-label="figure.label.tab-short-${name}"]`
|
|
268
|
-
/** The
|
|
269
|
-
const HISTORY = 'figure-editor >>> button[
|
|
268
|
+
/** The releases open from 「발행」 beside save, the bar's one way to them (user 2026-10-18); pressed only when not open already. */
|
|
269
|
+
const HISTORY = 'figure-editor >>> button[publish]'
|
|
270
270
|
async function openHistory() {
|
|
271
271
|
if ((await read(s => product.deep(s)[0]?.getAttribute('aria-pressed'), HISTORY)) !== 'true') await press(HISTORY, 'the history button')
|
|
272
272
|
}
|
|
@@ -296,7 +296,8 @@ describe('the history panel on the page', () => {
|
|
|
296
296
|
await until(p => product.deep(p)[0]?.shadowRoot.activeElement?.closest('section[release]'), 'the release section focused', PANEL)
|
|
297
297
|
const panel = await read(p => { const panel = product.deep(p)[0]; return { slot: panel.assignedSlot?.name, visible: panel.checkVisibility() } }, PANEL)
|
|
298
298
|
assert.deepEqual(panel, { slot: 'history', visible: true })
|
|
299
|
-
assert.equal(await read(s => product.deep(s)[0]?.getAttribute('aria-pressed'), HISTORY), 'true', 'the bar\'s
|
|
299
|
+
assert.equal(await read(s => product.deep(s)[0]?.getAttribute('aria-pressed'), HISTORY), 'true', 'the bar\'s 「발행」 pressed')
|
|
300
|
+
assert.equal(await read(() => product.deep('figure-editor >>> button[bar-overlay=history]').length), 0, 'no second way to the same panel')
|
|
300
301
|
})
|
|
301
302
|
|
|
302
303
|
test('a released version downloads as a package by its full reference', async () => {
|
|
@@ -310,8 +311,7 @@ describe('the history panel on the page', () => {
|
|
|
310
311
|
const before = await read(() => product.view())
|
|
311
312
|
await press(`${PANEL} >>> button[fit-base]`, 'fit')
|
|
312
313
|
await until(r => product.view().revision === r + 1, 'one step', before.revision)
|
|
313
|
-
await
|
|
314
|
-
await until(() => product.deep('figure-properties >>> div[row] > span')[0]?.textContent === 'figure.text.declared-space-declared', 'the room declared')
|
|
314
|
+
await until(() => !!JSON.parse(product.view().source).occupancy, 'the room declared')
|
|
315
315
|
await openHistory()
|
|
316
316
|
})
|
|
317
317
|
|
|
@@ -534,11 +534,30 @@ describe('the page around the figure: the shell, the dock, the properties', () =
|
|
|
534
534
|
await until(() => product.view().name === 'Door', 'undone')
|
|
535
535
|
})
|
|
536
536
|
|
|
537
|
-
|
|
537
|
+
/*
|
|
538
|
+
Nothing to set up before work goes on (user 2026-10-18, after a walk without it): the tab names the figure and
|
|
539
|
+
where it stands. The room is declared where release asks for it; no face is chosen here.
|
|
540
|
+
*/
|
|
541
|
+
test('the properties tab neither declares the room nor picks a face; where it stands is one step', async () => {
|
|
542
|
+
const shown = await read(() => {
|
|
543
|
+
const root = product.deep('figure-modeller-page >>> figure-properties')[0].shadowRoot
|
|
544
|
+
return {
|
|
545
|
+
headings: [...root.querySelectorAll('section > h3')].map(h => h.textContent.trim()),
|
|
546
|
+
actions: root.querySelectorAll('button[act]').length,
|
|
547
|
+
mounting: !!root.querySelector('div[mounting-face]'),
|
|
548
|
+
selects: [...root.querySelectorAll('select')].map(s => s.previousElementSibling?.textContent.trim())
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
assert.deepEqual(shown, { headings: ['figure.label.group-figure', 'figure.label.group-catalog'], actions: 0, mounting: false, selects: ['figure.label.figure-placement'] })
|
|
538
552
|
const before = await read(() => product.view())
|
|
539
|
-
await
|
|
540
|
-
|
|
541
|
-
|
|
553
|
+
await read(() => {
|
|
554
|
+
const select = product.deep('figure-modeller-page >>> figure-properties')[0].shadowRoot.querySelector('select')
|
|
555
|
+
select.value = 'ceiling'
|
|
556
|
+
select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
557
|
+
})
|
|
558
|
+
await until(r => product.view().revision === r + 1, 'one step', before.revision)
|
|
559
|
+
assert.equal(JSON.parse((await read(() => product.view())).source).placement, 'ceiling')
|
|
560
|
+
assert.equal(JSON.parse((await read(() => product.view())).source).occupancy, undefined, 'no room declared on the way')
|
|
542
561
|
})
|
|
543
562
|
|
|
544
563
|
test('unsaved: closing or reloading the tab is asked about', async () => {
|
|
@@ -769,6 +788,38 @@ describe('the catalogue preview', () => {
|
|
|
769
788
|
})
|
|
770
789
|
})
|
|
771
790
|
|
|
791
|
+
/* A mounting face named already -- by the assistant, or before 2026-10-18 -- is said, and can be taken off; nothing more. */
|
|
792
|
+
describe('a mounting face named already', () => {
|
|
793
|
+
const line = () =>
|
|
794
|
+
read(() => {
|
|
795
|
+
const root = product.deep('figure-modeller-page >>> figure-properties')[0]?.shadowRoot
|
|
796
|
+
const field = root?.querySelector('div[mounting-face]')
|
|
797
|
+
return field ? { label: field.querySelector('label').textContent.trim(), said: field.querySelector('div[row] > span').textContent.trim(), clear: !!field.querySelector('button[quiet][clear-mounting-face]') } : null
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
test('it is one line with the way to take it off, which is one step', async () => {
|
|
801
|
+
await fresh()
|
|
802
|
+
await read(() => product.seed({ id: 'm', type: 'mounted', source: product.mountedSource('floor') }))
|
|
803
|
+
await open('m')
|
|
804
|
+
await until(() => !!product.deep('figure-modeller-page >>> figure-properties')[0]?.shadowRoot?.querySelector('div[mounting-face]'), 'the mounting face said')
|
|
805
|
+
assert.deepEqual(await line(), { label: 'figure.label.mounting-face', said: 'box · figure.label.face-bottom', clear: true })
|
|
806
|
+
const before = await read(() => product.view())
|
|
807
|
+
await press('figure-properties >>> button[clear-mounting-face]', 'take the mounting face off')
|
|
808
|
+
await until(r => product.view().revision === r + 1, 'one step', before.revision)
|
|
809
|
+
assert.equal(JSON.parse((await read(() => product.view())).source).occupancy.contact, undefined, 'taken off')
|
|
810
|
+
assert.ok(JSON.parse((await read(() => product.view())).source).occupancy.bounds, 'the room stays')
|
|
811
|
+
await until(() => !product.deep('figure-modeller-page >>> figure-properties')[0].shadowRoot.querySelector('div[mounting-face]'), 'the line gone')
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
test('on a centred figure it says the face is not checked there', async () => {
|
|
815
|
+
await fresh()
|
|
816
|
+
await read(() => product.seed({ id: 'mc', type: 'mounted', source: product.mountedSource('center') }))
|
|
817
|
+
await open('mc')
|
|
818
|
+
await until(() => !!product.deep('figure-modeller-page >>> figure-properties')[0]?.shadowRoot?.querySelector('div[mounting-face]'), 'the mounting face said')
|
|
819
|
+
assert.equal((await line()).said, 'box · figure.label.face-bottom · figure.text.mounting-face-unchecked-at-center')
|
|
820
|
+
})
|
|
821
|
+
})
|
|
822
|
+
|
|
772
823
|
describe('the page at a narrow width', () => {
|
|
773
824
|
before(async () => {
|
|
774
825
|
await fresh()
|
|
@@ -17,10 +17,10 @@ import { thumbnailHashOf } from 'figure-ui-client/modeller/thumbnail-hash.js'
|
|
|
17
17
|
import { client } from '@operato/graphql'
|
|
18
18
|
import { i18next } from '@operato/i18n'
|
|
19
19
|
import { store } from '@operato/shell'
|
|
20
|
-
import {
|
|
21
|
-
import '
|
|
20
|
+
import { addressController } from '@operato/utils/address.js'
|
|
21
|
+
import './popup-stand-in.js'
|
|
22
22
|
import { html, render } from 'lit'
|
|
23
|
-
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
23
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, proposeFigureOccupancy, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
24
24
|
|
|
25
25
|
const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
|
|
26
26
|
|
|
@@ -30,6 +30,15 @@ export function boxSource(name = 'Probe') {
|
|
|
30
30
|
return serializeFigureAsset(addFigureBox(asset, { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } }))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/* A box whose bottom is named as its mounting face -- the assistant can name one; the properties tab only takes it off. */
|
|
34
|
+
export function mountedSource(placement = 'floor') {
|
|
35
|
+
let a = applyFigureAuthoring(createFigureAsset('Mounted'), { kind: 'set-placement', placement })
|
|
36
|
+
a.palette = { ...a.palette, ...PALETTE }
|
|
37
|
+
a = addFigureBox(a, { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } })
|
|
38
|
+
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a) })
|
|
39
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-mounting-face', part: 'box', face: 'bottom' }))
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
/* A door whose leaf swings on a clip: a proposal that takes the leaf away would stop the clip. */
|
|
34
43
|
export function doorSource() {
|
|
35
44
|
let a = createFigureAsset('Door')
|
|
@@ -129,32 +138,6 @@ function everywhere(selector, root) {
|
|
|
129
138
|
return found
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
/* Popups, drawn as the layout draws them: an overlay of their own in the body, the result given when it leaves. */
|
|
133
|
-
let popups = 0
|
|
134
|
-
setOpenPopupImplementation((template, options = {}) => {
|
|
135
|
-
const name = `$popup-${popups++}`
|
|
136
|
-
const fragment = document.createDocumentFragment()
|
|
137
|
-
render(html`<ox-floating-overlay id=${name} name=${name} .backdrop=${!!options.backdrop} .hovering=${options.hovering} .title=${options.title || ''} .size=${options.size || 'small'} .historical=${false}>${template}</ox-floating-overlay>`, fragment)
|
|
138
|
-
document.body.appendChild(fragment)
|
|
139
|
-
const handle = {
|
|
140
|
-
name,
|
|
141
|
-
close: () => {
|
|
142
|
-
const popup = document.getElementById(name)
|
|
143
|
-
if (!popup) return false
|
|
144
|
-
popup.close()
|
|
145
|
-
return true
|
|
146
|
-
},
|
|
147
|
-
closed: false
|
|
148
|
-
}
|
|
149
|
-
document.addEventListener('overlay-closed', function listener(e) {
|
|
150
|
-
if (e.detail !== name) return
|
|
151
|
-
document.removeEventListener('overlay-closed', listener)
|
|
152
|
-
handle.closed = true
|
|
153
|
-
handle.onclosed && handle.onclosed()
|
|
154
|
-
})
|
|
155
|
-
return handle
|
|
156
|
-
})
|
|
157
|
-
|
|
158
141
|
/* Notices the product raised (`notify`), as the snack bar would show them. */
|
|
159
142
|
const notices = []
|
|
160
143
|
document.addEventListener('notify', e => notices.push({ level: e.detail.level, message: e.detail.message }))
|
|
@@ -162,8 +145,20 @@ document.addEventListener('notify', e => notices.push({ level: e.detail.level, m
|
|
|
162
145
|
/*
|
|
163
146
|
The shell's router, for what the product navigates to: the store's route follows the address, and the modeller page
|
|
164
147
|
is told the figure the address names. Each navigation is kept, with whether the detail preview was still shown then.
|
|
148
|
+
Like the shell's own router (@operato/shell router.js), it follows the entries the address controller commits;
|
|
149
|
+
`navigate` sends no `popstate`. The addresses the shell itself moves to below (`goTo`, `open`) are not the product's.
|
|
165
150
|
*/
|
|
166
151
|
const navigations = []
|
|
152
|
+
let shellMoving = false
|
|
153
|
+
/** The shell's own move to an address: routed by the caller, not kept as the product's navigation. */
|
|
154
|
+
function shellMoves(move) {
|
|
155
|
+
shellMoving = true
|
|
156
|
+
try {
|
|
157
|
+
move()
|
|
158
|
+
} finally {
|
|
159
|
+
shellMoving = false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
167
162
|
const detailShown = () => !!store.getState().layout?.viewparts?.['figure-detail']?.show
|
|
168
163
|
function route() {
|
|
169
164
|
const [, page = '', resourceId] = location.pathname.match(/^\/([^/]*)\/?([^/]*)/) ?? []
|
|
@@ -171,10 +166,11 @@ function route() {
|
|
|
171
166
|
const element = page === 'figure-modeller' ? document.querySelector('figure-modeller-page') : null
|
|
172
167
|
if (element) element.lifecycle = { ...(element.lifecycle ?? {}), resourceId: resourceId || undefined }
|
|
173
168
|
}
|
|
174
|
-
|
|
169
|
+
addressController().subscribe(() => {
|
|
170
|
+
if (shellMoving) return
|
|
175
171
|
navigations.push({ path: location.pathname, entries: history.length, detailShown: detailShown() })
|
|
176
172
|
route()
|
|
177
|
-
})
|
|
173
|
+
}, false)
|
|
178
174
|
|
|
179
175
|
/* The aside bar's part: the detail viewpart drawn while the store shows it. */
|
|
180
176
|
const aside = document.createElement('div')
|
|
@@ -202,6 +198,7 @@ document.head.appendChild(fill)
|
|
|
202
198
|
window.product = {
|
|
203
199
|
boxSource,
|
|
204
200
|
doorSource,
|
|
201
|
+
mountedSource,
|
|
205
202
|
page,
|
|
206
203
|
deep,
|
|
207
204
|
calls: () => calls.map(c => ({ ...c })),
|
|
@@ -227,7 +224,7 @@ window.product = {
|
|
|
227
224
|
route: () => ({ page: store.getState().route?.page, resourceId: store.getState().route?.resourceId }),
|
|
228
225
|
/** The shell moved to a page (an address the product did not navigate to itself). */
|
|
229
226
|
goTo(path) {
|
|
230
|
-
history.pushState({}, '', path)
|
|
227
|
+
shellMoves(() => history.pushState({}, '', path))
|
|
231
228
|
route()
|
|
232
229
|
},
|
|
233
230
|
/** A row on the server, as a figure saved earlier. */
|
|
@@ -250,7 +247,7 @@ window.product = {
|
|
|
250
247
|
shell.replaceChildren(element)
|
|
251
248
|
document.body.replaceChildren(shell)
|
|
252
249
|
}
|
|
253
|
-
history.replaceState({}, '', resourceId ? `/figure-modeller/${resourceId}` : '/figure-modeller')
|
|
250
|
+
shellMoves(() => history.replaceState({}, '', resourceId ? `/figure-modeller/${resourceId}` : '/figure-modeller'))
|
|
254
251
|
store.dispatch({ type: 'UPDATE_PAGE', page: 'figure-modeller', resourceId, params: {} })
|
|
255
252
|
element.lifecycle = { ...(element.lifecycle ?? {}), resourceId }
|
|
256
253
|
element.active = true
|
|
@@ -26,9 +26,9 @@ const sections = () =>
|
|
|
26
26
|
describe('sections an app adds', () => {
|
|
27
27
|
test('only for the figures they are for, after the panel\'s own', async () => {
|
|
28
28
|
await page().evaluate(() => harness.show('EQUIPMENT'))
|
|
29
|
-
assert.deepEqual(await sections(), ['own', 'own'
|
|
29
|
+
assert.deepEqual(await sections(), ['own', 'own'])
|
|
30
30
|
await page().evaluate(() => harness.show('CARRIER'))
|
|
31
|
-
assert.deepEqual(await sections(), ['own', 'own', '
|
|
31
|
+
assert.deepEqual(await sections(), ['own', 'own', 'carrier'])
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test('a section changes the figure by the modeller\'s command', async () => {
|
|
@@ -41,7 +41,7 @@ describe('sections an app adds', () => {
|
|
|
41
41
|
test('a section that fails says so in its place; the others stand', async () => {
|
|
42
42
|
await page().evaluate(() => harness.failOn())
|
|
43
43
|
await page().evaluate(() => harness.show('CARRIER'))
|
|
44
|
-
assert.deepEqual(await sections(), ['own', 'own', '
|
|
44
|
+
assert.deepEqual(await sections(), ['own', 'own', 'carrier', 'broken'])
|
|
45
45
|
assert.match(await page().evaluate(() => document.querySelector('figure-properties').shadowRoot.querySelector('section[data-section="broken"] [role="alert"]').textContent), /figure\.text\.property-section-failed|no table/)
|
|
46
46
|
await page().evaluate(() => harness.failOff())
|
|
47
47
|
})
|
|
@@ -99,7 +99,7 @@ async function tab(name) {
|
|
|
99
99
|
await read(() => document.querySelector('figure-editor').updateComplete)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
describe('the side: six tabs, the view and the
|
|
102
|
+
describe('the side: six tabs, the view and the releases from the bar', () => {
|
|
103
103
|
before(() => mount('rollers'))
|
|
104
104
|
|
|
105
105
|
test('properties, responsive, motion, capability, data and the verdict -- the six tabs, in that order (user 2026-10-17)', async () => {
|
|
@@ -119,13 +119,31 @@ describe('the side: six tabs, the view and the history from the bar', () => {
|
|
|
119
119
|
await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('div[bar-overlay-panel=view]').getBoundingClientRect().height === 0, [], 'closed by Esc')
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
-
test('the
|
|
122
|
+
test('the releases open from 「발행」 beside save, the one way to them, and the view closes for them', async () => {
|
|
123
|
+
assert.equal(await read(() => document.querySelectorAll('figure-editor')[0].shadowRoot.querySelectorAll('button[bar-overlay=history]').length), 0, 'no history button beside it')
|
|
123
124
|
await press('ox-canvas-bar button[bar-overlay=view]', 'the view button')
|
|
124
|
-
await press('ox-canvas-bar button[
|
|
125
|
+
await press('ox-canvas-bar button[publish]', 'the release button')
|
|
125
126
|
const open = await read(() => ['view', 'history'].map(w => document.querySelector('figure-editor').shadowRoot.querySelector(`div[bar-overlay-panel=${w}]`).getBoundingClientRect().height > 0))
|
|
126
127
|
assert.deepEqual(open, [false, true], 'one at a time')
|
|
127
128
|
assert.equal(await read(() => !!document.querySelector('figure-editor').shadowRoot.querySelector('div[bar-overlay-panel=history] slot[name=history]')), true, 'the page\'s history goes in it')
|
|
128
|
-
await press('ox-canvas-bar button[
|
|
129
|
+
await press('ox-canvas-bar button[publish]', 'the release button')
|
|
130
|
+
assert.equal(await read(() => document.querySelector('figure-editor').shadowRoot.querySelector('div[bar-overlay-panel=history]').getBoundingClientRect().height > 0), false, 'a second press closes it')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/* User 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 해주고, 버튼 안에 적절한 아이콘을 추가해주면 혼동되지 않을 것」. */
|
|
134
|
+
test('save and release: one round shape, one height, an icon inside each', async () => {
|
|
135
|
+
const look = await read(() => {
|
|
136
|
+
const bar = document.querySelector('figure-editor').shadowRoot
|
|
137
|
+
return ['save', 'publish'].map(k => {
|
|
138
|
+
const b = bar.querySelector(`ox-canvas-bar button[${k}]`)
|
|
139
|
+
const r = b.getBoundingClientRect()
|
|
140
|
+
return { height: Math.round(r.height), round: parseFloat(getComputedStyle(b).borderTopLeftRadius) >= r.height / 2, icon: b.querySelector('md-icon')?.textContent.trim() }
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
assert.deepEqual(look, [
|
|
144
|
+
{ height: 32, round: true, icon: 'save' },
|
|
145
|
+
{ height: 32, round: true, icon: 'publish' }
|
|
146
|
+
])
|
|
129
147
|
})
|
|
130
148
|
})
|
|
131
149
|
|
|
@@ -150,7 +168,7 @@ describe('a part\'s groups in one order, whatever its state', () => {
|
|
|
150
168
|
await tab('properties')
|
|
151
169
|
})
|
|
152
170
|
|
|
153
|
-
test('a fastened part\'s place is one line and 「바꾸기」: no locked cells, no drawing,
|
|
171
|
+
test('a fastened part\'s place is one line and 「바꾸기」: no locked cells, no drawing; 「바꾸기」 opens the window, not the panel', async () => {
|
|
154
172
|
await pick('mark')
|
|
155
173
|
await tab('properties')
|
|
156
174
|
const place = () =>
|
|
@@ -166,8 +184,11 @@ describe('a part\'s groups in one order, whatever its state', () => {
|
|
|
166
184
|
})
|
|
167
185
|
assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false })
|
|
168
186
|
await press('button[change-fastening]', '「바꾸기」')
|
|
169
|
-
await until(() => !!document.querySelector('figure-
|
|
170
|
-
|
|
187
|
+
await until(() => !!document.querySelector('figure-fasten-dialog'), [], 'the window')
|
|
188
|
+
assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false }, 'the panel still says the result')
|
|
189
|
+
/* Closed with nothing written; another part chosen and back: one line. */
|
|
190
|
+
await page().keyboard.press('Escape')
|
|
191
|
+
await until(() => !document.querySelector('figure-fasten-dialog'), [], 'the window closed')
|
|
171
192
|
await pick('bed')
|
|
172
193
|
await pick('mark')
|
|
173
194
|
assert.equal((await place()).change, true, 'said in one line again')
|
|
@@ -60,11 +60,16 @@ const FIGURES = {
|
|
|
60
60
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'mast', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
61
61
|
return applyFigureAuthoring(a, { kind: 'declare-instance-size', range: { x: { min: 200, max: 200 }, y: { min: 1000, max: 3000 }, z: { min: 300, max: 300 } } })
|
|
62
62
|
},
|
|
63
|
-
/*
|
|
63
|
+
/*
|
|
64
|
+
A block fastened on a base: a part with a fastening to take back, under a figure with no size of its own yet; its
|
|
65
|
+
base's bottom named as the mounting face, which the figure panel offers to take off.
|
|
66
|
+
*/
|
|
64
67
|
stack: () => {
|
|
65
68
|
let a = addFigureBox(named('Stack'), { id: 'base', size: { x: 1000, y: 200, z: 600 }, position: { x: 0, y: 100, z: 0 } })
|
|
66
69
|
a = addFigureBox(a, { id: 'top', size: { x: 200, y: 200, z: 200 }, position: { x: 0, y: 300, z: 0 } })
|
|
67
|
-
|
|
70
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'base', face: 'top' } })
|
|
71
|
+
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a) })
|
|
72
|
+
return applyFigureAuthoring(a, { kind: 'set-mounting-face', part: 'base', face: 'bottom' })
|
|
68
73
|
},
|
|
69
74
|
/* A conveyor that follows its width, a roller every 200 mm, and the space it takes declared: the one figure stretched. */
|
|
70
75
|
stretch: () => {
|
|
@@ -82,10 +82,14 @@ const tabOn = () => read(() => screenCheck.editor().shadowRoot.querySelector('bu
|
|
|
82
82
|
const pressBar = action => press(action, a => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').shadowRoot.querySelector(`button[data-action=${a}]`), action)
|
|
83
83
|
const pressTab = name => press(name, name => screenCheck.editor().shadowRoot.querySelector(`button[tab][aria-label="${name}"]`), name)
|
|
84
84
|
const TAB = name => `figure.label.tab-short-${name}`
|
|
85
|
-
|
|
85
|
+
/*
|
|
86
|
+
The view settings or the releases, opened from the bar over the work (they were tabs once); left open if open. The
|
|
87
|
+
releases open from 「발행」, the bar's one way to them (user 2026-10-18).
|
|
88
|
+
*/
|
|
89
|
+
const OVERLAY_BUTTON = { view: 'button[bar-overlay=view]', history: 'button[publish]' }
|
|
86
90
|
async function openOverlay(which) {
|
|
87
|
-
if (await read(
|
|
88
|
-
await press(`the ${which} button`,
|
|
91
|
+
if (await read(b => screenCheck.editor().shadowRoot.querySelector(`ox-canvas-bar ${b}`)?.getAttribute('aria-pressed') === 'true', OVERLAY_BUTTON[which])) return
|
|
92
|
+
await press(`the ${which} button`, b => screenCheck.editor().shadowRoot.querySelector(`ox-canvas-bar ${b}`), OVERLAY_BUTTON[which])
|
|
89
93
|
await until(`${which} open`, w => screenCheck.editor().shadowRoot.querySelector(`div[bar-overlay-panel=${w}]`)?.getBoundingClientRect().height > 0, which)
|
|
90
94
|
}
|
|
91
95
|
/** A row of the parts list, by the part's name. */
|
|
@@ -870,39 +874,40 @@ describe('the side panel is found as it was left, per part', () => {
|
|
|
870
874
|
})
|
|
871
875
|
})
|
|
872
876
|
|
|
873
|
-
describe('the fastening
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
const
|
|
877
|
+
describe('the fastening window says whether it changes the fastening the part has or fastens it anew', () => {
|
|
878
|
+
const applyWords = () => read(() => screenCheck.deep('[apply]').map(b => b.textContent.trim()))
|
|
879
|
+
/* A chip of the window's list, by its part. */
|
|
880
|
+
const pressTarget = part => press(`${part} in the list`, part => screenCheck.deep('md-filter-chip').find(o => o.getAttribute('data-value') === part), part)
|
|
877
881
|
|
|
878
|
-
test('a part fastened to nothing:
|
|
882
|
+
test('a part fastened to nothing: 「다른 부품에 붙이기」 opens the window; the sentence and what 붙이기 would do once a target is chosen; nothing done until 붙이기', async () => {
|
|
879
883
|
await mount('door')
|
|
880
884
|
await pressRow('leaf')
|
|
881
885
|
await until('leaf chosen', () => screenCheck.editor().selected === 'leaf')
|
|
882
886
|
await press('start fastening', () => screenCheck.deep('button').find(b => b.textContent.trim().endsWith('figure.button.fasten-start')))
|
|
883
|
-
await until('the
|
|
887
|
+
await until('the window', () => screenCheck.deep('figure-fasten-dialog').length === 1)
|
|
884
888
|
const before = await revision()
|
|
885
|
-
await (
|
|
886
|
-
await until('the sentence', () => screenCheck.deep('p[fasten-sentence]').
|
|
889
|
+
await pressTarget('frame')
|
|
890
|
+
await until('the sentence', () => screenCheck.deep('p[fasten-sentence]').some(p => !p.hasAttribute('hint')))
|
|
887
891
|
await until('what 붙이기 would do', () => screenCheck.deep('[preview-words]').length === 1)
|
|
888
|
-
assert.
|
|
892
|
+
assert.deepEqual((await applyWords()).map(w => w.endsWith('figure.button.fasten')), [true], '붙이기 for a new one')
|
|
889
893
|
assert.equal(await revision(), before, 'nothing done until it is pressed')
|
|
890
894
|
})
|
|
891
895
|
|
|
892
|
-
test('a part already fastened:
|
|
896
|
+
test('a part already fastened: one line with 「바꾸기」; opened, its button says 적용 -- another target tried, nothing written until 적용, then one step', async () => {
|
|
893
897
|
await mount('arm')
|
|
894
898
|
await pressRow('forearm')
|
|
895
899
|
await until('forearm chosen', () => screenCheck.editor().selected === 'forearm')
|
|
896
900
|
await until('the sentence', () => screenCheck.deep('p[fasten-sentence]').length === 1)
|
|
897
901
|
assert.equal(await read(() => screenCheck.deep('figure-fasten-drawing').length + screenCheck.deep('select').filter(s => s.closest('section[fastening]')).length), 0, 'one line: no drawing, no target to choose')
|
|
898
902
|
await press('change the fastening', () => screenCheck.deep('button[change-fastening]')[0])
|
|
899
|
-
await until('the
|
|
900
|
-
assert.
|
|
901
|
-
assert.equal(await fastenButton(), 0, 'no 붙이기')
|
|
903
|
+
await until('the window and its drawings', () => screenCheck.deep('figure-fasten-drawing').length === 2)
|
|
904
|
+
assert.deepEqual((await applyWords()).map(w => w.endsWith('figure.button.apply')), [true], '적용, not 붙이기')
|
|
902
905
|
await until('the fastening seen before it is done: its face lit on the canvas', () => !!screenCheck.deep('figure-canvas')[0].highlight)
|
|
903
|
-
assert.equal(await preview(), 0, 'no words for a command no button sends')
|
|
904
906
|
const before = await revision()
|
|
905
|
-
await (
|
|
907
|
+
await pressTarget('upper-arm')
|
|
908
|
+
await until('what 적용 would do', () => screenCheck.deep('[preview-words]').length === 1)
|
|
909
|
+
assert.equal(await revision(), before, 'nothing written while trying')
|
|
910
|
+
await press('apply', () => screenCheck.deep('[apply]')[0])
|
|
906
911
|
await until('one step', was => screenCheck.editor().session.read().revision === was + 1, before)
|
|
907
912
|
assert.equal(await read(() => JSON.parse(screenCheck.editor().session.source()).joints.find(j => j.body1.part === 'forearm').body0.part), 'upper-arm')
|
|
908
913
|
})
|
|
@@ -982,11 +987,11 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
982
987
|
/* First 15 minutes (2026-10-16): release was refused for a volume never declared, said only at the foot of the verdict tab. */
|
|
983
988
|
test('what blocks release is said beside 「발행」, with the way out at hand there', async () => {
|
|
984
989
|
const answer = blocked => read(blocked => product.answer('inspectFigureDraft', () => ({ __typename: 'FigureInspection', blocked, findings: blocked ? [alone.finding('occupancy-undeclared')] : [] })), blocked)
|
|
985
|
-
const bar = () => read(() => [...screenCheck.editor().shadowRoot.querySelectorAll('ox-canvas-bar > button[slot=commit]')].map(b => (b.hasAttribute('bar-overlay') ? '
|
|
990
|
+
const bar = () => read(() => [...screenCheck.editor().shadowRoot.querySelectorAll('ox-canvas-bar > button[slot=commit]')].map(b => (b.hasAttribute('bar-overlay') ? 'overlay' : b.hasAttribute('release-blocked') ? 'blocked' : b.hasAttribute('publish') ? 'publish' : 'save')))
|
|
986
991
|
await answer(true)
|
|
987
992
|
await open('fig-block', 'two', 'probe-block')
|
|
988
993
|
await until('said beside 발행', () => !!screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
|
|
989
|
-
assert.deepEqual(await bar(), ['
|
|
994
|
+
assert.deepEqual(await bar(), ['blocked', 'publish', 'save'])
|
|
990
995
|
await press('what blocks release', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
|
|
991
996
|
await until('the list under the bar', () => !!screenCheck.editor().shadowRoot.querySelector('div[release-blockers] li[finding]'))
|
|
992
997
|
assert.equal(await read(() => !!screenCheck.editor().session.revisionNow().asset.occupancy), false)
|
|
@@ -994,7 +999,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
994
999
|
await press('fit to the parts', () => screenCheck.editor().shadowRoot.querySelector('div[release-blockers] button[act]'))
|
|
995
1000
|
await until('the space declared, in one step', () => !!screenCheck.editor().session.revisionNow().asset.occupancy)
|
|
996
1001
|
await until('nothing blocks: the word is gone', () => !screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
|
|
997
|
-
assert.deepEqual(await bar(), ['
|
|
1002
|
+
assert.deepEqual(await bar(), ['publish', 'save'])
|
|
998
1003
|
await read(() => product.answer('inspectFigureDraft'))
|
|
999
1004
|
await saved()
|
|
1000
1005
|
await open('fig-a')
|
|
@@ -1055,7 +1060,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1055
1060
|
assert.deepEqual(first, { heading: 'figure.label.group-figure', assigned: true, order: [true, true] })
|
|
1056
1061
|
})
|
|
1057
1062
|
|
|
1058
|
-
test('the verdict tab shows the page\'s verdict, and the bar\'s
|
|
1063
|
+
test('the verdict tab shows the page\'s verdict, and the bar\'s 「발행」 the releases', async () => {
|
|
1059
1064
|
const assigned = within =>
|
|
1060
1065
|
read(w => (w ? screenCheck.editor().shadowRoot.querySelector(w) : screenCheck.shownPanel()).querySelector('slot')?.assignedElements().map(e => ({ tag: e.localName, height: Math.round(e.getBoundingClientRect().height) })), within)
|
|
1061
1066
|
await pressTab(TAB('verdict'))
|
|
@@ -1069,7 +1074,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
|
|
|
1069
1074
|
assert.equal(shown[0].tag, 'figure-history-panel')
|
|
1070
1075
|
assert.ok(shown[0].height > 0, 'figure-history-panel is shown')
|
|
1071
1076
|
/* Closed again: it lies over the side, and the checks after this press there. */
|
|
1072
|
-
await press('the
|
|
1077
|
+
await press('the release button', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[publish]'))
|
|
1073
1078
|
})
|
|
1074
1079
|
|
|
1075
1080
|
test('the verdict tab\'s count is the report\'s, measured by the verdict', async () => {
|
|
@@ -5,39 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import './more-open.js'
|
|
7
7
|
import './modeller-page.js'
|
|
8
|
-
import { html, render } from 'lit'
|
|
9
|
-
import { setOpenPopupImplementation } from '@operato/popup'
|
|
10
8
|
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
11
9
|
|
|
12
|
-
/*
|
|
13
|
-
* Popups drawn as the layout draws them -- an overlay of their own in the body, the result given when it leaves -- for
|
|
14
|
-
* the section editor's close question: the page has no layout to append it to.
|
|
15
|
-
*/
|
|
16
|
-
let popups = 0
|
|
17
|
-
setOpenPopupImplementation((template, options = {}) => {
|
|
18
|
-
const name = `$popup-${popups++}`
|
|
19
|
-
const fragment = document.createDocumentFragment()
|
|
20
|
-
render(html`<ox-floating-overlay id=${name} name=${name} .backdrop=${!!options.backdrop} .title=${options.title || ''} .size=${options.size || 'small'} .historical=${false}>${template}</ox-floating-overlay>`, fragment)
|
|
21
|
-
document.body.appendChild(fragment)
|
|
22
|
-
const handle = {
|
|
23
|
-
name,
|
|
24
|
-
close: () => {
|
|
25
|
-
const popup = document.getElementById(name)
|
|
26
|
-
if (!popup) return false
|
|
27
|
-
popup.close()
|
|
28
|
-
return true
|
|
29
|
-
},
|
|
30
|
-
closed: false
|
|
31
|
-
}
|
|
32
|
-
document.addEventListener('overlay-closed', function listener(e) {
|
|
33
|
-
if (e.detail !== name) return
|
|
34
|
-
document.removeEventListener('overlay-closed', listener)
|
|
35
|
-
handle.closed = true
|
|
36
|
-
handle.onclosed && handle.onclosed()
|
|
37
|
-
})
|
|
38
|
-
return handle
|
|
39
|
-
})
|
|
40
|
-
|
|
41
10
|
const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
|
|
42
11
|
|
|
43
12
|
function figure(name) {
|
|
@@ -203,21 +172,24 @@ Object.assign(window.probe, {
|
|
|
203
172
|
ends: Object.fromEntries([...root.querySelectorAll('g[end]')].map(g => [g.getAttribute('end'), { text: words(g.querySelector('text')), typed: g.querySelector('text').hasAttribute('typed') }]))
|
|
204
173
|
}
|
|
205
174
|
},
|
|
206
|
-
/** The fastening
|
|
175
|
+
/** The fastening window's drawings as a person sees them: the ways offered and the one on (none drawn when there is one way). */
|
|
207
176
|
fastenDrawing() {
|
|
208
|
-
const
|
|
209
|
-
if (!
|
|
210
|
-
const
|
|
177
|
+
const hosts = deep('figure-fasten-drawing')
|
|
178
|
+
if (!hosts.length) return null
|
|
179
|
+
const how = hosts.find(h => h.show === 'how')?.shadowRoot
|
|
211
180
|
return {
|
|
212
|
-
modes: [...
|
|
213
|
-
on:
|
|
181
|
+
modes: how ? [...how.querySelectorAll('button[mode]')].map(b => b.getAttribute('mode')) : [],
|
|
182
|
+
on: how?.querySelector('button[mode][on]')?.getAttribute('mode') ?? null
|
|
214
183
|
}
|
|
215
184
|
},
|
|
216
|
-
/** The labels before the fastening
|
|
185
|
+
/** The labels before the fastening window's lists, and the questions it asks. */
|
|
217
186
|
fastenForm() {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
187
|
+
const dialog = deep('figure-fasten-dialog')[0]
|
|
188
|
+
if (!dialog) return []
|
|
189
|
+
return [
|
|
190
|
+
...[...dialog.shadowRoot.querySelectorAll('h4')].map(h => ({ label: words(h) })),
|
|
191
|
+
...deep('select', dialog.shadowRoot).filter(s => s.previousElementSibling).map(s => ({ label: words(s.previousElementSibling), value: s.value }))
|
|
192
|
+
]
|
|
221
193
|
},
|
|
222
194
|
/** The page point of one of a part's buttons in the parts list (`remove-part`, ...). */
|
|
223
195
|
partOp(id, op) {
|