@things-factory/figure-ui 10.1.115 → 10.1.117

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 (68) hide show
  1. package/client/graphql/index.ts +6 -1
  2. package/client/modeller/editor.ts +5 -11
  3. package/client/modeller/fasten-geometry.ts +1 -1
  4. package/client/modeller/figure-links.ts +52 -17
  5. package/client/modeller/joint-axis.ts +36 -0
  6. package/client/modeller/joint-drag.ts +30 -10
  7. package/client/modeller/joint-fan.ts +2 -1
  8. package/client/modeller/links.ts +70 -20
  9. package/client/modeller/recipes.ts +6 -1
  10. package/client/pages/figure-list-page.ts +4 -4
  11. package/client/types.ts +1 -1
  12. package/client/viewparts/detail-motion.ts +11 -0
  13. package/client/viewparts/figure-asset-viewer.ts +177 -0
  14. package/client/viewparts/figure-detail-dock.ts +187 -0
  15. package/dist-client/graphql/index.d.ts +5 -0
  16. package/dist-client/graphql/index.js +5 -1
  17. package/dist-client/graphql/index.js.map +1 -1
  18. package/dist-client/modeller/clips.d.ts +1 -1
  19. package/dist-client/modeller/editor.d.ts +3 -2
  20. package/dist-client/modeller/editor.js +5 -11
  21. package/dist-client/modeller/editor.js.map +1 -1
  22. package/dist-client/modeller/fasten-geometry.js +1 -1
  23. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  24. package/dist-client/modeller/figure-links.d.ts +1 -0
  25. package/dist-client/modeller/figure-links.js +65 -33
  26. package/dist-client/modeller/figure-links.js.map +1 -1
  27. package/dist-client/modeller/joint-axis.d.ts +13 -0
  28. package/dist-client/modeller/joint-axis.js +31 -0
  29. package/dist-client/modeller/joint-axis.js.map +1 -0
  30. package/dist-client/modeller/joint-drag.js +29 -9
  31. package/dist-client/modeller/joint-drag.js.map +1 -1
  32. package/dist-client/modeller/joint-fan.js +2 -1
  33. package/dist-client/modeller/joint-fan.js.map +1 -1
  34. package/dist-client/modeller/links.d.ts +34 -4
  35. package/dist-client/modeller/links.js +59 -18
  36. package/dist-client/modeller/links.js.map +1 -1
  37. package/dist-client/modeller/recipes.js +5 -1
  38. package/dist-client/modeller/recipes.js.map +1 -1
  39. package/dist-client/pages/figure-list-page.d.ts +2 -3
  40. package/dist-client/pages/figure-list-page.js +4 -4
  41. package/dist-client/pages/figure-list-page.js.map +1 -1
  42. package/dist-client/route.d.ts +1 -1
  43. package/dist-client/tsconfig.tsbuildinfo +1 -1
  44. package/dist-client/types.d.ts +1 -1
  45. package/dist-client/types.js.map +1 -1
  46. package/dist-client/viewparts/detail-motion.d.ts +6 -0
  47. package/dist-client/viewparts/detail-motion.js +9 -0
  48. package/dist-client/viewparts/detail-motion.js.map +1 -0
  49. package/dist-client/viewparts/figure-asset-viewer.d.ts +31 -0
  50. package/dist-client/viewparts/figure-asset-viewer.js +198 -0
  51. package/dist-client/viewparts/figure-asset-viewer.js.map +1 -0
  52. package/dist-client/viewparts/figure-detail-dock.d.ts +36 -0
  53. package/dist-client/viewparts/figure-detail-dock.js +224 -0
  54. package/dist-client/viewparts/figure-detail-dock.js.map +1 -0
  55. package/package.json +6 -6
  56. package/test/browser/detail-page.js +34 -0
  57. package/test/browser/serve-detail.mjs +10 -0
  58. package/test/command-preview.test.ts +2 -2
  59. package/test/detail-motion.test.ts +26 -0
  60. package/test/joint-frames.test.ts +293 -0
  61. package/test/links.test.ts +20 -1
  62. package/test/modeller-route.test.ts +4 -2
  63. package/test/verdict.test.ts +3 -1
  64. package/translations/en.json +20 -2
  65. package/translations/ja.json +20 -2
  66. package/translations/ko.json +20 -2
  67. package/translations/ms.json +20 -2
  68. package/translations/zh.json +20 -2
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 화면이 다루는 모양. 도형 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.
3
3
  */
