@things-factory/figure-ui 10.1.147 → 10.1.149

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 (209) 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.mjs +6 -1
  141. package/test/browser/editing-page.js +224 -0
  142. package/test/browser/editing.check.mjs +550 -0
  143. package/test/browser/links-page.js +113 -0
  144. package/test/browser/links.check.mjs +568 -0
  145. package/test/browser/modeller.check.mjs +21 -0
  146. package/test/browser/parts-page.js +120 -0
  147. package/test/browser/parts.check.mjs +328 -0
  148. package/test/browser/places-mesh-page.js +36 -0
  149. package/test/browser/places-mesh.check.mjs +239 -0
  150. package/test/browser/product-page.check.mjs +682 -0
  151. package/test/browser/product-page.js +276 -0
  152. package/test/browser/screen-page.js +112 -0
  153. package/test/browser/screen-product-page.js +62 -0
  154. package/test/browser/screen.check.mjs +1003 -0
  155. package/test/browser/section-page.js +256 -0
  156. package/test/browser/section.check.mjs +719 -0
  157. package/test/browser/serve.mjs +20 -12
  158. package/test/carrier-declaration.test.ts +69 -0
  159. package/test/cavity-drawing.test.ts +2 -18
  160. package/test/clip-ai-proposal.test.ts +1 -5
  161. package/test/clip-consent.test.ts +0 -5
  162. package/test/clips.test.ts +0 -7
  163. package/test/command-preview.test.ts +0 -68
  164. package/test/controls.test.ts +0 -9
  165. package/test/detail-dock.test.ts +6 -25
  166. package/test/face-mapping.test.ts +1 -20
  167. package/test/fasten-drawing.test.ts +0 -23
  168. package/test/fields.test.ts +6 -33
  169. package/test/figure-assistant-surface.test.ts +32 -38
  170. package/test/first-minute.test.ts +3 -55
  171. package/test/first-save-suggest.test.ts +11 -8
  172. package/test/first-save.test.ts +4 -26
  173. package/test/gizmo.test.ts +0 -25
  174. package/test/joint-fan.test.ts +0 -10
  175. package/test/keys.test.ts +0 -7
  176. package/test/layout.test.ts +4 -73
  177. package/test/links.test.ts +48 -67
  178. package/test/live.test.ts +0 -34
  179. package/test/material.test.ts +17 -26
  180. package/test/maturity.test.ts +13 -16
  181. package/test/mesh-import.test.ts +76 -0
  182. package/test/modeller-route.test.ts +3 -19
  183. package/test/package-download.test.ts +6 -7
  184. package/test/parts-wiring.test.ts +9 -51
  185. package/test/preview.test.ts +5 -46
  186. package/test/properties.test.ts +11 -26
  187. package/test/proposal-record.test.ts +2 -19
  188. package/test/recipes.test.ts +0 -15
  189. package/test/refusals.test.ts +257 -151
  190. package/test/release-entry.test.ts +1 -9
  191. package/test/release-panel.test.ts +1 -22
  192. package/test/resize-with-placement.test.ts +0 -32
  193. package/test/revert-choice.test.ts +1 -22
  194. package/test/revert-keeping.test.ts +1 -12
  195. package/test/save-landed.test.ts +16 -14
  196. package/test/scene-canvas.test.ts +2 -13
  197. package/test/section-editor.test.ts +30 -83
  198. package/test/section-panel.test.ts +22 -17
  199. package/test/slot-layout-host.test.ts +117 -0
  200. package/test/theme.test.ts +5 -9
  201. package/test/verdict.test.ts +44 -41
  202. package/test/view.test.ts +5 -12
  203. package/translations/en.json +61 -1
  204. package/translations/es.json +61 -1
  205. package/translations/ja.json +61 -1
  206. package/translations/ko.json +61 -1
  207. package/translations/ms.json +61 -1
  208. package/translations/zh.json +61 -1
  209. package/test/thumbnail.test.ts +0 -29
