@things-factory/figure-ui 10.1.147 → 10.1.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/client/bootstrap.ts +10 -2
  2. package/client/graphql/index.ts +20 -0
  3. package/client/modeller/canvas.ts +6 -2
  4. package/client/modeller/carrier-declaration.ts +63 -0
  5. package/client/modeller/editor.ts +62 -27
  6. package/client/modeller/figure-carrier.ts +117 -0
  7. package/client/modeller/figure-cavity-drawing.ts +3 -9
  8. package/client/modeller/figure-first-save.ts +4 -7
  9. package/client/modeller/figure-history-panel.ts +2 -3
  10. package/client/modeller/figure-links.ts +46 -60
  11. package/client/modeller/figure-material-panel.ts +4 -14
  12. package/client/modeller/figure-mesh-choice.ts +68 -0
  13. package/client/modeller/figure-parts.ts +23 -1
  14. package/client/modeller/figure-preview.ts +2 -2
  15. package/client/modeller/figure-properties.ts +5 -3
  16. package/client/modeller/figure-section-editor.ts +12 -33
  17. package/client/modeller/figure-section-panel.ts +6 -7
  18. package/client/modeller/figure-slot-layout-panel.ts +208 -0
  19. package/client/modeller/figure-verdict.ts +21 -31
  20. package/client/modeller/first-save.ts +23 -0
  21. package/client/modeller/links.ts +52 -1
  22. package/client/modeller/material-edit.ts +38 -0
  23. package/client/modeller/maturity.ts +28 -0
  24. package/client/modeller/mesh-import.ts +77 -0
  25. package/client/modeller/modeller-route.ts +9 -0
  26. package/client/modeller/mounting-part.ts +15 -0
  27. package/client/modeller/newest-answer.ts +21 -0
  28. package/client/modeller/preview-model.ts +5 -0
  29. package/client/modeller/refusals.ts +168 -82
  30. package/client/modeller/section-controls.ts +25 -1
  31. package/client/modeller/section-drawing.ts +43 -1
  32. package/client/modeller/slot-layout-host.ts +140 -0
  33. package/client/pages/figure-modeller-page.ts +6 -5
  34. package/client/viewparts/detail-route.ts +7 -0
  35. package/client/viewparts/figure-detail-dock.ts +1 -3
  36. package/dist-client/bootstrap.d.ts +1 -1
  37. package/dist-client/bootstrap.js +9 -2
  38. package/dist-client/bootstrap.js.map +1 -1
  39. package/dist-client/graphql/index.d.ts +5 -0
  40. package/dist-client/graphql/index.js +19 -0
  41. package/dist-client/graphql/index.js.map +1 -1
  42. package/dist-client/modeller/canvas.d.ts +2 -0
  43. package/dist-client/modeller/canvas.js +10 -2
  44. package/dist-client/modeller/canvas.js.map +1 -1
  45. package/dist-client/modeller/carrier-declaration.d.ts +31 -0
  46. package/dist-client/modeller/carrier-declaration.js +53 -0
  47. package/dist-client/modeller/carrier-declaration.js.map +1 -0
  48. package/dist-client/modeller/editor.d.ts +13 -0
  49. package/dist-client/modeller/editor.js +62 -27
  50. package/dist-client/modeller/editor.js.map +1 -1
  51. package/dist-client/modeller/figure-carrier.d.ts +26 -0
  52. package/dist-client/modeller/figure-carrier.js +123 -0
  53. package/dist-client/modeller/figure-carrier.js.map +1 -0
  54. package/dist-client/modeller/figure-cavity-drawing.js +3 -11
  55. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  56. package/dist-client/modeller/figure-first-save.d.ts +4 -0
  57. package/dist-client/modeller/figure-first-save.js +5 -8
  58. package/dist-client/modeller/figure-first-save.js.map +1 -1
  59. package/dist-client/modeller/figure-history-panel.js +2 -4
  60. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  61. package/dist-client/modeller/figure-links.d.ts +4 -1
  62. package/dist-client/modeller/figure-links.js +47 -57
  63. package/dist-client/modeller/figure-links.js.map +1 -1
  64. package/dist-client/modeller/figure-material-panel.d.ts +0 -1
  65. package/dist-client/modeller/figure-material-panel.js +4 -14
  66. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  67. package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
  68. package/dist-client/modeller/figure-mesh-choice.js +75 -0
  69. package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
  70. package/dist-client/modeller/figure-parts.d.ts +1 -1
  71. package/dist-client/modeller/figure-parts.js +22 -0
  72. package/dist-client/modeller/figure-parts.js.map +1 -1
  73. package/dist-client/modeller/figure-preview.js +2 -2
  74. package/dist-client/modeller/figure-preview.js.map +1 -1
  75. package/dist-client/modeller/figure-properties.d.ts +2 -1
  76. package/dist-client/modeller/figure-properties.js +5 -3
  77. package/dist-client/modeller/figure-properties.js.map +1 -1
  78. package/dist-client/modeller/figure-section-editor.d.ts +2 -2
  79. package/dist-client/modeller/figure-section-editor.js +12 -33
  80. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  81. package/dist-client/modeller/figure-section-panel.js +6 -7
  82. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  83. package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
  84. package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
  85. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
  86. package/dist-client/modeller/figure-verdict.d.ts +1 -1
  87. package/dist-client/modeller/figure-verdict.js +19 -37
  88. package/dist-client/modeller/figure-verdict.js.map +1 -1
  89. package/dist-client/modeller/first-save.d.ts +10 -0
  90. package/dist-client/modeller/first-save.js +16 -0
  91. package/dist-client/modeller/first-save.js.map +1 -1
  92. package/dist-client/modeller/links.d.ts +26 -1
  93. package/dist-client/modeller/links.js +43 -0
  94. package/dist-client/modeller/links.js.map +1 -1
  95. package/dist-client/modeller/material-edit.d.ts +11 -0
  96. package/dist-client/modeller/material-edit.js +28 -0
  97. package/dist-client/modeller/material-edit.js.map +1 -0
  98. package/dist-client/modeller/maturity.d.ts +15 -0
  99. package/dist-client/modeller/maturity.js +23 -0
  100. package/dist-client/modeller/maturity.js.map +1 -1
  101. package/dist-client/modeller/mesh-import.d.ts +47 -0
  102. package/dist-client/modeller/mesh-import.js +47 -0
  103. package/dist-client/modeller/mesh-import.js.map +1 -0
  104. package/dist-client/modeller/modeller-route.d.ts +5 -0
  105. package/dist-client/modeller/modeller-route.js +9 -0
  106. package/dist-client/modeller/modeller-route.js.map +1 -1
  107. package/dist-client/modeller/mounting-part.d.ts +14 -0
  108. package/dist-client/modeller/mounting-part.js +10 -0
  109. package/dist-client/modeller/mounting-part.js.map +1 -0
  110. package/dist-client/modeller/newest-answer.d.ts +12 -0
  111. package/dist-client/modeller/newest-answer.js +21 -0
  112. package/dist-client/modeller/newest-answer.js.map +1 -0
  113. package/dist-client/modeller/preview-model.d.ts +2 -0
  114. package/dist-client/modeller/preview-model.js +4 -0
  115. package/dist-client/modeller/preview-model.js.map +1 -1
  116. package/dist-client/modeller/refusals.d.ts +26 -5
  117. package/dist-client/modeller/refusals.js +184 -86
  118. package/dist-client/modeller/refusals.js.map +1 -1
  119. package/dist-client/modeller/section-controls.d.ts +17 -1
  120. package/dist-client/modeller/section-controls.js +18 -0
  121. package/dist-client/modeller/section-controls.js.map +1 -1
  122. package/dist-client/modeller/section-drawing.d.ts +24 -1
  123. package/dist-client/modeller/section-drawing.js +42 -0
  124. package/dist-client/modeller/section-drawing.js.map +1 -1
  125. package/dist-client/modeller/slot-layout-host.d.ts +81 -0
  126. package/dist-client/modeller/slot-layout-host.js +105 -0
  127. package/dist-client/modeller/slot-layout-host.js.map +1 -0
  128. package/dist-client/pages/figure-modeller-page.js +5 -4
  129. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  130. package/dist-client/tsconfig.tsbuildinfo +1 -1
  131. package/dist-client/viewparts/detail-route.d.ts +5 -0
  132. package/dist-client/viewparts/detail-route.js +8 -0
  133. package/dist-client/viewparts/detail-route.js.map +1 -0
  134. package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
  135. package/dist-client/viewparts/figure-detail-dock.js +1 -3
  136. package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
  137. package/package.json +9 -8
  138. package/test/align.test.ts +0 -9
  139. package/test/browser/assistant-stand-in.js +32 -0
  140. package/test/browser/build.check.mjs +77 -0
  141. package/test/browser/build.mjs +31 -6
  142. package/test/browser/editing-page.js +224 -0
  143. package/test/browser/editing.check.mjs +550 -0
  144. package/test/browser/links-page.js +113 -0
  145. package/test/browser/links.check.mjs +568 -0
  146. package/test/browser/modeller.check.mjs +21 -0
  147. package/test/browser/parts-page.js +120 -0
  148. package/test/browser/parts.check.mjs +328 -0
  149. package/test/browser/places-mesh-page.js +36 -0
  150. package/test/browser/places-mesh.check.mjs +239 -0
  151. package/test/browser/product-page.check.mjs +682 -0
  152. package/test/browser/product-page.js +276 -0
  153. package/test/browser/screen-page.js +112 -0
  154. package/test/browser/screen-product-page.js +62 -0
  155. package/test/browser/screen.check.mjs +1003 -0
  156. package/test/browser/section-page.js +256 -0
  157. package/test/browser/section.check.mjs +719 -0
  158. package/test/browser/serve.mjs +20 -12
  159. package/test/carrier-declaration.test.ts +69 -0
  160. package/test/cavity-drawing.test.ts +2 -18
  161. package/test/clip-ai-proposal.test.ts +1 -5
  162. package/test/clip-consent.test.ts +0 -5
  163. package/test/clips.test.ts +0 -7
  164. package/test/command-preview.test.ts +0 -68
  165. package/test/controls.test.ts +0 -9
  166. package/test/detail-dock.test.ts +6 -25
  167. package/test/face-mapping.test.ts +1 -20
  168. package/test/fasten-drawing.test.ts +0 -23
  169. package/test/fields.test.ts +6 -33
  170. package/test/figure-assistant-surface.test.ts +32 -38
  171. package/test/first-minute.test.ts +3 -55
  172. package/test/first-save-suggest.test.ts +11 -8
  173. package/test/first-save.test.ts +4 -26
  174. package/test/gizmo.test.ts +0 -25
  175. package/test/joint-fan.test.ts +0 -10
  176. package/test/keys.test.ts +0 -7
  177. package/test/layout.test.ts +4 -73
  178. package/test/links.test.ts +48 -67
  179. package/test/live.test.ts +0 -34
  180. package/test/material.test.ts +17 -26
  181. package/test/maturity.test.ts +13 -16
  182. package/test/mesh-import.test.ts +76 -0
  183. package/test/modeller-route.test.ts +3 -19
  184. package/test/package-download.test.ts +6 -7
  185. package/test/parts-wiring.test.ts +9 -51
  186. package/test/preview.test.ts +5 -46
  187. package/test/properties.test.ts +11 -26
  188. package/test/proposal-record.test.ts +2 -19
  189. package/test/recipes.test.ts +0 -15
  190. package/test/refusals.test.ts +257 -151
  191. package/test/release-entry.test.ts +1 -9
  192. package/test/release-panel.test.ts +1 -22
  193. package/test/resize-with-placement.test.ts +0 -32
  194. package/test/revert-choice.test.ts +1 -22
  195. package/test/revert-keeping.test.ts +1 -12
  196. package/test/save-landed.test.ts +16 -14
  197. package/test/scene-canvas.test.ts +2 -13
  198. package/test/section-editor.test.ts +30 -83
  199. package/test/section-panel.test.ts +22 -17
  200. package/test/slot-layout-host.test.ts +117 -0
  201. package/test/theme.test.ts +5 -9
  202. package/test/verdict.test.ts +44 -41
  203. package/test/view.test.ts +5 -12
  204. package/translations/en.json +61 -1
  205. package/translations/es.json +61 -1
  206. package/translations/ja.json +61 -1
  207. package/translations/ko.json +61 -1
  208. package/translations/ms.json +61 -1
  209. package/translations/zh.json +61 -1
  210. package/test/thumbnail.test.ts +0 -29