4
- /** 서버가 돌려주는 Figure 한 벌. `source` 는 저작면에서만 받는다. */
4
+ /** 서버가 돌려주는 Figure 한 벌. `source` 는 상세 또는 저작면을 열 때만 받는다. */
5
5
  export interface Figure {
6
6
  id: string;
7
7
  /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"AAAA;;GAEG","sourcesContent":["/**\n * 화면이 다루는 모양. 도형 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.\n */\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 저작면에서만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** 도형 자산 JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.\n */\nexport interface FigurePatch {\n /** 발행 전 초안에서만 받는다 — 발행된 뒤에는 문이 거절한다(ADR-0063 결정 4). */\n type?: string\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"AAAA;;GAEG","sourcesContent":["/**\n * 화면이 다루는 모양. 도형 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.\n */\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 상세 또는 저작면을 열 때만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** 도형 자산 JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.\n */\nexport interface FigurePatch {\n /** 발행 전 초안에서만 받는다 — 발행된 뒤에는 문이 거절한다(ADR-0063 결정 4). */\n type?: string\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { FigureAsset } from '@hatiolab/figure-model';
2
+ /** External values win over drivers; release only the controls owned by the chosen clip. */
3
+ export declare function parametersForClip(asset: FigureAsset, values: Record<string, number>, clip: string): {
4
+ [k: string]: number;
5
+ };
6
+ export declare function clipForControl(asset: FigureAsset, id: string): string | undefined;
@@ -0,0 +1,9 @@
1
+ /** External values win over drivers; release only the controls owned by the chosen clip. */
2
+ export function parametersForClip(asset, values, clip) {
3
+ const driven = new Set((asset.drivers ?? []).filter(d => d.clip === clip).map(d => d.state));
4
+ return Object.fromEntries(Object.entries(values).filter(([id]) => !driven.has(id)));
5
+ }
6
+ export function clipForControl(asset, id) {
7
+ return asset.drivers?.find(d => d.state === id)?.clip;
8
+ }
9
+ //# sourceMappingURL=detail-motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detail-motion.js","sourceRoot":"","sources":["../../client/viewparts/detail-motion.ts"],"names":[],"mappings":"AAEA,4FAA4F;AAC5F,MAAM,UAAU,iBAAiB,CAAC,KAAkB,EAAE,MAA8B,EAAE,IAAY;IAChG,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;IAC5F,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;AACrF,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAkB,EAAE,EAAU;IAC3D,OAAO,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,EAAE,IAAI,CAAA;AACvD,CAAC","sourcesContent":["import type { FigureAsset } from '@hatiolab/figure-model'\n\n/** External values win over drivers; release only the controls owned by the chosen clip. */\nexport function parametersForClip(asset: FigureAsset, values: Record<string, number>, clip: string) {\n const driven = new Set((asset.drivers ?? []).filter(d => d.clip === clip).map(d => d.state))\n return Object.fromEntries(Object.entries(values).filter(([id]) => !driven.has(id)))\n}\n\nexport function clipForControl(asset: FigureAsset, id: string) {\n return asset.drivers?.find(d => d.state === id)?.clip\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import { LitElement, type PropertyValues } from 'lit';
2
+ import { type FigureAsset } from '@hatiolab/figure-model';
3
+ declare const FigureAssetViewer_base: (new (...args: any[]) => LitElement) & typeof LitElement;
4
+ /** A single board instance. Sizes and poses are transient, and never change the asset or call a mutation. */
5
+ export declare class FigureAssetViewer extends FigureAssetViewer_base {
6
+ static styles: import("lit").CSSResult;
7
+ asset?: FigureAsset;
8
+ private stage;
9
+ private sizes;
10
+ private values;
11
+ private playing;
12
+ private error;
13
+ private scene?;
14
+ private registered?;
15
+ private observer?;
16
+ private needsScene;
17
+ private played;
18
+ connectedCallback(): void;
19
+ protected updated(changed: PropertyValues): void;
20
+ firstUpdated(): void;
21
+ disconnectedCallback(): void;
22
+ render(): import("lit-html").TemplateResult<1>;
23
+ private tell;
24
+ private resize;
25
+ private pose;
26
+ private play;
27
+ private reset;
28
+ private standUp;
29
+ private teardown;
30
+ }
31
+ export {};
@@ -0,0 +1,198 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { css, html, LitElement, nothing } from 'lit';
3
+ import { customElement, property, query, state } from 'lit/decorators.js';
4
+ import { i18next, localize } from '@operato/i18n';
5
+ import { compileFigureAsset, FIGURE_INSTANCE_SIZE_INPUTS } from '@hatiolab/figure-model';
6
+ import { create, registerFigure, unregisterFigure } from '@hatiolab/things-scene';
7
+ import { authoredSizeOf } from '../modeller/preview-model.js';
8
+ import { controlsOf } from '../modeller/controls.js';
9
+ import { DEFAULT_VIEW, previewBoardDepth, sceneLook } from '../modeller/figure-view.js';
10
+ import { clipForControl, parametersForClip } from './detail-motion.js';
11
+ /** A single board instance. Sizes and poses are transient, and never change the asset or call a mutation. */
12
+ let FigureAssetViewer = class FigureAssetViewer extends localize(i18next)(LitElement) {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.sizes = { x: 1, y: 1, z: 1 };
16
+ this.values = {};
17
+ this.playing = '';
18
+ this.error = '';
19
+ this.needsScene = false;
20
+ this.played = new Set();
21
+ }
22
+ static { this.styles = css `
23
+ :host { display: block; min-width: 0; }
24
+ [viewport] { height: 300px; position: relative; background: #858585; }
25
+ [stage] { position: absolute; inset: 0; width: 100%; height: 100%; }
26
+ section { padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }
27
+ h3 { margin: 0 0 10px; font-size: var(--ui-t-body, 13px); }
28
+ label { display: grid; grid-template-columns: 72px minmax(0, 1fr) 90px; align-items: center; gap: 8px; margin: 8px 0; }
29
+ input { min-width: 0; width: 100%; box-sizing: border-box; }
30
+ input[type=number] { padding: 6px; }
31
+ input, button { font: inherit; color: inherit; }
32
+ button { padding: 8px 12px; border: 1px solid var(--md-sys-color-outline); border-radius: 8px; background: var(--md-sys-color-surface); cursor: pointer; }
33
+ [clips] { display: flex; flex-wrap: wrap; gap: 8px; }
34
+ p { font-size: 12px; line-height: 1.5; }
35
+ [error] { color: var(--md-sys-color-error); }
36
+ `; }
37
+ connectedCallback() {
38
+ super.connectedCallback();
39
+ this.needsScene = true;
40
+ if (this.hasUpdated)
41
+ this.observer?.observe(this.stage);
42
+ this.requestUpdate();
43
+ }
44
+ updated(changed) {
45
+ // Reconnection and a new asset can arrive in the same update. Build exactly once.
46
+ if (changed.has('asset') || this.needsScene) {
47
+ this.needsScene = false;
48
+ this.standUp();
49
+ }
50
+ }
51
+ firstUpdated() {
52
+ this.observer = new ResizeObserver(() => this.scene?.resize?.());
53
+ this.observer.observe(this.stage);
54
+ }
55
+ disconnectedCallback() {
56
+ super.disconnectedCallback();
57
+ this.observer?.disconnect();
58
+ this.teardown();
59
+ }
60
+ render() {
61
+ const asset = this.asset;
62
+ return html `<div viewport><div stage></div></div>
63
+ ${this.error ? html `<p error role="alert">${this.error}</p>` : nothing}
64
+ ${asset ? html `<section>
65
+ <h3>${i18next.t('figure.label.detail-size')}</h3>
66
+ ${['x', 'y', 'z'].map(axis => {
67
+ const input = asset.document.model.inputs.find(input => input.id === FIGURE_INSTANCE_SIZE_INPUTS[axis]);
68
+ return html `<label><span>${axis.toUpperCase()} · mm</span>
69
+ <input type="range" aria-label=${`${axis.toUpperCase()} mm`} min=${input?.min ?? this.sizes[axis]} max=${input?.max ?? this.sizes[axis]} step="any"
70
+ .value=${String(this.sizes[axis])} ?disabled=${!input || input.min === input.max}
71
+ @input=${(e) => this.resize(axis, e.target.valueAsNumber)}>
72
+ <input type="number" aria-label=${`${axis.toUpperCase()} mm value`} min=${input?.min ?? this.sizes[axis]} max=${input?.max ?? this.sizes[axis]} step="any"
73
+ .value=${String(this.sizes[axis])} ?disabled=${!input || input.min === input.max}
74
+ @change=${(e) => this.resize(axis, e.target.valueAsNumber)}>
75
+ </label>`;
76
+ })}
77
+ <button @click=${() => this.reset()}>${i18next.t('figure.button.detail-reset')}</button>
78
+ <p>${i18next.t('figure.text.detail-transient')}</p>
79
+ </section>
80
+ <section><h3>${i18next.t('figure.label.motion')}</h3>
81
+ ${controlsOf(asset, this.values).map(c => html `<label><span>${c.label} · ${c.unit}</span>
82
+ <input type="range" aria-label=${c.label} min=${c.min} max=${c.max} step=${c.step || 'any'} .value=${String(c.value)}
83
+ ?disabled=${!!this.playing && clipForControl(asset, c.id) === this.playing}
84
+ @input=${(e) => this.pose(c.id, e.target.valueAsNumber)}>
85
+ <output>${this.playing && clipForControl(asset, c.id) === this.playing ? i18next.t('figure.text.detail-playing')
86
+ : this.played.has(clipForControl(asset, c.id) ?? '') && !(c.id in this.values) ? i18next.t('figure.text.detail-paused') : Math.round(c.value * 100) / 100}</output></label>`)}
87
+ <div clips>${[...new Set((asset.drivers ?? []).map(d => d.clip))].map(name => html `<button aria-pressed=${String(this.playing === name)}
88
+ @click=${() => this.play(name)}>${this.playing === name ? '⏸' : '▶'} ${name}</button>`)}</div>
89
+ ${!controlsOf(asset).length && ![...new Set((asset.drivers ?? []).map(d => d.clip))].length ? html `<p>${i18next.t('figure.text.detail-no-motion')}</p>` : nothing}
90
+ </section>` : nothing}`;
91
+ }
92
+ tell(key, value) {
93
+ this.scene?.root?.components?.[0]?.set(key, value);
94
+ }
95
+ resize(axis, value) {
96
+ const input = this.asset?.document.model.inputs.find(input => input.id === FIGURE_INSTANCE_SIZE_INPUTS[axis]);
97
+ if (!input || !Number.isFinite(value)) {
98
+ this.requestUpdate();
99
+ return;
100
+ }
101
+ const used = Math.min(input.max, Math.max(input.min, value));
102
+ this.sizes = { ...this.sizes, [axis]: used };
103
+ this.tell(axis === 'x' ? 'width' : axis === 'y' ? 'depth' : 'height', used);
104
+ this.scene?.root?.fitCameraToModel?.();
105
+ }
106
+ pose(id, value) {
107
+ if (!Number.isFinite(value))
108
+ return;
109
+ this.values = { ...this.values, [id]: value };
110
+ this.tell('parameters', this.values);
111
+ }
112
+ play(name) {
113
+ this.playing = this.playing === name ? '' : name;
114
+ if (this.playing && this.asset) {
115
+ this.played.add(name);
116
+ this.values = parametersForClip(this.asset, this.values, name);
117
+ this.tell('parameters', this.values);
118
+ }
119
+ this.tell('clips', Object.fromEntries([...new Set((this.asset?.drivers ?? []).map(d => d.clip))].map(key => [key, key === this.playing ? 1 : 0])));
120
+ }
121
+ reset() {
122
+ if (!this.asset)
123
+ return;
124
+ // A new instance also resets driver clocks. Clearing speeds alone freezes the last pose.
125
+ this.standUp();
126
+ }
127
+ standUp() {
128
+ this.teardown();
129
+ this.error = '';
130
+ const asset = this.asset;
131
+ if (!asset)
132
+ return;
133
+ this.values = {};
134
+ this.playing = '';
135
+ this.played.clear();
136
+ try {
137
+ compileFigureAsset(asset);
138
+ if (!asset.appearance.length)
139
+ throw Error(i18next.t('figure.text.preview-needs-a-part'));
140
+ this.sizes = authoredSizeOf(asset);
141
+ const type = `figure-detail-${crypto.randomUUID()}`;
142
+ registerFigure(type, asset);
143
+ this.registered = type;
144
+ const size = this.sizes;
145
+ this.scene = create({ target: this.stage, mode: 0, model: {
146
+ type: 'model-layer', width: size.x * 2, height: size.z * 2,
147
+ depth: previewBoardDepth({ placement: asset.placement, base: { y: size.y } }, DEFAULT_VIEW.ceilingHeight, 1),
148
+ threed: true, sky: 'color', ...sceneLook(DEFAULT_VIEW),
149
+ placement: DEFAULT_VIEW.placement, heights: { floor: 0, ceiling: DEFAULT_VIEW.ceilingHeight },
150
+ pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
151
+ components: [{ type, id: 'detail-instance', left: size.x / 2, top: size.z / 2, width: size.x, height: size.z, depth: size.y }]
152
+ } });
153
+ this.scene.resize?.();
154
+ this.scene.fit?.('ratio');
155
+ this.scene.root?.fitCameraToModel?.();
156
+ }
157
+ catch (error) {
158
+ this.error = error.message;
159
+ this.teardown();
160
+ }
161
+ }
162
+ teardown() {
163
+ this.scene?.dispose?.();
164
+ this.scene = undefined;
165
+ if (this.registered)
166
+ unregisterFigure(this.registered);
167
+ this.registered = undefined;
168
+ }
169
+ };
170
+ __decorate([
171
+ property({ attribute: false }),
172
+ __metadata("design:type", Object)
173
+ ], FigureAssetViewer.prototype, "asset", void 0);
174
+ __decorate([
175
+ query('[stage]'),
176
+ __metadata("design:type", HTMLElement)
177
+ ], FigureAssetViewer.prototype, "stage", void 0);
178
+ __decorate([
179
+ state(),
180
+ __metadata("design:type", Object)
181
+ ], FigureAssetViewer.prototype, "sizes", void 0);
182
+ __decorate([
183
+ state(),
184
+ __metadata("design:type", Object)
185
+ ], FigureAssetViewer.prototype, "values", void 0);
186
+ __decorate([
187
+ state(),
188
+ __metadata("design:type", Object)
189
+ ], FigureAssetViewer.prototype, "playing", void 0);
190
+ __decorate([
191
+ state(),
192
+ __metadata("design:type", Object)
193
+ ], FigureAssetViewer.prototype, "error", void 0);
194
+ FigureAssetViewer = __decorate([
195
+ customElement('figure-asset-viewer')
196
+ ], FigureAssetViewer);
197
+ export { FigureAssetViewer };
198
+ //# sourceMappingURL=figure-asset-viewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-asset-viewer.js","sourceRoot":"","sources":["../../client/viewparts/figure-asset-viewer.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, nothing, type PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { compileFigureAsset, FIGURE_INSTANCE_SIZE_INPUTS, type FigureAsset } from '@hatiolab/figure-model'\nimport { create, registerFigure, unregisterFigure } from '@hatiolab/things-scene'\nimport { authoredSizeOf } from '../modeller/preview-model.js'\nimport { controlsOf } from '../modeller/controls.js'\nimport { DEFAULT_VIEW, previewBoardDepth, sceneLook } from '../modeller/figure-view.js'\nimport { clipForControl, parametersForClip } from './detail-motion.js'\n\ntype Axis = 'x' | 'y' | 'z'\ninterface Scene {\n dispose?(): void\n resize?(): void\n fit?(type?: string): void\n root?: {\n fitCameraToModel?(): void\n components?: Array<{ set(key: string, value: unknown): void }>\n }\n}\n\n/** A single board instance. Sizes and poses are transient, and never change the asset or call a mutation. */\n@customElement('figure-asset-viewer')\nexport class FigureAssetViewer extends localize(i18next)(LitElement) {\n static styles = css`\n :host { display: block; min-width: 0; }\n [viewport] { height: 300px; position: relative; background: #858585; }\n [stage] { position: absolute; inset: 0; width: 100%; height: 100%; }\n section { padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }\n h3 { margin: 0 0 10px; font-size: var(--ui-t-body, 13px); }\n label { display: grid; grid-template-columns: 72px minmax(0, 1fr) 90px; align-items: center; gap: 8px; margin: 8px 0; }\n input { min-width: 0; width: 100%; box-sizing: border-box; }\n input[type=number] { padding: 6px; }\n input, button { font: inherit; color: inherit; }\n button { padding: 8px 12px; border: 1px solid var(--md-sys-color-outline); border-radius: 8px; background: var(--md-sys-color-surface); cursor: pointer; }\n [clips] { display: flex; flex-wrap: wrap; gap: 8px; }\n p { font-size: 12px; line-height: 1.5; }\n [error] { color: var(--md-sys-color-error); }\n `\n @property({ attribute: false }) asset?: FigureAsset\n @query('[stage]') private stage!: HTMLElement\n @state() private sizes: Record<Axis, number> = { x: 1, y: 1, z: 1 }\n @state() private values: Record<string, number> = {}\n @state() private playing = ''\n @state() private error = ''\n private scene?: Scene\n private registered?: string\n private observer?: ResizeObserver\n private needsScene = false\n private played = new Set<string>()\n\n connectedCallback() {\n super.connectedCallback()\n this.needsScene = true\n if (this.hasUpdated) this.observer?.observe(this.stage)\n this.requestUpdate()\n }\n\n protected updated(changed: PropertyValues) {\n // Reconnection and a new asset can arrive in the same update. Build exactly once.\n if (changed.has('asset') || this.needsScene) {\n this.needsScene = false\n this.standUp()\n }\n }\n firstUpdated() {\n this.observer = new ResizeObserver(() => this.scene?.resize?.())\n this.observer.observe(this.stage)\n }\n disconnectedCallback() {\n super.disconnectedCallback()\n this.observer?.disconnect()\n this.teardown()\n }\n\n render() {\n const asset = this.asset\n return html`<div viewport><div stage></div></div>\n ${this.error ? html`<p error role=\"alert\">${this.error}</p>` : nothing}\n ${asset ? html`<section>\n <h3>${i18next.t('figure.label.detail-size')}</h3>\n ${(['x', 'y', 'z'] as const).map(axis => {\n const input = asset.document.model.inputs.find(input => input.id === FIGURE_INSTANCE_SIZE_INPUTS[axis])\n return html`<label><span>${axis.toUpperCase()} · mm</span>\n <input type=\"range\" aria-label=${`${axis.toUpperCase()} mm`} min=${input?.min ?? this.sizes[axis]} max=${input?.max ?? this.sizes[axis]} step=\"any\"\n .value=${String(this.sizes[axis])} ?disabled=${!input || input.min === input.max}\n @input=${(e: Event) => this.resize(axis, (e.target as HTMLInputElement).valueAsNumber)}>\n <input type=\"number\" aria-label=${`${axis.toUpperCase()} mm value`} min=${input?.min ?? this.sizes[axis]} max=${input?.max ?? this.sizes[axis]} step=\"any\"\n .value=${String(this.sizes[axis])} ?disabled=${!input || input.min === input.max}\n @change=${(e: Event) => this.resize(axis, (e.target as HTMLInputElement).valueAsNumber)}>\n </label>`\n })}\n <button @click=${() => this.reset()}>${i18next.t('figure.button.detail-reset')}</button>\n <p>${i18next.t('figure.text.detail-transient')}</p>\n </section>\n <section><h3>${i18next.t('figure.label.motion')}</h3>\n ${controlsOf(asset, this.values).map(c => html`<label><span>${c.label} · ${c.unit}</span>\n <input type=\"range\" aria-label=${c.label} min=${c.min} max=${c.max} step=${c.step || 'any'} .value=${String(c.value)}\n ?disabled=${!!this.playing && clipForControl(asset, c.id) === this.playing}\n @input=${(e: Event) => this.pose(c.id, (e.target as HTMLInputElement).valueAsNumber)}>\n <output>${this.playing && clipForControl(asset, c.id) === this.playing ? i18next.t('figure.text.detail-playing')\n : this.played.has(clipForControl(asset, c.id) ?? '') && !(c.id in this.values) ? i18next.t('figure.text.detail-paused') : Math.round(c.value * 100) / 100}</output></label>`)}\n <div clips>${[...new Set((asset.drivers ?? []).map(d => d.clip))].map(name => html`<button aria-pressed=${String(this.playing === name)}\n @click=${() => this.play(name)}>${this.playing === name ? '⏸' : '▶'} ${name}</button>`)}</div>\n ${!controlsOf(asset).length && ![...new Set((asset.drivers ?? []).map(d => d.clip))].length ? html`<p>${i18next.t('figure.text.detail-no-motion')}</p>` : nothing}\n </section>` : nothing}`\n }\n\n private tell(key: string, value: unknown) {\n this.scene?.root?.components?.[0]?.set(key, value)\n }\n private resize(axis: Axis, value: number) {\n const input = this.asset?.document.model.inputs.find(input => input.id === FIGURE_INSTANCE_SIZE_INPUTS[axis])\n if (!input || !Number.isFinite(value)) { this.requestUpdate(); return }\n const used = Math.min(input.max, Math.max(input.min, value))\n this.sizes = { ...this.sizes, [axis]: used }\n this.tell(axis === 'x' ? 'width' : axis === 'y' ? 'depth' : 'height', used)\n this.scene?.root?.fitCameraToModel?.()\n }\n private pose(id: string, value: number) {\n if (!Number.isFinite(value)) return\n this.values = { ...this.values, [id]: value }\n this.tell('parameters', this.values)\n }\n private play(name: string) {\n this.playing = this.playing === name ? '' : name\n if (this.playing && this.asset) {\n this.played.add(name)\n this.values = parametersForClip(this.asset, this.values, name)\n this.tell('parameters', this.values)\n }\n this.tell('clips', Object.fromEntries([...new Set((this.asset?.drivers ?? []).map(d => d.clip))].map(key => [key, key === this.playing ? 1 : 0])))\n }\n private reset() {\n if (!this.asset) return\n // A new instance also resets driver clocks. Clearing speeds alone freezes the last pose.\n this.standUp()\n }\n private standUp() {\n this.teardown()\n this.error = ''\n const asset = this.asset\n if (!asset) return\n this.values = {}\n this.playing = ''\n this.played.clear()\n try {\n compileFigureAsset(asset)\n if (!asset.appearance.length) throw Error(i18next.t('figure.text.preview-needs-a-part'))\n this.sizes = authoredSizeOf(asset)\n const type = `figure-detail-${crypto.randomUUID()}`\n registerFigure(type, asset)\n this.registered = type\n const size = this.sizes\n this.scene = create({ target: this.stage, mode: 0, model: {\n type: 'model-layer', width: size.x * 2, height: size.z * 2,\n depth: previewBoardDepth({ placement: asset.placement, base: { y: size.y } }, DEFAULT_VIEW.ceilingHeight, 1),\n threed: true, sky: 'color', ...sceneLook(DEFAULT_VIEW),\n placement: DEFAULT_VIEW.placement, heights: { floor: 0, ceiling: DEFAULT_VIEW.ceilingHeight },\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n components: [{ type, id: 'detail-instance', left: size.x / 2, top: size.z / 2, width: size.x, height: size.z, depth: size.y }]\n } }) as Scene\n this.scene.resize?.()\n this.scene.fit?.('ratio')\n this.scene.root?.fitCameraToModel?.()\n } catch (error) {\n this.error = (error as Error).message\n this.teardown()\n }\n }\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n if (this.registered) unregisterFigure(this.registered)\n this.registered = undefined\n }\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { LitElement } from 'lit';
2
+ import type { Figure } from '../types.js';
3
+ import './figure-asset-viewer.js';
4
+ export interface FigureDetailRecord {
5
+ saved: Figure;
6
+ released?: {
7
+ version: number;
8
+ source: string;
9
+ };
10
+ }
11
+ declare const FigureDetailDock_base: (new (...args: any[]) => LitElement) & typeof LitElement;
12
+ /** Catalogue details outlive the page; choosing a size or a pose has no write path. */
13
+ export declare class FigureDetailDock extends FigureDetailDock_base {
14
+ static styles: import("lit").CSSResult;
15
+ load: (id: string) => Promise<FigureDetailRecord>;
16
+ private figureId;
17
+ private record?;
18
+ private asset?;
19
+ private sourceKind;
20
+ private loading;
21
+ private error;
22
+ private cost?;
23
+ private ticket;
24
+ private unsubscribe?;
25
+ private boundary?;
26
+ private boundaryObserver?;
27
+ show(id: string): Promise<void>;
28
+ private choose;
29
+ connectedCallback(): void;
30
+ disconnectedCallback(): void;
31
+ private place;
32
+ private escape;
33
+ render(): import("lit-html").TemplateResult<1>;
34
+ }
35
+ export declare function openFigureDetail(figure: Figure): void;
36
+ export {};
@@ -0,0 +1,224 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { css, html, LitElement, nothing } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { i18next, localize } from '@operato/i18n';
5
+ import { appendViewpart, UPDATE_VIEWPART, VIEWPART_POSITION, VIEWPART_LEVEL } from '@operato/layout';
6
+ import { navigate, store } from '@operato/shell';
7
+ import { ASSISTANT_DOCK_VIEWPART } from '@things-factory/ai-assistant/dist-client/components/assistant-dock.js';
8
+ import { figureCostOf, parseFigureAsset } from '@hatiolab/figure-model';
9
+ import { capacityFrom } from '../modeller/scene-budget.js';
10
+ import { fetchFigure, fetchReleasedFigure } from '../graphql/index.js';
11
+ import { modellerPathOf } from '../modeller/modeller-route.js';
12
+ import './figure-asset-viewer.js';
13
+ const VIEWPART = 'figure-detail';
14
+ /** Catalogue details outlive the page; choosing a size or a pose has no write path. */
15
+ let FigureDetailDock = class FigureDetailDock extends localize(i18next)(LitElement) {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.load = async (id) => {
19
+ const [saved, released] = await Promise.all([fetchFigure(id), fetchReleasedFigure(id)]);
20
+ if (!saved?.source)
21
+ throw Error(i18next.t('figure.text.detail-no-source'));
22
+ return { saved, released };
23
+ };
24
+ this.figureId = '';
25
+ this.sourceKind = 'saved';
26
+ this.loading = false;
27
+ this.error = '';
28
+ this.ticket = 0;
29
+ this.place = () => {
30
+ const bar = visibleBar(document);
31
+ if (bar !== this.boundary) {
32
+ this.boundaryObserver?.disconnect();
33
+ this.boundary = bar;
34
+ if (bar) {
35
+ this.boundaryObserver = new ResizeObserver(this.place);
36
+ this.boundaryObserver.observe(bar);
37
+ }
38
+ }
39
+ this.style.setProperty('--figure-detail-top', `${bar?.getBoundingClientRect().bottom ?? 44}px`);
40
+ };
41
+ this.escape = (e) => {
42
+ if (e.key === 'Escape')
43
+ hideDetail();
44
+ };
45
+ }
46
+ static { this.styles = css `
47
+ :host { display: flex; flex-direction: column; width: 440px; max-width: 100vw; height: 100%; min-height: 0;
48
+ color: var(--md-sys-color-on-surface); background: var(--md-sys-color-surface); border-left: 1px solid var(--md-sys-color-outline-variant); font-size: 13px; }
49
+ header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }
50
+ h2 { margin: 0; font-size: var(--ui-t-title, 16px); }
51
+ button, select { font: inherit; color: inherit; padding: 8px 12px; border: 1px solid var(--md-sys-color-outline); border-radius: 8px; background: var(--md-sys-color-surface); cursor: pointer; }
52
+ [body] { flex: 1; overflow: auto; min-height: 0; }
53
+ [meta], [status] { padding: 12px 16px; }
54
+ [meta] { display: grid; gap: 8px; }
55
+ [summary] { padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }
56
+ h3 { margin: 0 0 8px; font-size: var(--ui-t-body, 13px); }
57
+ dl { display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; margin: 0; }
58
+ dd { margin: 0; font-variant-numeric: tabular-nums; }
59
+ p { margin: 0; line-height: 1.5; }
60
+ code { color: var(--md-sys-color-on-surface-variant); }
61
+ footer { padding: 12px 16px; border-top: 1px solid var(--md-sys-color-outline-variant); }
62
+ footer button { width: 100%; background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); }
63
+ @media (max-width: 1439.98px) {
64
+ :host { position: fixed; inset: var(--figure-detail-top, 44px) 0 0 auto; height: calc(100dvh - var(--figure-detail-top, 44px)); z-index: 11; box-shadow: -4px 0 16px rgb(0 0 0 / 20%); }
65
+ }
66
+ `; }
67
+ async show(id) {
68
+ const ticket = ++this.ticket;
69
+ this.figureId = id;
70
+ this.record = undefined;
71
+ this.asset = undefined;
72
+ this.cost = undefined;
73
+ this.loading = true;
74
+ this.error = '';
75
+ try {
76
+ const record = await this.load(id);
77
+ if (ticket !== this.ticket)
78
+ return;
79
+ this.record = record;
80
+ this.choose(record.released ? 'released' : 'saved');
81
+ }
82
+ catch {
83
+ if (ticket === this.ticket)
84
+ this.error = i18next.t('figure.text.detail-load-failed');
85
+ }
86
+ finally {
87
+ if (ticket === this.ticket)
88
+ this.loading = false;
89
+ }
90
+ }
91
+ choose(kind) {
92
+ this.sourceKind = kind;
93
+ this.asset = undefined;
94
+ this.cost = undefined;
95
+ this.error = '';
96
+ try {
97
+ const source = kind === 'released' ? this.record?.released?.source : this.record?.saved.source;
98
+ if (!source)
99
+ throw Error('missing source');
100
+ const asset = parseFigureAsset(source);
101
+ this.cost = figureCostOf(asset);
102
+ this.asset = asset;
103
+ }
104
+ catch {
105
+ this.error = i18next.t('figure.text.detail-invalid-source');
106
+ }
107
+ }
108
+ connectedCallback() {
109
+ super.connectedCallback();
110
+ window.addEventListener('resize', this.place);
111
+ window.addEventListener('popstate', this.place);
112
+ this.unsubscribe = store.subscribe(() => {
113
+ const state = store.getState();
114
+ const parts = state.layout?.viewparts;
115
+ if (parts?.[ASSISTANT_DOCK_VIEWPART]?.show && parts?.[VIEWPART]?.show)
116
+ hideDetail();
117
+ else
118
+ requestAnimationFrame(this.place);
119
+ });
120
+ this.place();
121
+ window.addEventListener('keydown', this.escape);
122
+ }
123
+ disconnectedCallback() {
124
+ super.disconnectedCallback();
125
+ window.removeEventListener('resize', this.place);
126
+ window.removeEventListener('popstate', this.place);
127
+ window.removeEventListener('keydown', this.escape);
128
+ this.unsubscribe?.();
129
+ this.boundaryObserver?.disconnect();
130
+ this.boundary = undefined;
131
+ }
132
+ render() {
133
+ const saved = this.record?.saved;
134
+ const cost = this.cost;
135
+ const capacity = cost ? capacityFrom(cost.groups, cost.triangles) : undefined;
136
+ return html `<header><h2>${saved?.name || i18next.t('figure.title.detail')}</h2>
137
+ <button aria-label=${i18next.t('figure.button.close')} @click=${() => hideDetail()}>×</button></header>
138
+ <div body>
139
+ ${this.loading ? html `<p status role="status">${i18next.t('figure.text.detail-loading')}</p>` : nothing}
140
+ ${this.error ? html `<div status role="alert"><p>${this.error}</p><button @click=${() => this.show(this.figureId)}>${i18next.t('figure.button.detail-retry')}</button></div>` : nothing}
141
+ ${saved ? html `<section meta><code>${saved.type}</code><p>${saved.description || ''}</p>
142
+ <label>${i18next.t('figure.label.detail-source')}
143
+ <select aria-label=${i18next.t('figure.label.detail-source')} .value=${this.sourceKind}
144
+ @change=${(e) => this.choose(e.target.value)}>
145
+ ${this.record?.released ? html `<option value="released">${i18next.t('figure.label.detail-released', { version: this.record.released.version })}</option>` : nothing}
146
+ <option value="saved">${i18next.t('figure.label.detail-saved')} · ${i18next.t(saved.state === 'released' ? 'figure.label.figure-released' : 'figure.label.draft')}</option>
147
+ </select></label>
148
+ ${!this.record?.released ? html `<p>${i18next.t('figure.text.detail-unreleased')}</p>` : nothing}
149
+ </section>` : nothing}
150
+ ${this.asset ? html `<figure-asset-viewer .asset=${this.asset}></figure-asset-viewer>
151
+ <section summary><h3>${i18next.t('figure.label.detail-summary')}</h3><dl>
152
+ <dt>${i18next.t('figure.label.budget-parts')}</dt><dd>${this.asset.appearance.length}</dd>
153
+ <dt>${i18next.t('figure.label.budget-groups')}</dt><dd>${cost?.groups ?? '—'}</dd>
154
+ <dt>${i18next.t('figure.label.triangles')}</dt><dd>${cost?.triangles ?? '—'}</dd>
155
+ </dl>${capacity ? html `<p>${i18next.t('figure.text.parts-budget', { triangles: cost.triangles, groups: cost.groups, instances: capacity.instances })}</p>` : nothing}
156
+ </section>` : nothing}
157
+ </div>
158
+ <footer><button ?disabled=${!saved} @click=${() => {
159
+ navigate(modellerPathOf(saved));
160
+ requestAnimationFrame(() => requestAnimationFrame(this.place));
161
+ }}>${i18next.t('figure.button.detail-edit')}</button></footer>`;
162
+ }
163
+ };
164
+ __decorate([
165
+ property({ attribute: false }),
166
+ __metadata("design:type", Function)
167
+ ], FigureDetailDock.prototype, "load", void 0);
168
+ __decorate([
169
+ state(),
170
+ __metadata("design:type", Object)
171
+ ], FigureDetailDock.prototype, "figureId", void 0);
172
+ __decorate([
173
+ state(),
174
+ __metadata("design:type", Object)
175
+ ], FigureDetailDock.prototype, "record", void 0);
176
+ __decorate([
177
+ state(),
178
+ __metadata("design:type", Object)
179
+ ], FigureDetailDock.prototype, "asset", void 0);
180
+ __decorate([
181
+ state(),
182
+ __metadata("design:type", String)
183
+ ], FigureDetailDock.prototype, "sourceKind", void 0);
184
+ __decorate([
185
+ state(),
186
+ __metadata("design:type", Object)
187
+ ], FigureDetailDock.prototype, "loading", void 0);
188
+ __decorate([
189
+ state(),
190
+ __metadata("design:type", Object)
191
+ ], FigureDetailDock.prototype, "error", void 0);
192
+ __decorate([
193
+ state(),
194
+ __metadata("design:type", void 0)
195
+ ], FigureDetailDock.prototype, "cost", void 0);
196
+ FigureDetailDock = __decorate([
197
+ customElement('figure-detail-dock')
198
+ ], FigureDetailDock);
199
+ export { FigureDetailDock };
200
+ function visibleBar(root) {
201
+ for (const element of root.querySelectorAll('*')) {
202
+ if (element.tagName === 'OX-CANVAS-BAR' && element.getBoundingClientRect().height > 0)
203
+ return element;
204
+ if (element.shadowRoot) {
205
+ const found = visibleBar(element.shadowRoot);
206
+ if (found)
207
+ return found;
208
+ }
209
+ }
210
+ }
211
+ let dock;
212
+ function hideDetail() {
213
+ store.dispatch({ type: UPDATE_VIEWPART, name: VIEWPART, override: { show: false } });
214
+ }
215
+ export function openFigureDetail(figure) {
216
+ if (!dock) {
217
+ dock = document.createElement('figure-detail-dock');
218
+ }
219
+ store.dispatch({ type: UPDATE_VIEWPART, name: ASSISTANT_DOCK_VIEWPART, override: { show: false } });
220
+ appendViewpart({ name: VIEWPART, position: VIEWPART_POSITION.ASIDEBAR,
221
+ viewpart: { show: true, persistent: true, level: VIEWPART_LEVEL.TOPMOST, template: html `${dock}` } });
222
+ void dock.show(figure.id);
223
+ }
224
+ //# sourceMappingURL=figure-detail-dock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-detail-dock.js","sourceRoot":"","sources":["../../client/viewparts/figure-detail-dock.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { appendViewpart, UPDATE_VIEWPART, VIEWPART_POSITION, VIEWPART_LEVEL } from '@operato/layout'\nimport { navigate, store } from '@operato/shell'\nimport { ASSISTANT_DOCK_VIEWPART } from '@things-factory/ai-assistant/dist-client/components/assistant-dock.js'\nimport { figureCostOf, parseFigureAsset, type FigureAsset } from '@hatiolab/figure-model'\nimport { capacityFrom } from '../modeller/scene-budget.js'\nimport { fetchFigure, fetchReleasedFigure } from '../graphql/index.js'\nimport { modellerPathOf } from '../modeller/modeller-route.js'\nimport type { Figure } from '../types.js'\nimport './figure-asset-viewer.js'\n\nconst VIEWPART = 'figure-detail'\nexport interface FigureDetailRecord {\n saved: Figure\n released?: { version: number; source: string }\n}\n\n/** Catalogue details outlive the page; choosing a size or a pose has no write path. */\n@customElement('figure-detail-dock')\nexport class FigureDetailDock extends localize(i18next)(LitElement) {\n static styles = css`\n :host { display: flex; flex-direction: column; width: 440px; max-width: 100vw; height: 100%; min-height: 0;\n color: var(--md-sys-color-on-surface); background: var(--md-sys-color-surface); border-left: 1px solid var(--md-sys-color-outline-variant); font-size: 13px; }\n header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }\n h2 { margin: 0; font-size: var(--ui-t-title, 16px); }\n button, select { font: inherit; color: inherit; padding: 8px 12px; border: 1px solid var(--md-sys-color-outline); border-radius: 8px; background: var(--md-sys-color-surface); cursor: pointer; }\n [body] { flex: 1; overflow: auto; min-height: 0; }\n [meta], [status] { padding: 12px 16px; }\n [meta] { display: grid; gap: 8px; }\n [summary] { padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }\n h3 { margin: 0 0 8px; font-size: var(--ui-t-body, 13px); }\n dl { display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; margin: 0; }\n dd { margin: 0; font-variant-numeric: tabular-nums; }\n p { margin: 0; line-height: 1.5; }\n code { color: var(--md-sys-color-on-surface-variant); }\n footer { padding: 12px 16px; border-top: 1px solid var(--md-sys-color-outline-variant); }\n footer button { width: 100%; background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); }\n @media (max-width: 1439.98px) {\n :host { position: fixed; inset: var(--figure-detail-top, 44px) 0 0 auto; height: calc(100dvh - var(--figure-detail-top, 44px)); z-index: 11; box-shadow: -4px 0 16px rgb(0 0 0 / 20%); }\n }\n `\n @property({ attribute: false }) load: (id: string) => Promise<FigureDetailRecord> = async id => {\n const [saved, released] = await Promise.all([fetchFigure(id), fetchReleasedFigure(id)])\n if (!saved?.source) throw Error(i18next.t('figure.text.detail-no-source'))\n return { saved, released }\n }\n @state() private figureId = ''\n @state() private record?: FigureDetailRecord\n @state() private asset?: FigureAsset\n @state() private sourceKind: 'saved' | 'released' = 'saved'\n @state() private loading = false\n @state() private error = ''\n @state() private cost?: ReturnType<typeof figureCostOf>\n private ticket = 0\n private unsubscribe?: () => void\n private boundary?: Element\n private boundaryObserver?: ResizeObserver\n\n async show(id: string) {\n const ticket = ++this.ticket\n this.figureId = id\n this.record = undefined\n this.asset = undefined\n this.cost = undefined\n this.loading = true\n this.error = ''\n try {\n const record = await this.load(id)\n if (ticket !== this.ticket) return\n this.record = record\n this.choose(record.released ? 'released' : 'saved')\n } catch {\n if (ticket === this.ticket) this.error = i18next.t('figure.text.detail-load-failed')\n } finally {\n if (ticket === this.ticket) this.loading = false\n }\n }\n private choose(kind: 'saved' | 'released') {\n this.sourceKind = kind\n this.asset = undefined\n this.cost = undefined\n this.error = ''\n try {\n const source = kind === 'released' ? this.record?.released?.source : this.record?.saved.source\n if (!source) throw Error('missing source')\n const asset = parseFigureAsset(source)\n this.cost = figureCostOf(asset)\n this.asset = asset\n } catch {\n this.error = i18next.t('figure.text.detail-invalid-source')\n }\n }\n connectedCallback() {\n super.connectedCallback()\n window.addEventListener('resize', this.place)\n window.addEventListener('popstate', this.place)\n this.unsubscribe = store.subscribe(() => {\n const state = store.getState() as { layout?: { viewparts?: Record<string, { show?: boolean }> } }\n const parts = state.layout?.viewparts\n if (parts?.[ASSISTANT_DOCK_VIEWPART]?.show && parts?.[VIEWPART]?.show) hideDetail()\n else requestAnimationFrame(this.place)\n })\n this.place()\n window.addEventListener('keydown', this.escape)\n }\n disconnectedCallback() {\n super.disconnectedCallback()\n window.removeEventListener('resize', this.place)\n window.removeEventListener('popstate', this.place)\n window.removeEventListener('keydown', this.escape)\n this.unsubscribe?.()\n this.boundaryObserver?.disconnect()\n this.boundary = undefined\n }\n private place = () => {\n const bar = visibleBar(document)\n if (bar !== this.boundary) {\n this.boundaryObserver?.disconnect()\n this.boundary = bar\n if (bar) {\n this.boundaryObserver = new ResizeObserver(this.place)\n this.boundaryObserver.observe(bar)\n }\n }\n this.style.setProperty('--figure-detail-top', `${bar?.getBoundingClientRect().bottom ?? 44}px`)\n }\n private escape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') hideDetail()\n }\n render() {\n const saved = this.record?.saved\n const cost = this.cost\n const capacity = cost ? capacityFrom(cost.groups, cost.triangles) : undefined\n return html`<header><h2>${saved?.name || i18next.t('figure.title.detail')}</h2>\n <button aria-label=${i18next.t('figure.button.close')} @click=${() => hideDetail()}>×</button></header>\n <div body>\n ${this.loading ? html`<p status role=\"status\">${i18next.t('figure.text.detail-loading')}</p>` : nothing}\n ${this.error ? html`<div status role=\"alert\"><p>${this.error}</p><button @click=${() => this.show(this.figureId)}>${i18next.t('figure.button.detail-retry')}</button></div>` : nothing}\n ${saved ? html`<section meta><code>${saved.type}</code><p>${saved.description || ''}</p>\n <label>${i18next.t('figure.label.detail-source')}\n <select aria-label=${i18next.t('figure.label.detail-source')} .value=${this.sourceKind}\n @change=${(e: Event) => this.choose((e.target as HTMLSelectElement).value as 'saved' | 'released')}>\n ${this.record?.released ? html`<option value=\"released\">${i18next.t('figure.label.detail-released', { version: this.record.released.version })}</option>` : nothing}\n <option value=\"saved\">${i18next.t('figure.label.detail-saved')} · ${i18next.t(saved.state === 'released' ? 'figure.label.figure-released' : 'figure.label.draft')}</option>\n </select></label>\n ${!this.record?.released ? html`<p>${i18next.t('figure.text.detail-unreleased')}</p>` : nothing}\n </section>` : nothing}\n ${this.asset ? html`<figure-asset-viewer .asset=${this.asset}></figure-asset-viewer>\n <section summary><h3>${i18next.t('figure.label.detail-summary')}</h3><dl>\n <dt>${i18next.t('figure.label.budget-parts')}</dt><dd>${this.asset.appearance.length}</dd>\n <dt>${i18next.t('figure.label.budget-groups')}</dt><dd>${cost?.groups ?? '—'}</dd>\n <dt>${i18next.t('figure.label.triangles')}</dt><dd>${cost?.triangles ?? '—'}</dd>\n </dl>${capacity ? html`<p>${i18next.t('figure.text.parts-budget', { triangles: cost!.triangles, groups: cost!.groups, instances: capacity.instances })}</p>` : nothing}\n </section>` : nothing}\n </div>\n <footer><button ?disabled=${!saved} @click=${() => {\n navigate(modellerPathOf(saved))\n requestAnimationFrame(() => requestAnimationFrame(this.place))\n }}>${i18next.t('figure.button.detail-edit')}</button></footer>`\n }\n}\n\nfunction visibleBar(root: Document | ShadowRoot): Element | undefined {\n for (const element of root.querySelectorAll('*')) {\n if (element.tagName === 'OX-CANVAS-BAR' && element.getBoundingClientRect().height > 0) return element\n if (element.shadowRoot) {\n const found = visibleBar(element.shadowRoot)\n if (found) return found\n }\n }\n}\n\nlet dock: FigureDetailDock | undefined\nfunction hideDetail() {\n store.dispatch({ type: UPDATE_VIEWPART, name: VIEWPART, override: { show: false } })\n}\nexport function openFigureDetail(figure: Figure) {\n if (!dock) {\n dock = document.createElement('figure-detail-dock') as FigureDetailDock\n }\n store.dispatch({ type: UPDATE_VIEWPART, name: ASSISTANT_DOCK_VIEWPART, override: { show: false } })\n appendViewpart({ name: VIEWPART, position: VIEWPART_POSITION.ASIDEBAR,\n viewpart: { show: true, persistent: true, level: VIEWPART_LEVEL.TOPMOST, template: html`${dock}` } })\n void dock.show(figure.id)\n}\n"]}