@@ -1,7 +1,10 @@
1
+ import { getPathInfo } from '@operato/utils'
2
+ import { setFigureMeshResolver } from '@hatiolab/things-scene'
3
+
1
4
  /**
2
5
  * 클라이언트 부트스트랩.
3
6
  *
4
- * 지금은 등록할 것이 없다. 그래도 이 파일을 두는 이유는 `things-factory.config.js` 가
7
+ * 메시 부품의 파일을 어디서 받는지를 things-scene 에 알린다(ADR-0129). 이 파일을 두는 다른 이유는 `things-factory.config.js` 가
5
8
  * `bootstrap` 을 가리키기 때문이고, 데이터 그리드용 편집기·렌더러나 속성 편집기를
6
9
  * 붙이게 되면 여기가 그 자리다(`board-ui/client/bootstrap.ts` 가 그 예다).
7
10
  *
@@ -12,5 +15,10 @@
12
15
  * 올리는 것은 운영자가 메뉴를 만들어 `figure-list` 를 가리키게 하는 일이다.
13
16
  */
14
17
  export default function bootstrap() {
15
- // 등록할 것이 생기면 여기에 둔다.
18
+ /*
19
+ Where a mesh part's file is fetched from on this server (ADR-0129 decision 8): `/figure-mesh/:hash`, behind the
20
+ figure permission. The domain part is read when a file is asked for, as the thumbnails read it -- the domain can
21
+ change while the app runs.
22
+ */
23
+ setFigureMeshResolver(hash => `${getPathInfo(location.pathname).contextPath}/figure-mesh/${hash}`)
16
24
  }
@@ -475,3 +475,23 @@ export async function fetchRecipeGuide(name: string, lang: string): Promise<stri
475
475
  })
476
476
  return unwrap<{ guide?: string } | null>(response, 'figureRecipeTemplate')?.guide ?? ''
477
477
  }