@@ -0,0 +1,276 @@
1
+ /*
2
+ * The page a browser check drives when what it checks is the modeller page itself (`figure-modeller-page`): the page
3
+ * the shell mounts, with its editor, properties, verdict and history, over a server in memory. Nothing is sent: the
4
+ * GraphQL client the product's own calls go through answers from the rows below, and every call is recorded so a
5
+ * check can read what the page asked for.
6
+ *
7
+ * What the shell does around a page is stood in for here, no more: popups are drawn where the layout would draw them,
8
+ * an address the product navigates to is routed to the page (and to the store's route, which the detail dock reads),
9
+ * the detail viewpart is drawn when the store shows it, and notices are kept.
10
+ */
11
+ import 'figure-ui-client/index.js'
12
+ import 'figure-ui-client/pages/figure-modeller-page.js'
13
+ import 'figure-ui-client/modeller/figure-history-panel.js'
14
+ import { openFigureDetail } from 'figure-ui-client/viewparts/figure-detail-dock.js'
15
+ import { pendNewFigure } from 'figure-ui-client/modeller/ai-proposal.js'
16
+ import { thumbnailHashOf } from 'figure-ui-client/modeller/thumbnail-hash.js'
17
+ import { client } from '@operato/graphql'
18
+ import { i18next } from '@operato/i18n'
19
+ import { store } from '@operato/shell'
20
+ import { setOpenPopupImplementation } from '@operato/popup'
21
+ import '@operato/popup/ox-floating-overlay.js'
22
+ import { html, render } from 'lit'
23
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
24
+
25
+ const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
26
+
27
+ export function boxSource(name = 'Probe') {
28
+ const asset = createFigureAsset(name)
29
+ asset.palette = { ...asset.palette, ...PALETTE }
30
+ return serializeFigureAsset(addFigureBox(asset, { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } }))
31
+ }
32
+
33
+ /* A door whose leaf swings on a clip: a proposal that takes the leaf away would stop the clip. */
34
+ export function doorSource() {
35
+ let a = createFigureAsset('Door')
36
+ a.palette = { ...a.palette, ...PALETTE }
37
+ a = addFigureBox(a, { id: 'frame', size: { x: 1000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
38
+ a = addFigureBox(a, { id: 'leaf', size: { x: 900, y: 1800, z: 40 }, position: { x: 0, y: 1000, z: 0 } })
39
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'leaf', face: 'bottom', to: { part: 'frame', face: 'top' } })
40
+ const j = a.joints.find(k => k.body1.part === 'leaf')
41
+ a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: 'open' } } })
42
+ a = editFigurePart(a, { kind: 'set-driver', state: 'open', clip: 'swing', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
43
+ return serializeFigureAsset(a)
44
+ }
45
+
46
+ /*
47
+ No translations are loaded, so a text is its key. The values put into a text are kept after it, so a check can read
48
+ them too: `figure.text.revert-with-unsaved {"version":"2"}`.
49
+ */
50
+ const translate = i18next.t.bind(i18next)
51
+ i18next.t = (key, options) => {
52
+ const text = translate(key, options)
53
+ const values = options && typeof options === 'object' ? Object.fromEntries(Object.entries(options).filter(([, v]) => typeof v === 'string' || typeof v === 'number')) : {}
54
+ return Object.keys(values).length ? `${text} ${JSON.stringify(values)}` : text
55
+ }
56
+
57
+ /* The rows the server holds, by id. */
58
+ const rows = new Map()
59
+ /* Every call the page made: the top field asked for and its variables. */
60
+ const calls = []
61
+ /* Answers a check wants instead of the server's: field -> function(variables) returning data or throwing. */
62
+ const overrides = new Map()
63
+ let made = 0
64
+
65
+ function row(id) {
66
+ const found = rows.get(id)
67
+ if (!found) throw Object.assign(Error(`no figure ${id}`), { graphQLErrors: [{ message: `no figure ${id}`, extensions: { code: 'NOT_FOUND' } }] })
68
+ return found
69
+ }
70
+
71
+ const answers = {
72
+ figure: ({ id }) => ({ __typename: 'Figure', properties: null, tags: null, category: null, description: null, score: null, triangles: null, groups: null, thumbnailUpdatedAt: null, updatedAt: null, updater: null, thumbnailHash: null, ...row(id) }),
73
+ figureTypeNames: () => [...rows.values()].map(r => r.type),
74
+ saveFigure: async ({ id, source, type, creationId, thumbnail }) => {
75
+ const saved = id ? row(id) : { id: creationId ?? `made-${++made}`, namespace: null, type: type ?? `figure-${made}`, state: 'draft', version: 0 }
76
+ /* The picture is kept by the hash of its bytes, as the server keeps it (ADR-0104 decision 6). */
77
+ Object.assign(saved, { source, name: JSON.parse(source).name, state: 'draft' }, thumbnail === undefined ? {} : { thumbnailHash: await thumbnailHashOf(thumbnail) })
78
+ rows.set(saved.id, saved)
79
+ return { __typename: 'FigureSaveResult', figure: { __typename: 'Figure', ...saved }, id: saved.id, source: saved.source, applied: [] }
80
+ },
81
+ updateFigure: ({ id, patch }) => {
82
+ const changed = Object.assign(row(id), patch)
83
+ return { __typename: 'FigureSaveResult', figure: { __typename: 'Figure', ...changed } }
84
+ },
85
+ releaseFigure: ({ id }) => {
86
+ const released = Object.assign(row(id), { state: 'released', version: (row(id).version ?? 0) + 1 })
87
+ return { __typename: 'Figure', id, version: released.version, state: 'released', updatedAt: null }
88
+ },
89
+ figureRecipeTemplates: () => [],
90
+ figureProposalOutcome: () => [],
91
+ figureReplacedDrafts: () => [],
92
+ figureVersions: () => [],
93
+ figureReleased: () => null,
94
+ inspectFigure: () => ({ __typename: 'FigureInspection', blocked: false, findings: [], instances: 1 }),
95
+ inspectFigureDraft: () => ({ __typename: 'FigureInspection', blocked: false, findings: [], instances: 1 })
96
+ }
97
+
98
+ function fieldOf(document) {
99
+ const operation = document.definitions.find(d => d.kind === 'OperationDefinition')
100
+ return operation.selectionSet.selections[0].name.value
101
+ }
102
+
103
+ async function answer(document, variables = {}) {
104
+ const field = fieldOf(document)
105
+ calls.push({ field, variables })
106
+ const respond = overrides.get(field) ?? answers[field]
107
+ if (!respond) return { data: { [field]: null } }
108
+ return { data: { [field]: await respond(variables) } }
109
+ }
110
+
111
+ client.query = ({ query, variables }) => answer(query, variables)
112
+ client.mutate = ({ mutation, variables }) => answer(mutation, variables)
113
+
114
+ /* Every element the selector finds, through shadow roots; `host >>> inner` is inner in the shadow root of each host. */
115
+ function deep(selector, root = document) {
116
+ const [first, ...inner] = selector.split(' >>> ')
117
+ let found = everywhere(first, root)
118
+ for (const step of inner) found = found.flatMap(element => [...(element.shadowRoot?.querySelectorAll(step) ?? [])])
119
+ return found
120
+ }
121
+ function everywhere(selector, root) {
122
+ const found = [...root.querySelectorAll(selector)]
123
+ for (const element of root.querySelectorAll('*')) if (element.shadowRoot) found.push(...everywhere(selector, element.shadowRoot))
124
+ return found
125
+ }
126
+
127
+ /* Popups, drawn as the layout draws them: an overlay of their own in the body, the result given when it leaves. */
128
+ let popups = 0
129
+ setOpenPopupImplementation((template, options = {}) => {
130
+ const name = `$popup-${popups++}`
131
+ const fragment = document.createDocumentFragment()
132
+ 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)
133
+ document.body.appendChild(fragment)
134
+ const handle = {
135
+ name,
136
+ close: () => {
137
+ const popup = document.getElementById(name)
138
+ if (!popup) return false
139
+ popup.close()
140
+ return true
141
+ },
142
+ closed: false
143
+ }
144
+ document.addEventListener('overlay-closed', function listener(e) {
145
+ if (e.detail !== name) return
146
+ document.removeEventListener('overlay-closed', listener)
147
+ handle.closed = true
148
+ handle.onclosed && handle.onclosed()
149
+ })
150
+ return handle
151
+ })
152
+
153
+ /* Notices the product raised (`notify`), as the snack bar would show them. */
154
+ const notices = []
155
+ document.addEventListener('notify', e => notices.push({ level: e.detail.level, message: e.detail.message }))
156
+
157
+ /*
158
+ The shell's router, for what the product navigates to: the store's route follows the address, and the modeller page
159
+ is told the figure the address names. Each navigation is kept, with whether the detail preview was still shown then.
160
+ */
161
+ const navigations = []
162
+ const detailShown = () => !!store.getState().layout?.viewparts?.['figure-detail']?.show
163
+ function route() {
164
+ const [, page = '', resourceId] = location.pathname.match(/^\/([^/]*)\/?([^/]*)/) ?? []
165
+ store.dispatch({ type: 'UPDATE_PAGE', page, resourceId: resourceId || undefined, params: {} })
166
+ const element = page === 'figure-modeller' ? document.querySelector('figure-modeller-page') : null
167
+ if (element) element.lifecycle = { ...(element.lifecycle ?? {}), resourceId: resourceId || undefined }
168
+ }
169
+ window.addEventListener('popstate', () => {
170
+ navigations.push({ path: location.pathname, entries: history.length, detailShown: detailShown() })
171
+ route()
172
+ })
173
+
174
+ /* The aside bar's part: the detail viewpart drawn while the store shows it. */
175
+ const aside = document.createElement('div')
176
+ aside.setAttribute('aside', '')
177
+ store.subscribe(() => {
178
+ const part = store.getState().layout?.viewparts?.['figure-detail']
179
+ if (part?.show && !aside.isConnected) document.body.appendChild(aside)
180
+ render(part?.show ? part.template : html``, aside)
181
+ })
182
+
183
+ /* The app's own base: an address the product navigates to (`figure-modeller/a`) is read from the root. */
184
+ const base = document.createElement('base')
185
+ base.href = '/'
186
+ document.head.appendChild(base)
187
+
188
+ const page = () => document.querySelector('figure-modeller-page')
189
+
190
+ /* Where the shell puts a page: its main area, a row the page fills (@operato/shell app-style). */
191
+ const shell = document.createElement('main')
192
+ shell.style.cssText = 'flex: 1; display: flex; flex-direction: row; overflow: hidden; min-width: 0'
193
+ const fill = document.createElement('style')
194
+ fill.textContent = 'main > * { flex: 1; }'
195
+ document.head.appendChild(fill)
196
+
197
+ window.product = {
198
+ boxSource,
199
+ doorSource,
200
+ page,
201
+ deep,
202
+ calls: () => calls.map(c => ({ ...c })),
203
+ forget: () => {
204
+ calls.length = 0
205
+ notices.length = 0
206
+ navigations.length = 0
207
+ },
208
+ rows: () => [...rows.values()].map(r => ({ ...r })),
209
+ notices: () => notices.map(n => ({ ...n })),
210
+ navigations: () => navigations.map(n => ({ ...n })),
211
+ /** What the session behind the page holds now. */
212
+ view: () => {
213
+ const element = page()
214
+ const view = element?.session.read()
215
+ return view && { id: view.id, name: view.name, dirty: !!view.dirty, revision: view.revision, context: view.context, source: element.session.source() }
216
+ },
217
+ /** The shell's record of the page: the context it was last told (title, the rail's menu). */
218
+ context: () => {
219
+ const { title, menu } = store.getState().route?.context ?? {}
220
+ return { title, menu }
221
+ },
222
+ route: () => ({ page: store.getState().route?.page, resourceId: store.getState().route?.resourceId }),
223
+ /** The shell moved to a page (an address the product did not navigate to itself). */
224
+ goTo(path) {
225
+ history.pushState({}, '', path)
226
+ route()
227
+ },
228
+ /** A row on the server, as a figure saved earlier. */
229
+ seed(figure) {
230
+ rows.set(figure.id, { namespace: null, state: 'draft', version: 0, name: JSON.parse(figure.source).name, ...figure })
231
+ },
232
+ /** What the server in memory answers to this field, as it would without an `answer` of the check's. */
233
+ server: (field, variables) => answers[field](variables),
234
+ /** The server answers this field so from now on (a function of the variables; throw to refuse). */
235
+ answer(field, respond) {
236
+ if (respond) overrides.set(field, respond)
237
+ else overrides.delete(field)
238
+ },
239
+ /** The shell's part: the page made active on an address (`resourceId`, or none for a new figure). */
240
+ async open(resourceId) {
241
+ let element = page()
242
+ if (!element) {
243
+ element = document.createElement('figure-modeller-page')
244
+ element.setAttribute('active', '')
245
+ shell.replaceChildren(element)
246
+ document.body.replaceChildren(shell)
247
+ }
248
+ history.replaceState({}, '', resourceId ? `/figure-modeller/${resourceId}` : '/figure-modeller')
249
+ store.dispatch({ type: 'UPDATE_PAGE', page: 'figure-modeller', resourceId, params: {} })
250
+ element.lifecycle = { ...(element.lifecycle ?? {}), resourceId }
251
+ element.active = true
252
+ await element.updateComplete
253
+ return element
254
+ },
255
+ async leave() {
256
+ const element = page()
257
+ element.active = false
258
+ await element.updateComplete
259
+ },
260
+ /** A history panel of its own, beside the page, given these properties. */
261
+ async panel(properties) {
262
+ document.querySelector('figure-history-panel[alone]')?.remove()
263
+ const panel = Object.assign(document.createElement('figure-history-panel'), properties)
264
+ panel.setAttribute('alone', '')
265
+ document.body.appendChild(panel)
266
+ await panel.updateComplete
267
+ return panel
268
+ },
269
+ /** The catalogue's preview of a figure, as a card opens it. */
270
+ openDetail: id => openFigureDetail({ id }),
271
+ detailShown,
272
+ /** The dock's handler opened a new figure for these steps (`figure-assistant.ts`). */
273
+ pendNewFigure: (steps, key) => pendNewFigure(steps, key).then(r => (window.product.pended = r)),
274
+ pended: undefined
275
+ }
276
+ window.harness = window.product
@@ -0,0 +1,112 @@
1
+ /*
2
+ * The page the screen check drives (`screen.check.mjs`): the editor harness (`modeller-page.js`, kept here as
3
+ * `window.editorHarness` -- the modeller page's harness takes `window.harness` after it), the figures the check opens,
4
+ * and what the app shows that is not in the editor: its notices, the card's picture, the scene's registered figures.
5
+ */
6
+ import './modeller-page.js'
7
+ import { captureFigureThumbnail } from 'figure-ui-client/modeller/thumbnail.js'
8
+ import { drawableFigureParts } from 'figure-ui-client/modeller/renderer.js'
9
+ import { previewTypeOf } from 'figure-ui-client/modeller/preview-model.js'
10
+ import { figureReportOf } from 'figure-ui-client/modeller/maturity.js'
11
+ import { hasFigure } from '@hatiolab/things-scene'
12
+ import {
13
+ addFigureBox,
14
+ addFigurePrimitive,
15
+ applyFigureAuthoring,
16
+ compileFigureAsset,
17
+ createFigureAsset,
18
+ editFigurePart,
19
+ robotArmFigure,
20
+ serializeFigureAsset
21
+ } from '@hatiolab/figure-model'
22
+
23
+ window.editorHarness = window.harness
24
+
25
+ const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
26
+
27
+ function named(name) {
28
+ const asset = createFigureAsset(name)
29
+ asset.palette = { ...asset.palette, ...PALETTE }
30
+ return asset
31
+ }
32
+ const cube = (id, x, extra = {}) => ({ id, size: { x: 400, y: 400, z: 400 }, position: { x, y: 200, z: 0 }, ...extra })
33
+
34
+ /** The figures the check opens, as saved text. */
35
+ const FIGURES = {
36
+ /* A new figure: nothing in it yet. */
37
+ empty: () => named('New figure'),
38
+ /* Two boxes in two colours: two draw groups. */
39
+ two: () => {
40
+ let a = addFigureBox(named('Two'), cube('left', -300))
41
+ a = addFigureBox(a, cube('right', 300))
42
+ return editFigurePart(a, { kind: 'set-material', target: 'right', material: { token: 'palette.accent' } })
43
+ },
44
+ /* Two boxes of two transparent glasses: past the transparent limit, graded D. */
45
+ glass: () => {
46
+ const a = named('Glass')
47
+ a.palette = { ...a.palette, 'glass.a': '#88aacc', 'glass.b': '#aaccee' }
48
+ const b = addFigureBox(a, cube('a', -300, { material: { token: 'glass.a', transparent: true } }))
49
+ return addFigureBox(b, cube('b', 300, { material: { token: 'glass.b', transparent: true } }))
50
+ },
51
+ /* A figure hung from the ceiling, its box under it. */
52
+ ceiling: () => applyFigureAuthoring(addFigureBox(named('Hung'), { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: -200, z: 0 } }), { kind: 'set-placement', placement: 'ceiling' }),
53
+ /* A drum: a palette part whose sizes are its own fields. */
54
+ drum: () => addFigurePrimitive(named('Drum'), { id: 'drum', kind: 'cylinder', dimensions: { radius: 200, length: 500 }, position: { x: 0, y: 250, z: 0 } }),
55
+ /* A lighting tower that follows its height only: the wider variants ask a width it does not declare. */
56
+ tower: () => {
57
+ let a = addFigureBox(named('Tower'), { id: 'mast', size: { x: 200, y: 2000, z: 300 }, position: { x: 0, y: 0, z: 0 } })
58
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'mast', face: 'bottom', to: { plane: 'mounting-plane' } })
59
+ return applyFigureAuthoring(a, { kind: 'declare-instance-size', range: { x: { min: 200, max: 200 }, y: { min: 1000, max: 3000 }, z: { min: 300, max: 300 } } })
60
+ },
61
+ /* The robot arm with a clip that turns its base: controls and a clip in the preview. */
62
+ 'arm-clip': () => editFigurePart(robotArmFigure(), { kind: 'set-driver', state: 'j1-base', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
63
+ }
64
+
65
+ /** A figure by name, as saved text. */
66
+ window.figureText = name => serializeFigureAsset(FIGURES[name]())
67
+
68
+ function deep(selector, root = document) {
69
+ const found = [...root.querySelectorAll(selector)]
70
+ for (const element of root.querySelectorAll('*')) if (element.shadowRoot) found.push(...deep(selector, element.shadowRoot))
71
+ return found
72
+ }
73
+
74
+ /** An element's own words, without its children's (a heading beside its help button, a label beside its unit). */
75
+ const ownText = element => [...(element?.childNodes ?? [])].filter(n => n.nodeType === Node.TEXT_NODE).map(n => n.textContent).join('').trim()
76
+
77
+ /* The app's notices (`notify` from @operato/layout), as the shell would show them. */
78
+ const told = []
79
+ document.addEventListener('notify', e => told.push(e.detail))
80
+
81
+ window.screenCheck = {
82
+ deep,
83
+ /** The editor: the harness's, or the one the modeller page draws. */
84
+ editor: () => document.querySelector('figure-editor') ?? deep('figure-editor')[0],
85
+ ownText,
86
+ /** The side panel shown now: the one the tab that is on shows. */
87
+ shownPanel: () => [...window.screenCheck.editor().shadowRoot.querySelectorAll('aside > div[panel]')].find(p => !p.hasAttribute('off')),
88
+ /** A control of the view tab, by the words beside it (the i18n key). */
89
+ setting: key => deep('label', deep('figure-settings')[0].shadowRoot).find(l => l.querySelector('span')?.textContent.trim().startsWith(key)),
90
+ /** A control of the preview's own, by the name it shows: a state's id, or a clip's. */
91
+ previewControl: name => [...deep('figure-preview')[0].shadowRoot.querySelectorAll('div[clips] label')].find(l => ownText(l.querySelector('span')) === name),
92
+ told: () => told.map(n => ({ level: n.level, message: n.message })),
93
+ forgetTold: () => (told.length = 0),
94
+ /** Whether the scene has a figure registered under a type. */
95
+ registered: type => hasFigure(type),
96
+ previewTypeOf,
97
+ /** The report the verdict tab measures, for the figure the editor holds. */
98
+ capacityNow: () => figureReportOf(window.screenCheck.editor().session.revisionNow().asset)?.capacity.instances,
99
+ /** The card's picture of a figure (saved text), as a save takes it. */
100
+ async picture(text) {
101
+ const asset = JSON.parse(text)
102
+ const evaluation = compileFigureAsset(asset).evaluate()
103
+ return await captureFigureThumbnail(drawableFigureParts(evaluation.geometry), evaluation.palette ?? asset.palette ?? {})
104
+ },
105
+ /** An image's size, from its data URL. */
106
+ async imageSize(url) {
107
+ const image = new Image()
108
+ image.src = url
109
+ await image.decode()
110
+ return [image.naturalWidth, image.naturalHeight]
111
+ }
112
+ }
@@ -0,0 +1,62 @@
1
+ /*
2
+ * The screen check's page with the modeller page beside the editor: `screen-page.js` first (the editor harness is kept
3
+ * as `window.editorHarness`), then the modeller page over a server in memory (`product-page.js`, `window.product`).
4
+ * Built with the assistant dock's stand-in (`assistant-stand-in.js`).
5
+ *
6
+ * Added here: what the page sent the server, as documents (the fields each question asks for), and the verdict tab and
7
+ * the release history mounted alone over a figure, as the modeller page mounts them, for the rows that are theirs.
8
+ */
9
+ import './screen-page.js'
10
+ import './product-page.js'
11
+ import 'figure-ui-client/modeller/figure-verdict.js'
12
+ import 'figure-ui-client/modeller/figure-history-panel.js'
13
+ import { createFigureProductSession } from 'figure-ui-client/modeller/product-session.js'
14
+ import { client } from '@operato/graphql'
15
+
16
+ /* Each question's document, by its top field, as the page sent it. */
17
+ const documents = new Map()
18
+ const answer = client.query
19
+ client.query = options => {
20
+ const operation = options.query.definitions.find(d => d.kind === 'OperationDefinition')
21
+ documents.set(operation.selectionSet.selections[0].name.value, operation.selectionSet.selections[0])
22
+ return answer(options)
23
+ }
24
+
25
+ /** The fields a selection asks for, nested, in order: `a b { c d }`. */
26
+ function fieldsOf(selection) {
27
+ const inner = selection.selectionSet?.selections ?? []
28
+ return inner.map(one => (one.selectionSet ? `${one.name.value} { ${fieldsOf(one)} }` : one.name.value)).join(' ')
29
+ }
30
+
31
+ /** A release finding as the server words it: its message is its code, so the check can tell them apart. */
32
+ const finding = (code, blocking = true) => ({ __typename: 'FigureGateFinding', code, message: code, why: null, how: null, at: null, blocking })
33
+
34
+ window.alone = {
35
+ /** The fields the page asked for in its last question of this name. */
36
+ fieldsAsked: field => (documents.has(field) ? fieldsOf(documents.get(field)) : null),
37
+ finding,
38
+ /**
39
+ * The verdict tab over a figure (saved text), its id, and the findings the server answers for it -- as the modeller
40
+ * page mounts it in the editor's verdict slot. Also the release history over the same id, when asked.
41
+ */
42
+ async mount(text, { figureId, findings = [], history = false } = {}) {
43
+ const inspection = { __typename: 'FigureInspection', blocked: findings.some(f => f.blocking), findings }
44
+ window.product.answer('inspectFigureDraft', () => inspection)
45
+ window.product.answer('inspectFigure', () => inspection)
46
+ const session = createFigureProductSession({ load: async id => ({ id, source: text }), save: async request => ({ id: request.id ?? 'probe', source: request.source }) })
47
+ await session.open(figureId ?? 'probe')
48
+ const verdict = document.createElement('figure-verdict')
49
+ verdict.figureId = figureId
50
+ verdict.revision = session.revisionNow()
51
+ const shown = [verdict]
52
+ if (history) {
53
+ const panel = document.createElement('figure-history-panel')
54
+ panel.figureId = figureId
55
+ panel.dirty = false
56
+ shown.push(panel)
57
+ }
58
+ document.body.replaceChildren(...shown)
59
+ window.alone.verdict = verdict
60
+ await verdict.updateComplete
61
+ }
62
+ }