@things-factory/figure-ui 10.1.181 → 10.1.183
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-fasten-dialog.ts +442 -0
- package/client/modeller/figure-fasten-drawing.ts +32 -11
- package/client/modeller/figure-links.ts +56 -300
- package/client/modeller/modeller-keys.ts +4 -4
- 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-fasten-dialog.d.ts +64 -0
- package/dist-client/modeller/figure-fasten-dialog.js +418 -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 +15 -39
- package/dist-client/modeller/figure-links.js +58 -301
- package/dist-client/modeller/figure-links.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 +5 -5
- package/test/browser/links-page.js +4 -4
- package/test/browser/links.check.mjs +184 -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 +4 -3
- package/test/browser/product-page.js +19 -32
- package/test/browser/props.check.mjs +28 -7
- 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/translations/en.json +4 -3
- package/translations/es.json +4 -3
- package/translations/ja.json +4 -3
- package/translations/ko.json +4 -3
- package/translations/ms.json +4 -3
- package/translations/zh.json +4 -3
|
@@ -17,8 +17,8 @@ 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
23
|
import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
24
24
|
|
|
@@ -129,32 +129,6 @@ function everywhere(selector, root) {
|
|
|
129
129
|
return found
|
|
130
130
|
}
|
|
131
131
|
|
|
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
132
|
/* Notices the product raised (`notify`), as the snack bar would show them. */
|
|
159
133
|
const notices = []
|
|
160
134
|
document.addEventListener('notify', e => notices.push({ level: e.detail.level, message: e.detail.message }))
|
|
@@ -162,8 +136,20 @@ document.addEventListener('notify', e => notices.push({ level: e.detail.level, m
|
|
|
162
136
|
/*
|
|
163
137
|
The shell's router, for what the product navigates to: the store's route follows the address, and the modeller page
|
|
164
138
|
is told the figure the address names. Each navigation is kept, with whether the detail preview was still shown then.
|
|
139
|
+
Like the shell's own router (@operato/shell router.js), it follows the entries the address controller commits;
|
|
140
|
+
`navigate` sends no `popstate`. The addresses the shell itself moves to below (`goTo`, `open`) are not the product's.
|
|
165
141
|
*/
|
|
166
142
|
const navigations = []
|
|
143
|
+
let shellMoving = false
|
|
144
|
+
/** The shell's own move to an address: routed by the caller, not kept as the product's navigation. */
|
|
145
|
+
function shellMoves(move) {
|
|
146
|
+
shellMoving = true
|
|
147
|
+
try {
|
|
148
|
+
move()
|
|
149
|
+
} finally {
|
|
150
|
+
shellMoving = false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
167
153
|
const detailShown = () => !!store.getState().layout?.viewparts?.['figure-detail']?.show
|
|
168
154
|
function route() {
|
|
169
155
|
const [, page = '', resourceId] = location.pathname.match(/^\/([^/]*)\/?([^/]*)/) ?? []
|
|
@@ -171,10 +157,11 @@ function route() {
|
|
|
171
157
|
const element = page === 'figure-modeller' ? document.querySelector('figure-modeller-page') : null
|
|
172
158
|
if (element) element.lifecycle = { ...(element.lifecycle ?? {}), resourceId: resourceId || undefined }
|
|
173
159
|
}
|
|
174
|
-
|
|
160
|
+
addressController().subscribe(() => {
|
|
161
|
+
if (shellMoving) return
|
|
175
162
|
navigations.push({ path: location.pathname, entries: history.length, detailShown: detailShown() })
|
|
176
163
|
route()
|
|
177
|
-
})
|
|
164
|
+
}, false)
|
|
178
165
|
|
|
179
166
|
/* The aside bar's part: the detail viewpart drawn while the store shows it. */
|
|
180
167
|
const aside = document.createElement('div')
|
|
@@ -227,7 +214,7 @@ window.product = {
|
|
|
227
214
|
route: () => ({ page: store.getState().route?.page, resourceId: store.getState().route?.resourceId }),
|
|
228
215
|
/** The shell moved to a page (an address the product did not navigate to itself). */
|
|
229
216
|
goTo(path) {
|
|
230
|
-
history.pushState({}, '', path)
|
|
217
|
+
shellMoves(() => history.pushState({}, '', path))
|
|
231
218
|
route()
|
|
232
219
|
},
|
|
233
220
|
/** A row on the server, as a figure saved earlier. */
|
|
@@ -250,7 +237,7 @@ window.product = {
|
|
|
250
237
|
shell.replaceChildren(element)
|
|
251
238
|
document.body.replaceChildren(shell)
|
|
252
239
|
}
|
|
253
|
-
history.replaceState({}, '', resourceId ? `/figure-modeller/${resourceId}` : '/figure-modeller')
|
|
240
|
+
shellMoves(() => history.replaceState({}, '', resourceId ? `/figure-modeller/${resourceId}` : '/figure-modeller'))
|
|
254
241
|
store.dispatch({ type: 'UPDATE_PAGE', page: 'figure-modeller', resourceId, params: {} })
|
|
255
242
|
element.lifecycle = { ...(element.lifecycle ?? {}), resourceId }
|
|
256
243
|
element.active = true
|
|
@@ -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')
|
|
@@ -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) {
|
|
@@ -521,40 +521,32 @@ describe('the inside\'s drawing in the 단면 group', () => {
|
|
|
521
521
|
})
|
|
522
522
|
})
|
|
523
523
|
|
|
524
|
-
describe('the fastening drawing in the fastening
|
|
524
|
+
describe('the fastening drawing in the fastening window', () => {
|
|
525
525
|
const labels = () => read(() => probe.fastenForm().map(s => s.label))
|
|
526
|
-
const
|
|
527
|
-
/* A fastening the part has is said in one line; 「바꾸기」 opens the
|
|
528
|
-
const change = () =>
|
|
526
|
+
const targets = () => read(() => probe.fastenTargets())
|
|
527
|
+
/* A fastening the part has is said in one line; 「바꾸기」 opens the window on it (user 2026-10-17). */
|
|
528
|
+
const change = async () => {
|
|
529
|
+
await press('button[change-fastening]')
|
|
530
|
+
await until(() => !!probe.fastenTargets(), 'the fastening window')
|
|
531
|
+
}
|
|
529
532
|
|
|
530
|
-
test('the part\'s own face and the facing are not asked:
|
|
533
|
+
test('the part\'s own face and the facing are not asked: what, which face and how, the face and the way drawn', async () => {
|
|
531
534
|
await mount('arm')
|
|
532
535
|
await pick('forearm')
|
|
533
536
|
await change()
|
|
534
537
|
const asked = await labels()
|
|
535
|
-
assert.
|
|
538
|
+
assert.deepEqual(asked.slice(0, 3), ['figure.label.fasten-what', 'figure.label.fasten-face-on-part', 'figure.label.fasten-how'])
|
|
536
539
|
assert.ok(!asked.includes('figure.label.my-face') && !asked.includes('figure.label.facing'), `asked: ${asked}`)
|
|
537
540
|
assert.ok(await read(() => !!probe.fastenDrawing()), 'the drawing in their place')
|
|
538
541
|
})
|
|
539
542
|
|
|
540
|
-
test('a fastened part: a face of the target pressed
|
|
543
|
+
test('a fastened part: a face of the target pressed, flush pressed, the footprint dragged -- tried, nothing written', async () => {
|
|
541
544
|
const start = await state()
|
|
542
545
|
const was = await read(() => probe.fastening('forearm'))
|
|
543
546
|
const face = was.body0.face === 'front' ? 'right' : 'front'
|
|
544
547
|
await press(`[face="${face}"]`, { within: 'figure-fasten-drawing' })
|
|
545
|
-
await until(r => probe.state().revision === r + 1, 'one step', start.revision)
|
|
546
|
-
assert.equal((await read(() => probe.fastening('forearm'))).body0.face, face)
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
test('a fastened part: flush pressed is one step, and the drawing shows it', async () => {
|
|
550
|
-
const start = await state()
|
|
551
548
|
await press('button[mode="flush"]', { within: 'figure-fasten-drawing' })
|
|
552
|
-
await until(
|
|
553
|
-
assert.equal(await read(() => probe.fastenMode()), 'flush')
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
test('a fastened part: the footprint dragged on the face and let go is one step', async () => {
|
|
557
|
-
const start = await state()
|
|
549
|
+
await until(() => probe.fastenMode() === 'flush', 'the drawing shows flush')
|
|
558
550
|
const foot = await element('g[footprint] rect', { within: 'figure-fasten-drawing' })
|
|
559
551
|
const at = await foot.evaluate(e => {
|
|
560
552
|
e.scrollIntoView({ block: 'center' })
|
|
@@ -565,25 +557,46 @@ describe('the fastening drawing in the fastening form', () => {
|
|
|
565
557
|
await page().mouse.down()
|
|
566
558
|
for (let i = 1; i <= 5; i++) await page().mouse.move(at.x + 6 * i, at.y)
|
|
567
559
|
await page().mouse.up()
|
|
560
|
+
await sleep(300)
|
|
561
|
+
assert.equal((await state()).revision, start.revision, 'nothing written while trying')
|
|
562
|
+
assert.deepEqual(await read(() => probe.fastening('forearm')), was)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('a fastened part: 적용 writes all of it as one step', async () => {
|
|
566
|
+
const start = await state()
|
|
567
|
+
const was = await read(() => probe.fastening('forearm'))
|
|
568
|
+
const face = was.body0.face === 'front' ? 'right' : 'front'
|
|
569
|
+
await press('[apply]', { within: 'figure-fasten-dialog' })
|
|
568
570
|
await until(r => probe.state().revision === r + 1, 'one step', start.revision)
|
|
569
571
|
await sleep(300)
|
|
570
572
|
assert.equal((await state()).revision, start.revision + 1, 'and only one')
|
|
573
|
+
const now = await read(() => probe.fastening('forearm'))
|
|
574
|
+
assert.deepEqual([now.body0.face, now.origin?.facing], [face, 'flush'])
|
|
571
575
|
})
|
|
572
576
|
|
|
573
577
|
test('fastened to the plane: the plane is named by the placement, chosen, and set on only', async () => {
|
|
574
578
|
await mount('rollers')
|
|
575
579
|
await pick('bed')
|
|
576
580
|
await change()
|
|
577
|
-
const list = await
|
|
578
|
-
|
|
579
|
-
assert.
|
|
581
|
+
const list = await targets()
|
|
582
|
+
const plane = list.find(t => t.target === '__plane')
|
|
583
|
+
assert.deepEqual([plane.selected, plane.text], [true, 'figure.label.resize-place-floor'])
|
|
584
|
+
assert.equal((await labels())[1], 'figure.label.fasten-face-on-plane', 'the face asked on the floor, not of a part')
|
|
580
585
|
assert.deepEqual((await read(() => probe.fastenDrawing())).modes, [], 'one way only: no choice offered')
|
|
581
586
|
})
|
|
582
587
|
|
|
588
|
+
/* The window is a popup over the work: closed, nothing written, before another part is picked. */
|
|
589
|
+
const cancel = async () => {
|
|
590
|
+
await press('[cancel]', { within: 'figure-fasten-dialog' })
|
|
591
|
+
await until(() => !probe.fastenTargets(), 'the window closed')
|
|
592
|
+
}
|
|
593
|
+
|
|
583
594
|
test('a part that turns in place may have its centre on the face; a part that does not, not', async () => {
|
|
595
|
+
await cancel()
|
|
584
596
|
await pick('roller')
|
|
585
597
|
await change()
|
|
586
598
|
assert.deepEqual((await read(() => probe.fastenDrawing())).modes, ['meet', 'flush', 'centre'])
|
|
599
|
+
await cancel()
|
|
587
600
|
await pick('mark')
|
|
588
601
|
await change()
|
|
589
602
|
assert.deepEqual((await read(() => probe.fastenDrawing())).modes, ['meet', 'flush'])
|
|
@@ -592,19 +605,15 @@ describe('the fastening drawing in the fastening form', () => {
|
|
|
592
605
|
test('a new fastening waits for 붙이기; the part\'s own face follows the target\'s face and how', async () => {
|
|
593
606
|
await mount('door')
|
|
594
607
|
await pick('leaf')
|
|
595
|
-
await press('button[
|
|
596
|
-
await until(() => probe.
|
|
597
|
-
|
|
598
|
-
const deep = (root = document) => [...root.querySelectorAll('select')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
599
|
-
return deep().find(s => s.previousElementSibling?.textContent?.trim() === 'figure.label.target-part') ?? null
|
|
600
|
-
})
|
|
601
|
-
await targetSelect.select('frame')
|
|
608
|
+
await press('button[fasten-start]')
|
|
609
|
+
await until(() => !!probe.fastenTargets(), 'the window')
|
|
610
|
+
await press('md-filter-chip[data-value="frame"]', { within: 'figure-fasten-dialog' })
|
|
602
611
|
await until(() => !!probe.fastenDrawing(), 'the fastening drawing')
|
|
603
612
|
const start = await state()
|
|
604
613
|
await press('[face="front"]', { within: 'figure-fasten-drawing' })
|
|
605
614
|
await sleep(300)
|
|
606
615
|
assert.equal((await state()).revision, start.revision, 'nothing sent before 붙이기')
|
|
607
|
-
await press('
|
|
616
|
+
await press('[apply]', { within: 'figure-fasten-dialog', has: 'figure.button.fasten' })
|
|
608
617
|
await until(r => probe.state().revision === r + 1, 'one step', start.revision)
|
|
609
618
|
const joint = await read(() => probe.fastening('leaf'))
|
|
610
619
|
assert.deepEqual(joint.body0, { part: 'frame', face: 'front' })
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The fastening as it is chosen in the window (`fasten-form.ts`): opened on what the part has, aimed, changed by the
|
|
3
|
+
* drawing, and the one attach command worked out from it -- the command the window applies, its preview shows and the
|
|
4
|
+
* panel's 「정렬 고치기」 sends.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
|
|
9
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset } from '@hatiolab/figure-model'
|
|
10
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
11
|
+
|
|
12
|
+
import { aimFasten, fastenChanged, fastenDetailWordsOf, fastenFormOf, fastenPlanOf, fastenSentenceOf, fastenTargetsOf, freshFasten } from '../client/modeller/fasten-form.ts'
|
|
13
|
+
import { revisionOf } from './revision-of.ts'
|
|
14
|
+
|
|
15
|
+
const box = (asset: FigureAsset, id: string, [x, y, z]: number[], at = [0, 0, 0]) => addFigureBox(asset, { id, size: { x, y, z }, position: { x: at[0]!, y: at[1]!, z: at[2]! } })
|
|
16
|
+
const fasten = (asset: FigureAsset, part: string, to: string, more = {}) =>
|
|
17
|
+
applyFigureAuthoring(asset, { kind: 'attach', part, face: 'bottom', to: to === 'plane' ? { plane: 'mounting-plane' } : { part: to, face: 'top' }, ...more } as never)
|
|
18
|
+
|
|
19
|
+
/* A frame on the floor, a roller on the frame 300 mm off its centre, a loose lid beside them. */
|
|
20
|
+
function conveyor() {
|
|
21
|
+
let a = fasten(box(createFigureAsset('Conveyor'), 'frame', [2000, 100, 600], [0, 50, 0]), 'frame', 'plane')
|
|
22
|
+
a = fasten(box(a, 'roller', [60, 60, 560], [300, 130, 0]), 'roller', 'frame', { align: { x: { mm: 300 } } })
|
|
23
|
+
return revisionOf(box(a, 'lid', [100, 20, 100], [1500, 10, 0]))
|
|
24
|
+
}
|
|
25
|
+
const say = (key: string, params: Record<string, string> = {}) => `${key}(${Object.entries(params).map(([k, v]) => `${k}=${v}`).join(',')})`
|
|
26
|
+
|
|
27
|
+
test('opened on what the part has; a loose part opens on nothing chosen', () => {
|
|
28
|
+
const r = conveyor()
|
|
29
|
+
const own = fastenFormOf(r, 'roller')
|
|
30
|
+
assert.deepEqual([own.target, own.targetFace, own.toPlane, own.align.x, own.alignMm.x], ['frame', 'top', false, 'mm', 300])
|
|
31
|
+
assert.deepEqual(fastenFormOf(r, 'lid'), freshFasten())
|
|
32
|
+
assert.equal(fastenPlanOf(r, 'lid', fastenFormOf(r, 'lid')).action, undefined, 'nothing to apply until something is chosen')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('the one it has, unchanged, comes to the attach that replaces it with the same place across the face', () => {
|
|
36
|
+
const r = conveyor()
|
|
37
|
+
const plan = fastenPlanOf(r, 'roller', fastenFormOf(r, 'roller'))
|
|
38
|
+
assert.deepEqual(plan.action, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' }, facing: 'meet', twist: 0, align: { x: { mm: 300 } }, replace: true })
|
|
39
|
+
assert.deepEqual(plan.face, { part: 'frame', face: 'top' }, 'the face it meets, lit while tried')
|
|
40
|
+
assert.equal(fastenSentenceOf('roller', fastenFormOf(r, 'roller'), plan, '바닥', say), 'figure.text.fasten-sentence-meet(part=roller,face=figure.label.face-bottom(),to=frame의 figure.label.face-top())')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('aimed at a part, the loose lid keeps its place across the face; aimed at the plane, nothing is said of a part face', () => {
|
|
44
|
+
const r = conveyor()
|
|
45
|
+
const onFrame = aimFasten(r, 'lid', fastenFormOf(r, 'lid'), { part: 'frame' })
|
|
46
|
+
assert.deepEqual([onFrame.target, onFrame.toPlane, onFrame.align.x, onFrame.alignMm.x], ['frame', false, 'mm', 1500])
|
|
47
|
+
const plan = fastenPlanOf(r, 'lid', onFrame)
|
|
48
|
+
assert.equal((plan.action as { replace: boolean }).replace, false, 'a new fastening')
|
|
49
|
+
const onPlane = fastenPlanOf(r, 'lid', aimFasten(r, 'lid', onFrame, { plane: true }))
|
|
50
|
+
assert.deepEqual((onPlane.action as { to: unknown }).to, { plane: 'mounting-plane' })
|
|
51
|
+
assert.equal(onPlane.face, undefined)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('the drawing\'s changes: another face aims again, the way, the gap, a place across the face', () => {
|
|
55
|
+
const r = conveyor()
|
|
56
|
+
let f = fastenFormOf(r, 'roller')
|
|
57
|
+
f = fastenChanged(r, 'roller', f, { mode: 'flush' })
|
|
58
|
+
assert.equal(f.facing, 'flush')
|
|
59
|
+
f = fastenChanged(r, 'roller', f, { gap: 40 })
|
|
60
|
+
assert.deepEqual([f.gap, f.gapKept], [40, undefined])
|
|
61
|
+
f = fastenChanged(r, 'roller', f, { axis: 'x', how: 'max' })
|
|
62
|
+
assert.equal(f.align.x, 'max')
|
|
63
|
+
assert.deepEqual(fastenDetailWordsOf(f, say), ['figure.text.fasten-summary-gap(mm=40)'])
|
|
64
|
+
const action = fastenPlanOf(r, 'roller', f).action as { facing: string; gap: number; align: unknown }
|
|
65
|
+
assert.deepEqual([action.facing, action.gap, action.align], ['flush', 40, { x: 'max' }])
|
|
66
|
+
f = fastenChanged(r, 'roller', f, { targetFace: 'front' })
|
|
67
|
+
assert.equal(fastenPlanOf(r, 'roller', f).targetFace, 'front')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('the target list: every other part, the one hanging from this part listed as a loop', () => {
|
|
71
|
+
const r = conveyor()
|
|
72
|
+
assert.deepEqual(fastenTargetsOf(r, 'frame'), [
|
|
73
|
+
{ part: 'roller', loops: true },
|
|
74
|
+
{ part: 'lid', loops: false }
|
|
75
|
+
])
|
|
76
|
+
assert.deepEqual(fastenTargetsOf(r, 'lid').map(t => t.part), ['frame', 'roller'])
|
|
77
|
+
})
|
|
@@ -87,6 +87,8 @@ test('the drawing\'s two names do not lie over each other (user 2026-10-16: 「r
|
|
|
87
87
|
assert.deepEqual(thin.face, { x: 20, y: 55 })
|
|
88
88
|
assert.equal(thin.part.inside, false)
|
|
89
89
|
assert.ok(thin.part.y > 63 + 84 || thin.part.y < 63, 'outside the part')
|
|
90
|
+
/* As tall as the face, almost: under the part is the face's edge, and the name goes under that line (2026-10-18). */
|
|
91
|
+
assert.ok(thin.part.y - 11 >= face.y + face.h + 2, `clear of the face's edge: ${thin.part.y}`)
|
|
90
92
|
/* A wide block: its name in it. */
|
|
91
93
|
const wide = fastenLabelsOf(face, { x: 120, y: 80, w: 100, h: 40 }, 'lid', 180)
|
|
92
94
|
assert.deepEqual(wide.part, { x: 170, y: 104, inside: true })
|
|
@@ -52,11 +52,14 @@ test('the start card and the figure panel choose the placement as the figure\'s
|
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
/* B.1 and the designer's items (2026-09-27): each target's panel is found as it was left; the fastening form says which it is. */
|
|
55
|
-
test('the fastening
|
|
56
|
-
/*
|
|
55
|
+
test('the fastening window says by its button whether it changes the one the part has or fastens anew', () => {
|
|
56
|
+
/* 붙이기 for a new one, 적용 for the one it has, nothing more said: the browser check's (test/browser/screen.check.mjs). Here, the words. */
|
|
57
57
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
58
|
-
assert.equal(ko['figure.
|
|
59
|
-
assert.equal(ko['figure.button.
|
|
58
|
+
assert.equal(ko['figure.button.fasten'], '붙이기')
|
|
59
|
+
assert.equal(ko['figure.button.apply'], '적용')
|
|
60
|
+
assert.equal(ko['figure.text.fasten-new'], undefined)
|
|
61
|
+
/* The questions point at the result sentence's pieces: which part, which of its faces, how it is set there (user 2026-10-18). */
|
|
62
|
+
assert.deepEqual(['figure.label.fasten-what', 'figure.label.fasten-face-on-part', 'figure.label.fasten-face-on-plane', 'figure.label.fasten-how'].map(k => ko[k]), ['어느 부품에', '{part}의 어느 면에', '{plane}의 어디에', '어떻게 놓을지'])
|
|
60
63
|
})
|
|
61
64
|
|
|
62
65
|
test('a measure that follows something says what, where the row once said 「다른 규칙에 연결됨」 (user 2026-10-16)', () => {
|
package/translations/en.json
CHANGED
|
@@ -530,7 +530,6 @@
|
|
|
530
530
|
"figure.label.fastened-to": "fastened to",
|
|
531
531
|
"figure.label.not-fastened": "not fastened",
|
|
532
532
|
"figure.label.my-face": "my face",
|
|
533
|
-
"figure.label.target-part": "target part",
|
|
534
533
|
"figure.label.target-face": "target face",
|
|
535
534
|
"figure.label.stands-on-plane": "stand on the mounting plane",
|
|
536
535
|
"figure.label.facing-meet": "faces meet",
|
|
@@ -560,6 +559,10 @@
|
|
|
560
559
|
"figure.text.preview-face-unknown": "This face cannot be shown on the canvas.",
|
|
561
560
|
"figure.text.target-would-loop": "{part} — attached to {mine}, cannot be chosen",
|
|
562
561
|
"figure.label.fasten-detail": "Details",
|
|
562
|
+
"figure.label.fasten-what": "Which part",
|
|
563
|
+
"figure.label.fasten-face-on-part": "Which face of {part}",
|
|
564
|
+
"figure.label.fasten-face-on-plane": "Where on the {plane}",
|
|
565
|
+
"figure.label.fasten-how": "How it sits",
|
|
563
566
|
"figure.label.facing": "How faces meet",
|
|
564
567
|
"figure.text.fasten-summary-gap": "gap {mm} mm",
|
|
565
568
|
"figure.text.fasten-summary-gap-worked-out": "gap worked out by a formula",
|
|
@@ -877,10 +880,8 @@
|
|
|
877
880
|
"figure.button.show-model-settings": "Show model settings",
|
|
878
881
|
"figure.text.new-figure-name": "New figure",
|
|
879
882
|
"figure.text.fasten-editing": "Editing the current attachment: its {face} on {target}",
|
|
880
|
-
"figure.text.fasten-new": "Attach anew",
|
|
881
883
|
"figure.text.fasten-start": "Attach this part to a face on another part so they keep their relationship when either changes.",
|
|
882
884
|
"figure.button.fasten-start": "Attach to another part",
|
|
883
|
-
"figure.label.target-part-placeholder": "Choose a part",
|
|
884
885
|
"figure.text.fasten-target-first": "Choose the part to attach to first.",
|
|
885
886
|
"figure.text.fasten-edit-on-plane": "This part is connected to the model's placement reference. Detach it before attaching it to another part.",
|
|
886
887
|
"figure.button.align-to-floor": "set on the floor",
|