478
+
479
+ /**
480
+ * A GLB for a mesh part (ADR-0129): the server reads it, keeps it by its hash and answers what it holds. A file a board
481
+ * cannot draw is refused with the reason in the screen's words.
482
+ */
483
+ export async function uploadFigureMesh(file: File): Promise<import('../modeller/mesh-import.js').MeshUpload> {
484
+ const response = await client.mutate({
485
+ mutation: gql`
486
+ mutation UploadFigureMesh($file: Upload!) {
487
+ uploadFigureMesh(file: $file) {
488
+ hash
489
+ summary
490
+ }
491
+ }
492
+ `,
493
+ variables: { file },
494
+ context: { hasUpload: true }
495
+ })
496
+ return unwrap(response, 'uploadFigureMesh')
497
+ }
@@ -7,6 +7,7 @@ import type { FigureRenderPart } from './renderer.js'
7
7
  import { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'
8
8
  import { boardFor, changedKeys, componentOf, gizmoResultOf, type Board, type ScaleAnchor } from './stage-model.js'
9
9
  import { JOINT_FAN } from './joint-fan.js'
10
+ import { SLOT_MARK } from './slot-layout-host.js'
10
11
  import { PREVIEW, TARGET_FACE } from './command-preview.js'
11
12
  import { ownerOf } from './fields.js'
12
13
 
@@ -95,6 +96,8 @@ export class FigureCanvas extends LitElement {
95
96
  */
96
97
  @property({ attribute: false }) ghost: FigureRenderPart[] = []
97
98
  @property({ attribute: false }) highlight: FigureRenderPart | null = null
99
+ /** The slots a slot layout unfolds to (ADR-0118), as boxes of what each allows: shown, never picked, never saved. */
100
+ @property({ attribute: false }) marks: FigureRenderPart[] = []
98
101
 
99
102
  @query('div[stage]') private stage!: HTMLDivElement
100
103
 
@@ -124,7 +127,7 @@ export class FigureCanvas extends LitElement {
124
127
 
125
128
  protected updated(changed: PropertyValues) {
126
129
  if (!this.hasUpdated) return
127
- if (['parts', 'palette', 'context', 'fan', 'anchors', 'ghost', 'highlight'].some(key => changed.has(key))) this.standUp()
130
+ if (['parts', 'palette', 'context', 'fan', 'anchors', 'ghost', 'highlight', 'marks'].some(key => changed.has(key))) this.standUp()
128
131
  if (changed.has('view')) this.applyView()
129
132
  if (changed.has('selected')) this.showSelected()
130
133
  }
@@ -223,7 +226,8 @@ export class FigureCanvas extends LitElement {
223
226
  /* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */
224
227
  const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board!) : null
225
228
  const accent = this.palette['palette.accent'] ?? FAN_COLOUR
226
- const shown = [...(fan ? [fan] : []), ...this.ghost.map(part => componentOf(part, { ...this.palette, [PREVIEW]: accent, [TARGET_FACE]: accent }, this.board!)), ...(this.highlight ? [componentOf(this.highlight, { ...this.palette, [TARGET_FACE]: accent }, this.board!)] : [])]
229
+ const slots = this.marks.map(part => componentOf(part, { ...this.palette, [SLOT_MARK]: this.palette['palette.secondary'] ?? accent }, this.board!))
230
+ const shown = [...(fan ? [fan] : []), ...slots, ...this.ghost.map(part => componentOf(part, { ...this.palette, [PREVIEW]: accent, [TARGET_FACE]: accent }, this.board!)), ...(this.highlight ? [componentOf(this.highlight, { ...this.palette, [TARGET_FACE]: accent }, this.board!)] : [])]
227
231
  return [...parts, ...shown.filter((c): c is Record<string, unknown> => !!c).map(c => ({ ...c, capturable: false, locked: true }))]
228
232
  }
229
233
 
@@ -0,0 +1,63 @@
1
+ /*
2
+ * A figure that is a carrier type declares so (ADR-0123): the CARRIERTYPE code a material handling system reports, and
3
+ * the carrier's physical size in mm. Kept apart from the element so it is tested without a browser.
4
+ *
5
+ * The rule is figure-model's (`carrierDeclarationRefusal`); the field asks it before sending `set-carrier`, so a value
6
+ * the kernel refuses is said beside the field and never becomes a step of the history. The size is what the author
7
+ * types, never read off the occupied room.
8
+ */
9
+ import { carrierDeclarationRefusal, FIGURE_CARRIER_TYPE_MAX, normalizeCarrierType, type FigureAsset, type FigureAuthoringAction } from '@hatiolab/figure-model'
10
+
11
+ type Say = (key: string, params: Record<string, string>) => string
12
+ type Declaration = NonNullable<FigureAsset['carrier']>
13
+
14
+ export const CARRIER_AXES = ['width', 'length', 'height'] as const
15
+ export type CarrierAxis = (typeof CARRIER_AXES)[number]
16
+
17
+ /** What the field holds while the author types: the code and the three lengths as typed. */
18
+ export type CarrierDraft = { type: string } & Record<CarrierAxis, string>
19
+
20
+ /** The field's values for a declaration, or blank for a figure that declares none. */
21
+ export function carrierDraftOf(carrier?: Declaration): CarrierDraft {
22
+ return {
23
+ type: carrier?.type ?? '',
24
+ width: carrier ? String(carrier.dimensions.width) : '',
25
+ length: carrier ? String(carrier.dimensions.length) : '',
26
+ height: carrier ? String(carrier.dimensions.height) : ''
27
+ }
28
+ }
29
+
30
+ /** Whether the field says what is declared already: nothing to send. */
31
+ export function draftIsDeclared(draft: CarrierDraft, carrier?: Declaration): boolean {
32
+ const declared = carrierDraftOf(carrier)
33
+ return normalizeCarrierType(draft.type) === declared.type && CARRIER_AXES.every(axis => Object.is(lengthOf(draft[axis]), lengthOf(declared[axis])))
34
+ }
35
+
36
+ /** A typed length as a number; blank is no length. */
37
+ const lengthOf = (typed: string) => (typed.trim() === '' ? Number.NaN : Number(typed))
38
+
39
+ /**
40
+ * The `set-carrier` command for what the field holds, or why the kernel would refuse it in the author's words. The
41
+ * code is sent as the kernel stores it (trimmed, upper case).
42
+ */
43
+ export function carrierCommandOf(draft: CarrierDraft, say: Say): { action: FigureAuthoringAction } | { refusal: string } {
44
+ const carrier = {
45
+ type: normalizeCarrierType(draft.type),
46
+ dimensions: { width: lengthOf(draft.width), length: lengthOf(draft.length), height: lengthOf(draft.height) }
47
+ }
48
+ const refused = carrierDeclarationRefusal(carrier)
49
+ if (refused) return { refusal: carrierRefusalOf(refused, carrier.type, say) }
50
+ return { action: { kind: 'set-carrier', carrier } }
51
+ }
52
+
53
+ /**
54
+ * A carrier declaration refused, said from its code and path (ADR-0127): the code missing or too long, or the length
55
+ * the path names. The kernel's English is never shown; a code the field has no words for is said with the code.
56
+ */
57
+ export function carrierRefusalOf(refused: { code: string; path: string }, type: string, say: Say): string {
58
+ if (refused.code === 'CARRIER_TYPE')
59
+ return type ? say('figure.text.carrier-type-too-long', { max: String(FIGURE_CARRIER_TYPE_MAX) }) : say('figure.text.carrier-type-required', {})
60
+ const axis = CARRIER_AXES.find(one => refused.path.endsWith(`.${one}`))
61
+ if (refused.code === 'CARRIER_DIMENSIONS' && axis) return say('figure.text.carrier-length-positive', { axis: say(`figure.label.carrier-${axis}`, {}) })
62
+ return say('figure.text.edit-refused', { code: refused.code })
63
+ }
@@ -2,7 +2,7 @@ import { LitElement, html, css, nothing, type PropertyValues } from 'lit'
2
2
  import { i18next } from '@operato/i18n'
3
3
  import type { OxThreePane } from '@operato/layout'
4
4
  import '@operato/layout'
5
- import { notify } from '@operato/layout'
5
+ import { notify, openPopup } from '@operato/layout'
6
6
  import { OxPrompt } from '@operato/popup'
7
7
  import { customElement, property, state } from 'lit/decorators.js'
8
8
  import { live } from 'lit/directives/live.js'
@@ -35,6 +35,12 @@ import { ScrollbarStyles } from '@operato/styles'
35
35
  import { alignChanges, canMirror, mirrorChanges, type AlignTool } from './align.js'
36
36
  import type { FigurePaletteKind, FigurePartRow } from './figure-parts.js'
37
37
  import { paletteRecipe } from './palette.js'
38
+ import { meshChoicesOf, meshPartIdOf, meshPartOf } from './mesh-import.js'
39
+ import './figure-mesh-choice.js'
40
+ import { uploadFigureMesh } from '../graphql/index.js'
41
+ import { slotMarksOf } from './slot-layout-host.js'
42
+
43
+ const NO_MARKS: FigureRenderPart[] = []
38
44
  import { apexOver, drawnOf } from './section-drawing.js'
39
45
  import { END_FACE } from './section-controls.js'
40
46
  import type { FigureCanvas } from './canvas.js'
@@ -43,7 +49,7 @@ import { nudgeOf, placementOf, type ScaleAnchor } from './stage-model.js'
43
49
  /** The numbers a rule gives a part's position, turn and size, per axis, for the cells that have no field. */
44
50
  type HeldValues = { position?: number[]; rotation?: number[]; size?: (number | undefined)[]; sizeRuled?: boolean }
45
51
  import { roomRefitRowsOf } from './occupancy.js'
46
- import { editRefusalOf, inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, recipeRefusalOf, removalNoticeOf, sizeRangeRefusalOf } from './refusals.js'
52
+ import { editRefusalOf, inDeclaredRoom, narrowingRetryOf, partNameRefusalOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js'
47
53
  import type { FigureRenderPart } from './renderer.js'
48
54
  import type { FigureBudgetLine } from './maturity.js'
49
55
  import { endPlaceKeyOf, fieldWordsOf, followOf, measureNow, partSummaryOf, instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
@@ -709,8 +715,7 @@ export class FigureEditor extends LitElement {
709
715
  this.changed()
710
716
  } catch (error) {
711
717
  if (this.askClipLoss(error, () => this.run(action))) return
712
- const say = (key: string, params: Record<string, string>) => i18next.t(key, params)
713
- this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? this.said(error)
718
+ this.error = this.said(error)
714
719
  } finally {
715
720
  /* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
716
721
  this.live = NOTHING_LIVE
@@ -806,6 +811,39 @@ export class FigureEditor extends LitElement {
806
811
  this.selected = part.id
807
812
  })
808
813
  }
814
+ /**
815
+ * A GLB made into a mesh part (ADR-0129): the server reads and keeps it, the author picks the whole file or one node
816
+ * of it, and the part is added as one step of the history, standing where it stood in the file. A file the server
817
+ * refuses is said in its words; nothing is added.
818
+ */
819
+ private async importMesh(file: File) {
820
+ if (!this.session) return
821
+ let upload: Awaited<ReturnType<typeof uploadFigureMesh>>
822
+ try {
823
+ upload = await uploadFigureMesh(file)
824
+ } catch (error) {
825
+ this.error = this.said(error)
826
+ return
827
+ }
828
+ const choices = meshChoicesOf(upload)
829
+ /* Only the whole file and the one node that is all of it: there is nothing to choose between. */
830
+ const choice =
831
+ choices.length <= 2
832
+ ? {}
833
+ : await openPopup(html`<figure-mesh-choice .choices=${choices}></figure-mesh-choice>`, {
834
+ backdrop: true,
835
+ size: 'small',
836
+ title: i18next.t('figure.title.choose-mesh-part')
837
+ }).result
838
+ if (!choice) return
839
+ const node = upload.summary.nodes.find(n => n.index === (choice as { node?: number }).node)
840
+ this.run(() => {
841
+ const taken = new Set(this.session!.read().geometry.map(g => g.id))
842
+ const id = meshPartIdOf(node?.name ?? file.name.replace(/\.glb$/i, ''), taken)
843
+ this.session!.add(meshPartOf(upload, choice as { node?: number }, id))
844
+ this.selected = id
845
+ })
846
+ }
809
847
  /**
810
848
  * The list's own operations, as part edits — each one step of the session's history. The copy and the moved
811
849
  * part stay selected.
@@ -979,12 +1017,7 @@ export class FigureEditor extends LitElement {
979
1017
  this.error = ''
980
1018
  this.changed()
981
1019
  } catch (error) {
982
- const message = error instanceof Error ? error.message : String(error)
983
- this.error = /carried by a joint/.test(message)
984
- ? i18next.t('figure.text.gizmo-joint-places', { part: partLabelOf(id) })
985
- : /is fastened to/.test(message)
986
- ? i18next.t('figure.text.gizmo-fastening-places', { part: partLabelOf(id) })
987
- : this.said(error)
1020
+ this.error = placedRefusalOf(error, partLabelOf(id), (key, params) => i18next.t(key, params)) ?? this.said(error)
988
1021
  this.requestUpdate()
989
1022
  }
990
1023
  }
@@ -1104,13 +1137,8 @@ export class FigureEditor extends LitElement {
1104
1137
  this.changed()
1105
1138
  } catch (error) {
1106
1139
  input.value = from
1107
- const code = (error as { code?: string })?.code
1108
- this.error =
1109
- code === 'PART_ID'
1110
- ? i18next.t(/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name) ? 'figure.text.part-name-taken' : 'figure.text.part-name-invalid', { name })
1111
- : error instanceof Error
1112
- ? error.message
1113
- : String(error)
1140
+ /* Whether a name is a name is the kernel's rule, said from its refusal (ADR-0127), not checked again here. */
1141
+ this.error = partNameRefusalOf(error, (key, params) => i18next.t(key, params)) ?? this.said(error)
1114
1142
  }
1115
1143
  }
1116
1144
  /** Size response, fastening and motion for the selected part, as authoring commands. */
@@ -1237,9 +1265,7 @@ export class FigureEditor extends LitElement {
1237
1265
  ? i18next.t('figure.text.follow-would-loop')
1238
1266
  : code === 'FASTEN_CYCLE' || code === 'ATTACH_CYCLE'
1239
1267
  ? i18next.t('figure.text.fasten-would-loop')
1240
- : sizeRangeRefusalOf(error, (key, params) => i18next.t(key, params)) ??
1241
- placementRefusalOf(error, (key, params) => i18next.t(key, params)) ??
1242
- this.said(error)
1268
+ : this.said(error)
1243
1269
  return false
1244
1270
  }
1245
1271
  }
@@ -1596,13 +1622,7 @@ export class FigureEditor extends LitElement {
1596
1622
  this.changed()
1597
1623
  } catch (error) {
1598
1624
  input.value = String(field.value)
1599
- const code = (error as { code?: string })?.code
1600
- const message = error instanceof Error ? error.message : String(error)
1601
- const found = /\((\d[\d.]*) mm\) a pitch under ([\d.]+) mm makes more than (\d+)/.exec(message)
1602
- this.error =
1603
- code === 'INPUT_RANGE' && found
1604
- ? i18next.t('figure.text.repeat-pitch-too-small', { length: Number(found[1]).toLocaleString(), min: found[2], limit: found[3] })
1605
- : this.said(error)
1625
+ this.error = this.said(error)
1606
1626
  }
1607
1627
  }
1608
1628
  /** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
@@ -1799,6 +1819,19 @@ export class FigureEditor extends LitElement {
1799
1819
  const trying = this.middle === 'preview' ? this.renderRoot.querySelector<HTMLElement & { tried?: () => { clips?: Record<string, number>; controls?: Record<string, number> } | undefined }>('figure-preview')?.tried?.() : undefined
1800
1820
  return { ...(this.selected ? { part: this.selected } : {}), ...(trying ? { trying } : {}) }
1801
1821
  }
1822
+ /**
1823
+ * The slots the figure's slot layout unfolds to, where they fall now (ADR-0118): drawn while the places are being
1824
+ * edited. Made again only when the evaluation is new, so the canvas is not stood up again for nothing.
1825
+ */
1826
+ private slotMarks(view: { bindings?: readonly unknown[] }, asset: unknown) {
1827
+ const places = view.bindings ?? []
1828
+ if (this.slotMarksDrawn?.of !== places) {
1829
+ const layout = (asset as { document?: { capabilities?: { slotLayout?: { layout: never } } } })?.document?.capabilities?.slotLayout?.layout
1830
+ this.slotMarksDrawn = { of: places, marks: slotMarksOf(places as never, layout) as never }
1831
+ }
1832
+ return this.slotMarksDrawn.marks
1833
+ }
1834
+ private slotMarksDrawn?: { of: readonly unknown[]; marks: FigureRenderPart[] }
1802
1835
  private choose(id: string) {
1803
1836
  /* A copy on the canvas selects its repeat: the part the author made. */
1804
1837
  this.selected = id ? ownerOf(id) : id
@@ -2057,6 +2090,7 @@ export class FigureEditor extends LitElement {
2057
2090
  @show-model-settings=${() => this.showModelSettings()}
2058
2091
  ?locked=${unavailable}
2059
2092
  @add-part=${(e: CustomEvent) => this.add(e.detail.kind)}
2093
+ @import-mesh=${(e: CustomEvent) => this.importMesh(e.detail.file)}
2060
2094
  .recipeCount=${this.templates.length}
2061
2095
  @open-recipe-catalog=${() => ((this.catalogOpen = true), (this.recipeError = ''))}
2062
2096
  @select-part=${(e: CustomEvent) => this.choose(e.detail.id)}
@@ -2096,6 +2130,7 @@ export class FigureEditor extends LitElement {
2096
2130
  .fan=${this.fanOf(shown, live)}
2097
2131
  .ghost=${this.previewShown().ghost}
2098
2132
  .highlight=${this.previewShown().highlight}
2133
+ .marks=${this.tab === 'capability' ? this.slotMarks(view, asset) : NO_MARKS}
2099
2134
  .palette=${view.palette ?? {}}
2100
2135
  .context=${view.context ?? ''}
2101
2136
  .selected=${this.selected}
@@ -0,0 +1,117 @@
1
+ import { css, html, LitElement, nothing, type PropertyValues } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+ import { live } from 'lit/directives/live.js'
4
+
5
+ import { i18next, localize } from '@operato/i18n'
6
+ import type { FigureAsset, FigureAuthoringAction } from '@hatiolab/figure-model'
7
+
8
+ import { CARRIER_AXES, carrierCommandOf, carrierDraftOf, draftIsDeclared, type CarrierAxis, type CarrierDraft } from './carrier-declaration.js'
9
+ import { SidePanelStyles } from './side-panel-styles.js'
10
+
11
+ const AXIS_LETTER: Record<CarrierAxis, string> = { width: 'W', length: 'L', height: 'H' }
12
+
13
+ /**
14
+ * 캐리어 — the CARRIERTYPE code and physical size a carrier figure declares (ADR-0123), in the properties tab.
15
+ *
16
+ * Declaring and clearing are `set-carrier` commands, sent up as `command` like the tab's others, so the page runs them as
17
+ * steps of the session's history: undo takes them back and they are saved with the figure. A value the kernel refuses
18
+ * is said here and sends nothing.
19
+ */
20
+ @customElement('figure-carrier')
21
+ export class FigureCarrier extends localize(i18next)(LitElement) {
22
+ static styles = [SidePanelStyles, css`
23
+ div[row] {
24
+ display: flex;
25
+ gap: 6px;
26
+ }
27
+ div[row] > button[act] {
28
+ flex: 1;
29
+ margin-top: 0;
30
+ }
31
+ small[role='alert'] {
32
+ color: var(--md-sys-color-error);
33
+ }
34
+ `]
35
+
36
+ /** What the figure on screen declares; absent when it is not a carrier type. */
37
+ @property({ attribute: false }) carrier?: FigureAsset['carrier']
38
+ @state() private draft: CarrierDraft = carrierDraftOf()
39
+ @state() private refusal = ''
40
+ /* The declaration the field was last filled from: other edits draw the tab again and must not wipe what is being typed. */
41
+ private filledFrom?: string
42
+
43
+ willUpdate(changed: PropertyValues<this>) {
44
+ if (!changed.has('carrier')) return
45
+ const declared = JSON.stringify(this.carrier ?? null)
46
+ if (declared === this.filledFrom) return
47
+ this.filledFrom = declared
48
+ this.draft = carrierDraftOf(this.carrier)
49
+ this.refusal = ''
50
+ }
51
+
52
+ render() {
53
+ const pending = !draftIsDeclared(this.draft, this.carrier)
54
+ return html`<section>
55
+ <h3>${i18next.t('figure.label.group-carrier')}</h3>
56
+ <div field>
57
+ <label for="carrier-type">${i18next.t('figure.label.carrier-type')}</label>
58
+ <input
59
+ id="carrier-type"
60
+ .value=${live(this.draft.type)}
61
+ placeholder=${i18next.t('figure.text.carrier-type-placeholder')}
62
+ @input=${(e: Event) => this.type('type', e)}
63
+ @keydown=${this.enter}
64
+ />
65
+ </div>
66
+ <div vector-row>
67
+ <label>${i18next.t('figure.label.carrier-size')} <span unit>mm</span></label>
68
+ <div triple>
69
+ ${CARRIER_AXES.map(
70
+ axis => html`<div cell>
71
+ <span axis>${AXIS_LETTER[axis]}</span>
72
+ <input
73
+ type="number"
74
+ min="0"
75
+ step="any"
76
+ aria-label=${i18next.t(`figure.label.carrier-${axis}`)}
77
+ .value=${live(this.draft[axis])}
78
+ @input=${(e: Event) => this.type(axis, e)}
79
+ @keydown=${this.enter}
80
+ />
81
+ </div>`
82
+ )}
83
+ </div>
84
+ </div>
85
+ ${this.refusal ? html`<small role="alert">${this.refusal}</small>` : nothing}
86
+ <div row>
87
+ <button act ?disabled=${!pending} @click=${this.declare}>
88
+ <md-icon>check</md-icon>${i18next.t('figure.button.declare-carrier')}
89
+ </button>
90
+ <button act ?disabled=${!this.carrier} @click=${this.clear}>
91
+ <md-icon>close</md-icon>${i18next.t('figure.button.clear-carrier')}
92
+ </button>
93
+ </div>
94
+ </section>`
95
+ }
96
+
97
+ private type(key: keyof CarrierDraft, event: Event) {
98
+ this.draft = { ...this.draft, [key]: (event.target as HTMLInputElement).value }
99
+ this.refusal = ''
100
+ }
101
+
102
+ private enter = (event: KeyboardEvent) => {
103
+ if (event.key === 'Enter' && !draftIsDeclared(this.draft, this.carrier)) this.declare()
104
+ }
105
+
106
+ private declare = () => {
107
+ const made = carrierCommandOf(this.draft, (key, params) => i18next.t(key, params))
108
+ if ('refusal' in made) this.refusal = made.refusal
109
+ else this.command(made.action)
110
+ }
111
+
112
+ private clear = () => this.command({ kind: 'set-carrier', carrier: null })
113
+
114
+ private command(action: FigureAuthoringAction) {
115
+ this.dispatchEvent(new CustomEvent('command', { detail: { action }, bubbles: true, composed: true }))
116
+ }
117
+ }
@@ -5,7 +5,7 @@ import { ref } from 'lit/directives/ref.js'
5
5
  import { i18next, localize } from '@operato/i18n'
6
6
  import { figureCavitySectionOf } from '@hatiolab/figure-model'
7
7
  import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape, FigureSection } from '@hatiolab/figure-model'
8
- import { cavityRefusalOf, cavityWorkingOf, END_FACE, endToggled, mm, SIDE_FACE, sideWallLimitOf, wallDragged } from './section-controls.js'
8
+ import { cavityRefusalWordsOf, cavityWorkingOf, END_FACE, endToggled, mm, SIDE_FACE, wallDragged } from './section-controls.js'
9
9
 
10
10
  type Axis = 'x' | 'y' | 'z'
11
11
 
@@ -87,14 +87,8 @@ export class FigureCavityDrawing extends localize(i18next)(LitElement) {
87
87
 
88
88
  /** What the kernel would refuse, in words; for one side's wall, the most that side can be with the opposite kept. */
89
89
  private judge(w: NonNullable<ReturnType<typeof cavityWorkingOf>>, side?: string): string {
90
- const ask = cavityRefusalOf(this.body!.shape, this.request(w))
91
- if (!ask) return ''
92
- if (ask.code === 'wall-too-thick')
93
- return i18next.t('figure.text.section-wall-over', {
94
- max: String(this.perSide && side ? mm(sideWallLimitOf(this.body!.shape, w.wall, side)) : ask.max)
95
- })
96
- if (ask.code === 'ends-too-thick') return i18next.t('figure.text.section-ends-over', { max: String(ask.max) })
97
- return i18next.t('figure.text.section-not-positive')
90
+ const words = cavityRefusalWordsOf(this.body!.shape, w, this.perSide, side)
91
+ return words ? i18next.t(words.key, words.params) : ''
98
92
  }
99
93
 
100
94
  /** The working inside while dragging: drawn, judged, and previewed behind when the kernel would take it. */
@@ -7,15 +7,12 @@ import { CommonPopupStyles, FormFieldStyles } from '@operato/styles'
7
7
  import '@material/web/button/filled-button.js'
8
8
  import '@material/web/button/text-button.js'
9
9
 
10
- import { firstSaveRefusalOf, releasableType, suggestTypeOf, typeOf, type FirstSave } from './first-save.js'
10
+ import { firstSaveRefusalOf, offeredTypeOf, releasableType, typeOf, type FirstSave } from './first-save.js'
11
11
 
12
12
  /**
13
13
  * The first save of a new figure: its type and its name, asked once, in the suite's popup look.
14
14
  * Closes with the answer, or with nothing when cancelled -- then nothing is saved.
15
15
  */
16
- /** The shape of an identifier the modeller offers (`suggestTypeOf`'s random form). */
17
- const OFFERED = /^fig-[0-9a-z]{6}$/
18
-
19
16
  @customElement('figure-first-save')
20
17
  export class FigureFirstSave extends LitElement {
21
18
  static styles = [
@@ -61,9 +58,9 @@ export class FigureFirstSave extends LitElement {
61
58
  */
62
59
  willUpdate(changed: Map<string, unknown>) {
63
60
  if (!(changed.has('type') || changed.has('taken'))) return
64
- const offeredAndTaken = OFFERED.test(this.type) && this.taken.includes(this.type)
65
- if (!this.type || offeredAndTaken) this.type = this.suggested = suggestTypeOf(this.name, this.taken)
66
- else if (OFFERED.test(this.type)) this.suggested = this.type
61
+ const offer = offeredTypeOf(this.type, this.name, this.taken)
62
+ this.type = offer.type
63
+ if (offer.suggested !== undefined) this.suggested = offer.suggested
67
64
  }
68
65
 
69
66
  render() {
@@ -14,6 +14,7 @@ import { getPathInfo } from '@operato/utils'
14
14
  import { fetchFigureVersions, fetchReplacedDrafts, inspectFigure, releaseFigure, revertFigureVersion, undoFigureRevert } from '../graphql/index.js'
15
15
  import type { KeptDraft } from '../graphql/index.js'
16
16
  import { afterRevertAnswer, revertOutcomeUnknown, revertRequestFor } from './revert-request.js'
17
+ import { packageHrefOf } from './modeller-route.js'
17
18
  import type { RevertRequest } from './revert-request.js'
18
19
  import type { FigureGateFinding, FigureInspection, FigureVersion } from '../types.js'
19
20
  import { OxPrompt } from '@operato/popup'
@@ -433,9 +434,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
433
434
 
434
435
  /** Where one released version downloads as a package file, or undefined before the figure has a full name. */
435
436
  private packageHref(version: number): string | undefined {
436
- if (!this.figureNamespace || !this.figureType) return undefined
437
- const { contextPath } = getPathInfo(location.pathname)
438
- return `${contextPath}/figure-package?refs=${encodeURIComponent(`${this.figureNamespace}/${this.figureType}@${version}`)}`
437
+ return packageHrefOf(getPathInfo(location.pathname).contextPath, this.figureNamespace, this.figureType, version)
439
438
  }
440
439
 
441
440
  private renderVersion(version: FigureVersion) {