@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.
- package/client/bootstrap.ts +10 -2
- package/client/graphql/index.ts +20 -0
- package/client/modeller/canvas.ts +6 -2
- package/client/modeller/carrier-declaration.ts +63 -0
- package/client/modeller/editor.ts +62 -27
- package/client/modeller/figure-carrier.ts +117 -0
- package/client/modeller/figure-cavity-drawing.ts +3 -9
- package/client/modeller/figure-first-save.ts +4 -7
- package/client/modeller/figure-history-panel.ts +2 -3
- package/client/modeller/figure-links.ts +46 -60
- package/client/modeller/figure-material-panel.ts +4 -14
- package/client/modeller/figure-mesh-choice.ts +68 -0
- package/client/modeller/figure-parts.ts +23 -1
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-properties.ts +5 -3
- package/client/modeller/figure-section-editor.ts +12 -33
- package/client/modeller/figure-section-panel.ts +6 -7
- package/client/modeller/figure-slot-layout-panel.ts +208 -0
- package/client/modeller/figure-verdict.ts +21 -31
- package/client/modeller/first-save.ts +23 -0
- package/client/modeller/links.ts +52 -1
- package/client/modeller/material-edit.ts +38 -0
- package/client/modeller/maturity.ts +28 -0
- package/client/modeller/mesh-import.ts +77 -0
- package/client/modeller/modeller-route.ts +9 -0
- package/client/modeller/mounting-part.ts +15 -0
- package/client/modeller/newest-answer.ts +21 -0
- package/client/modeller/preview-model.ts +5 -0
- package/client/modeller/refusals.ts +168 -82
- package/client/modeller/section-controls.ts +25 -1
- package/client/modeller/section-drawing.ts +43 -1
- package/client/modeller/slot-layout-host.ts +140 -0
- package/client/pages/figure-modeller-page.ts +6 -5
- package/client/viewparts/detail-route.ts +7 -0
- package/client/viewparts/figure-detail-dock.ts +1 -3
- package/dist-client/bootstrap.d.ts +1 -1
- package/dist-client/bootstrap.js +9 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/graphql/index.d.ts +5 -0
- package/dist-client/graphql/index.js +19 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +2 -0
- package/dist-client/modeller/canvas.js +10 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/carrier-declaration.d.ts +31 -0
- package/dist-client/modeller/carrier-declaration.js +53 -0
- package/dist-client/modeller/carrier-declaration.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +13 -0
- package/dist-client/modeller/editor.js +62 -27
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-carrier.d.ts +26 -0
- package/dist-client/modeller/figure-carrier.js +123 -0
- package/dist-client/modeller/figure-carrier.js.map +1 -0
- package/dist-client/modeller/figure-cavity-drawing.js +3 -11
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +4 -0
- package/dist-client/modeller/figure-first-save.js +5 -8
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -4
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -1
- package/dist-client/modeller/figure-links.js +47 -57
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +0 -1
- package/dist-client/modeller/figure-material-panel.js +4 -14
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
- package/dist-client/modeller/figure-mesh-choice.js +75 -0
- package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +22 -0
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +2 -1
- package/dist-client/modeller/figure-properties.js +5 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +2 -2
- package/dist-client/modeller/figure-section-editor.js +12 -33
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +6 -7
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
- package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +1 -1
- package/dist-client/modeller/figure-verdict.js +19 -37
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +10 -0
- package/dist-client/modeller/first-save.js +16 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/links.d.ts +26 -1
- package/dist-client/modeller/links.js +43 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +11 -0
- package/dist-client/modeller/material-edit.js +28 -0
- package/dist-client/modeller/material-edit.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +15 -0
- package/dist-client/modeller/maturity.js +23 -0
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +47 -0
- package/dist-client/modeller/mesh-import.js +47 -0
- package/dist-client/modeller/mesh-import.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +5 -0
- package/dist-client/modeller/modeller-route.js +9 -0
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/mounting-part.d.ts +14 -0
- package/dist-client/modeller/mounting-part.js +10 -0
- package/dist-client/modeller/mounting-part.js.map +1 -0
- package/dist-client/modeller/newest-answer.d.ts +12 -0
- package/dist-client/modeller/newest-answer.js +21 -0
- package/dist-client/modeller/newest-answer.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +2 -0
- package/dist-client/modeller/preview-model.js +4 -0
- package/dist-client/modeller/preview-model.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +26 -5
- package/dist-client/modeller/refusals.js +184 -86
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.d.ts +17 -1
- package/dist-client/modeller/section-controls.js +18 -0
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +24 -1
- package/dist-client/modeller/section-drawing.js +42 -0
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +81 -0
- package/dist-client/modeller/slot-layout-host.js +105 -0
- package/dist-client/modeller/slot-layout-host.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.js +5 -4
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/detail-route.d.ts +5 -0
- package/dist-client/viewparts/detail-route.js +8 -0
- package/dist-client/viewparts/detail-route.js.map +1 -0
- package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
- package/dist-client/viewparts/figure-detail-dock.js +1 -3
- package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
- package/package.json +9 -8
- package/test/align.test.ts +0 -9
- package/test/browser/assistant-stand-in.js +32 -0
- package/test/browser/build.check.mjs +77 -0
- package/test/browser/build.mjs +31 -6
- package/test/browser/editing-page.js +224 -0
- package/test/browser/editing.check.mjs +550 -0
- package/test/browser/links-page.js +113 -0
- package/test/browser/links.check.mjs +568 -0
- package/test/browser/modeller.check.mjs +21 -0
- package/test/browser/parts-page.js +120 -0
- package/test/browser/parts.check.mjs +328 -0
- package/test/browser/places-mesh-page.js +36 -0
- package/test/browser/places-mesh.check.mjs +239 -0
- package/test/browser/product-page.check.mjs +682 -0
- package/test/browser/product-page.js +276 -0
- package/test/browser/screen-page.js +112 -0
- package/test/browser/screen-product-page.js +62 -0
- package/test/browser/screen.check.mjs +1003 -0
- package/test/browser/section-page.js +256 -0
- package/test/browser/section.check.mjs +719 -0
- package/test/browser/serve.mjs +20 -12
- package/test/carrier-declaration.test.ts +69 -0
- package/test/cavity-drawing.test.ts +2 -18
- package/test/clip-ai-proposal.test.ts +1 -5
- package/test/clip-consent.test.ts +0 -5
- package/test/clips.test.ts +0 -7
- package/test/command-preview.test.ts +0 -68
- package/test/controls.test.ts +0 -9
- package/test/detail-dock.test.ts +6 -25
- package/test/face-mapping.test.ts +1 -20
- package/test/fasten-drawing.test.ts +0 -23
- package/test/fields.test.ts +6 -33
- package/test/figure-assistant-surface.test.ts +32 -38
- package/test/first-minute.test.ts +3 -55
- package/test/first-save-suggest.test.ts +11 -8
- package/test/first-save.test.ts +4 -26
- package/test/gizmo.test.ts +0 -25
- package/test/joint-fan.test.ts +0 -10
- package/test/keys.test.ts +0 -7
- package/test/layout.test.ts +4 -73
- package/test/links.test.ts +48 -67
- package/test/live.test.ts +0 -34
- package/test/material.test.ts +17 -26
- package/test/maturity.test.ts +13 -16
- package/test/mesh-import.test.ts +76 -0
- package/test/modeller-route.test.ts +3 -19
- package/test/package-download.test.ts +6 -7
- package/test/parts-wiring.test.ts +9 -51
- package/test/preview.test.ts +5 -46
- package/test/properties.test.ts +11 -26
- package/test/proposal-record.test.ts +2 -19
- package/test/recipes.test.ts +0 -15
- package/test/refusals.test.ts +257 -151
- package/test/release-entry.test.ts +1 -9
- package/test/release-panel.test.ts +1 -22
- package/test/resize-with-placement.test.ts +0 -32
- package/test/revert-choice.test.ts +1 -22
- package/test/revert-keeping.test.ts +1 -12
- package/test/save-landed.test.ts +16 -14
- package/test/scene-canvas.test.ts +2 -13
- package/test/section-editor.test.ts +30 -83
- package/test/section-panel.test.ts +22 -17
- package/test/slot-layout-host.test.ts +117 -0
- package/test/theme.test.ts +5 -9
- package/test/verdict.test.ts +44 -41
- package/test/view.test.ts +5 -12
- package/translations/en.json +61 -1
- package/translations/es.json +61 -1
- package/translations/ja.json +61 -1
- package/translations/ko.json +61 -1
- package/translations/ms.json +61 -1
- package/translations/zh.json +61 -1
- package/test/thumbnail.test.ts +0 -29
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalogue preview belongs to the page it was opened on: it closes once the shell leaves that page, by any route
|
|
3
|
+
* (`opened` is that page, `current` the page the shell is on now). Opened on no known page, it is never closed by this.
|
|
4
|
+
*/
|
|
5
|
+
export declare function leftOpeningPage(opened: string | undefined, current: string | undefined): boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalogue preview belongs to the page it was opened on: it closes once the shell leaves that page, by any route
|
|
3
|
+
* (`opened` is that page, `current` the page the shell is on now). Opened on no known page, it is never closed by this.
|
|
4
|
+
*/
|
|
5
|
+
export function leftOpeningPage(opened, current) {
|
|
6
|
+
return opened !== undefined && current !== opened;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=detail-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detail-route.js","sourceRoot":"","sources":["../../client/viewparts/detail-route.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAA0B,EAAE,OAA2B;IACrF,OAAO,MAAM,KAAK,SAAS,IAAI,OAAO,KAAK,MAAM,CAAA;AACnD,CAAC","sourcesContent":["/**\n * The catalogue preview belongs to the page it was opened on: it closes once the shell leaves that page, by any route\n * (`opened` is that page, `current` the page the shell is on now). Opened on no known page, it is never closed by this.\n */\nexport function leftOpeningPage(opened: string | undefined, current: string | undefined) {\n return opened !== undefined && current !== opened\n}\n"]}
|
|
@@ -32,6 +32,5 @@ export declare class FigureDetailDock extends FigureDetailDock_base {
|
|
|
32
32
|
private escape;
|
|
33
33
|
render(): import("lit-html").TemplateResult<1>;
|
|
34
34
|
}
|
|
35
|
-
export declare function leftOpeningPage(opened: string | undefined, current: string | undefined): boolean;
|
|
36
35
|
export declare function openFigureDetail(figure: Figure): void;
|
|
37
36
|
export {};
|
|
@@ -10,6 +10,7 @@ import { figureCostOf, parseFigureAsset } from '@hatiolab/figure-model';
|
|
|
10
10
|
import { capacityFrom } from '../modeller/scene-budget.js';
|
|
11
11
|
import { fetchFigure, fetchReleasedFigure } from '../graphql/index.js';
|
|
12
12
|
import { modellerPathOf } from '../modeller/modeller-route.js';
|
|
13
|
+
import { leftOpeningPage } from './detail-route.js';
|
|
13
14
|
import './figure-asset-viewer.js';
|
|
14
15
|
const VIEWPART = 'figure-detail';
|
|
15
16
|
/** Catalogue details stay while the catalogue is shown; choosing a size or a pose has no write path. */
|
|
@@ -213,9 +214,6 @@ function visibleBar(root) {
|
|
|
213
214
|
let dock;
|
|
214
215
|
/** The page the preview was opened on; the preview belongs to that page and closes when the shell leaves it. */
|
|
215
216
|
let openedOn;
|
|
216
|
-
export function leftOpeningPage(opened, current) {
|
|
217
|
-
return opened !== undefined && current !== opened;
|
|
218
|
-
}
|
|
219
217
|
function hideDetail() {
|
|
220
218
|
store.dispatch({ type: UPDATE_VIEWPART, name: VIEWPART, override: { show: false } });
|
|
221
219
|
}
|
|
@@ -1 +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 { ScrollbarStyles } from '@operato/styles'\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 stay while the catalogue is shown; 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 = [ScrollbarStyles, 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 }> }; route?: { page?: string } }\n const parts = state.layout?.viewparts\n if (parts?.[VIEWPART]?.show && (parts?.[ASSISTANT_DOCK_VIEWPART]?.show || leftOpeningPage(openedOn, state.route?.page))) 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 // The modeller shows the figure itself; a preview of it beside the editor only covers the work.\n hideDetail()\n navigate(modellerPathOf(saved))\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\n/** The page the preview was opened on; the preview belongs to that page and closes when the shell leaves it. */\nlet openedOn: string | undefined\nexport function leftOpeningPage(opened: string | undefined, current: string | undefined) {\n return opened !== undefined && current !== opened\n}\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 openedOn = (store.getState() as { route?: { page?: string } }).route?.page\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"]}
|
|
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 { ScrollbarStyles } from '@operato/styles'\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 { leftOpeningPage } from './detail-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 stay while the catalogue is shown; 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 = [ScrollbarStyles, 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 }> }; route?: { page?: string } }\n const parts = state.layout?.viewparts\n if (parts?.[VIEWPART]?.show && (parts?.[ASSISTANT_DOCK_VIEWPART]?.show || leftOpeningPage(openedOn, state.route?.page))) 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 // The modeller shows the figure itself; a preview of it beside the editor only covers the work.\n hideDetail()\n navigate(modellerPathOf(saved))\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\n/** The page the preview was opened on; the preview belongs to that page and closes when the shell leaves it. */\nlet openedOn: string | 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 openedOn = (store.getState() as { route?: { page?: string } }).route?.page\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.150",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "npm run build:server && npm run build:client",
|
|
20
|
-
"test": "FIGURE_FREEZE_COMPILED=1 node --import ./test/resolve-ts-siblings.mjs --test
|
|
21
|
-
"test:browser": "node --test
|
|
20
|
+
"test": "FIGURE_FREEZE_COMPILED=1 node --import ./test/resolve-ts-siblings.mjs --test test/*.test.ts",
|
|
21
|
+
"test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs",
|
|
22
22
|
"copy:files": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
|
|
23
23
|
"build:client": "npm run copy:files && npx tsc --p ./client/tsconfig.json",
|
|
24
24
|
"build:server": "npx tsc --p ./server/tsconfig.json",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.5.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.5.2",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.82",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^10.0.0",
|
|
@@ -38,11 +38,12 @@
|
|
|
38
38
|
"@operato/p13n": "^10.0.0",
|
|
39
39
|
"@operato/popup": "^10.23.0",
|
|
40
40
|
"@operato/shell": "^10.0.0",
|
|
41
|
+
"@operato/slot-layout-editor": "^10.29.25",
|
|
41
42
|
"@operato/styles": "^10.0.0",
|
|
42
43
|
"@operato/utils": "^10.0.0",
|
|
43
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
44
|
-
"@things-factory/figure-service": "^10.1.
|
|
44
|
+
"@things-factory/ai-assistant": "^10.1.150",
|
|
45
|
+
"@things-factory/figure-service": "^10.1.150",
|
|
45
46
|
"three": "^0.185.1"
|
|
46
47
|
},
|
|
47
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "39c135863f5f7455dcd80b8907550a1c91a86118"
|
|
48
49
|
}
|
package/test/align.test.ts
CHANGED
|
@@ -50,9 +50,6 @@ test('기준면 맞추기 — 배치를 따르고, 한 번 옮길 뿐 붙이지
|
|
|
50
50
|
const hung = applyFigureAuthoring(probe({ x: 0, y: -300, z: 0 }), { kind: 'set-placement', placement: 'ceiling' })
|
|
51
51
|
const top = box(applied(hung, figurePlaneAlignChanges(hung, 'body').changes), 'body')!
|
|
52
52
|
assert.equal(Math.round(top.centre.y + top.extent.y / 2), 0, '천장: 윗면이 y 0')
|
|
53
|
-
const editor = read('../client/modeller/editor.ts')
|
|
54
|
-
assert.ok(editor.includes('const { changes, contact, refusal } = figurePlaneAlignChanges(asset, this.selected)'))
|
|
55
|
-
assert.ok(!read('../client/modeller/align.ts').includes("case 'floor'"), '화면의 제 산수는 없다')
|
|
56
53
|
})
|
|
57
54
|
|
|
58
55
|
test('눈금은 1 · 2 · 5 간격으로 자리와 크기를 맞추고, 크기를 0 으로 만들지 않는다', () => {
|
|
@@ -74,7 +71,6 @@ test('대칭은 복제본을 가운데 면 반대편 같은 거리에 둔다 ·
|
|
|
74
71
|
|
|
75
72
|
test('붙인 부품처럼 옮길 수 없는 것은 맞추기도 없다', () => {
|
|
76
73
|
assert.deepEqual(alignChanges(revisionOf(probe()), 'body', { translate: false, rotate: false, scale: false }, 'centre-x'), [])
|
|
77
|
-
assert.match(read('../client/modeller/editor.ts'), /if \(!this\.selected \|\| !reach\.translate \|\| !shown\) return null/)
|
|
78
74
|
})
|
|
79
75
|
|
|
80
76
|
test('the grid step reads well -- 27 or 33 would make coordinates unreadable -- and is never 0', () => {
|
|
@@ -88,9 +84,6 @@ test('a tilted part is told it touches by a point or an edge, a ball by its lowe
|
|
|
88
84
|
const tilted = applyFigureAuthoring(probe({ x: 0, y: 300, z: 0 }, { x: 0, y: 0, z: 45 }), { kind: 'set-placement', placement: 'floor' })
|
|
89
85
|
assert.equal(figurePlaneAlignChanges(tilted, 'body').contact, 'tilted')
|
|
90
86
|
assert.equal(figurePlaneAlignChanges(probe({ x: 0, y: 300, z: 0 }), 'body').contact, 'face')
|
|
91
|
-
const editor = read('../client/modeller/editor.ts')
|
|
92
|
-
assert.ok(editor.includes("const told = contact === 'tilted' || contact === 'extreme' ? `-${contact}` : ''"), 'the tooltip follows the contact')
|
|
93
|
-
assert.ok(editor.includes("this.notice = i18next.t(`figure.text.aligned-${contact}-${asset.placement ?? 'floor'}`)"), 'and so does the word after')
|
|
94
87
|
for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
|
|
95
88
|
const t = JSON.parse(read(`../translations/${lang}.json`))
|
|
96
89
|
for (const placement of ['floor', 'ceiling', 'wall'])
|
|
@@ -104,8 +97,6 @@ test('a tilted part is told it touches by a point or an edge, a ball by its lowe
|
|
|
104
97
|
|
|
105
98
|
/* Designer 2026-09-27: the gizmo's reason for a tilted part named the floor's lowest point in every placement. */
|
|
106
99
|
test('the size gizmo\'s reason names what the placement keeps, in every language', () => {
|
|
107
|
-
const editor = read('../client/modeller/editor.ts')
|
|
108
|
-
assert.equal(editor.match(/'figure\.text\.gizmo-size-turned-fastening'[^\n]*extreme: this\.keptExtremeWord\(\)/g)?.length, 3, 'every place it is said')
|
|
109
100
|
for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
|
|
110
101
|
const t = JSON.parse(read(`../translations/${lang}.json`))
|
|
111
102
|
assert.ok(t['figure.text.gizmo-size-turned-fastening'].includes('{extreme}'), lang)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The assistant dock's door as the modeller page uses it, recorded: what the page provided, told, took back and asked.
|
|
3
|
+
* The real dock needs the whole app around it; what is checked here is the page's side of the door.
|
|
4
|
+
*/
|
|
5
|
+
const said = []
|
|
6
|
+
window.assistant = {
|
|
7
|
+
said: () => said.map(s => ({ ...s })),
|
|
8
|
+
forget: () => (said.length = 0),
|
|
9
|
+
/** The surface last provided, as the dock would read it when a message is sent. */
|
|
10
|
+
surface: null,
|
|
11
|
+
/** Proposal handlers by tool, as the dock would run one for a card. */
|
|
12
|
+
handlers: {}
|
|
13
|
+
}
|
|
14
|
+
export function provideAssistantSurface(host, surface) {
|
|
15
|
+
window.assistant.surface = surface
|
|
16
|
+
said.push({ call: 'provide', key: surface.key })
|
|
17
|
+
}
|
|
18
|
+
export function notifyAssistantSurface() {
|
|
19
|
+
said.push({ call: 'notify' })
|
|
20
|
+
}
|
|
21
|
+
export function clearAssistantSurface() {
|
|
22
|
+
window.assistant.surface = null
|
|
23
|
+
said.push({ call: 'clear' })
|
|
24
|
+
}
|
|
25
|
+
export function askAssistant(request) {
|
|
26
|
+
said.push({ call: 'ask', draft: request?.draft })
|
|
27
|
+
}
|
|
28
|
+
export function registerProposalHandler(tool, handler) {
|
|
29
|
+
window.assistant.handlers[tool] = handler
|
|
30
|
+
said.push({ call: 'register', tool })
|
|
31
|
+
}
|
|
32
|
+
export const ASSISTANT_DOCK_VIEWPART = 'assistant-dock'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Two runs building the same page at the same time, as two sessions on one machine do: each builds in a folder of its
|
|
3
|
+
* own, neither takes away the other's page, and each folder goes when its process ends -- by itself or stopped.
|
|
4
|
+
*
|
|
5
|
+
* Run: node --test test/browser/build.check.mjs
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { dirname } from 'node:path'
|
|
12
|
+
|
|
13
|
+
const BUILD = new URL('./build.mjs', import.meta.url).href
|
|
14
|
+
|
|
15
|
+
/* A run: builds the page, says where and what it built, and when asked says whether its page is still as it built it. */
|
|
16
|
+
const RUN = `
|
|
17
|
+
import { buildPage } from ${JSON.stringify(BUILD)}
|
|
18
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
19
|
+
import { createHash } from 'node:crypto'
|
|
20
|
+
const hash = file => (existsSync(file) ? createHash('sha1').update(readFileSync(file)).digest('hex') : null)
|
|
21
|
+
const file = await buildPage('modeller-page.js')
|
|
22
|
+
const built = hash(file)
|
|
23
|
+
console.log(JSON.stringify({ file }))
|
|
24
|
+
process.stdin.on('data', () => console.log(JSON.stringify({ intact: hash(file) === built })))
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
/** Starts a run; `next()` gives the next line it says, as JSON. */
|
|
28
|
+
function start() {
|
|
29
|
+
const child = spawn(process.execPath, ['--input-type=module', '-e', RUN], { stdio: ['pipe', 'pipe', 'inherit'] })
|
|
30
|
+
const lines = []
|
|
31
|
+
const waiting = []
|
|
32
|
+
let rest = ''
|
|
33
|
+
child.stdout.on('data', chunk => {
|
|
34
|
+
rest += chunk
|
|
35
|
+
for (let at = rest.indexOf('\n'); at >= 0; at = rest.indexOf('\n')) {
|
|
36
|
+
const line = rest.slice(0, at)
|
|
37
|
+
rest = rest.slice(at + 1)
|
|
38
|
+
if (!line.startsWith('{')) continue
|
|
39
|
+
const said = JSON.parse(line)
|
|
40
|
+
waiting.length ? waiting.shift()(said) : lines.push(said)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
const ended = new Promise(done => child.on('exit', done))
|
|
44
|
+
/* A run that ends before it says what was asked has failed -- its build did not finish: said at once, not waited out. */
|
|
45
|
+
const quit = ended.then(code => {
|
|
46
|
+
throw Error(`the run ended (${code}) before it said what was asked`)
|
|
47
|
+
})
|
|
48
|
+
quit.catch(() => {})
|
|
49
|
+
return {
|
|
50
|
+
child,
|
|
51
|
+
ended,
|
|
52
|
+
next: () => (lines.length ? Promise.resolve(lines.shift()) : Promise.race([new Promise(done => waiting.push(done)), quit]))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test('two runs at once build in folders of their own, keep each other\'s page, and take their folder with them', { timeout: 300000 }, async t => {
|
|
57
|
+
const a = start()
|
|
58
|
+
const b = start()
|
|
59
|
+
/* Whatever fails below, no run is left waiting: a run left behind keeps this file from ending. */
|
|
60
|
+
t.after(() => {
|
|
61
|
+
for (const run of [a, b]) if (run.child.exitCode === null && run.child.signalCode === null) run.child.kill('SIGKILL')
|
|
62
|
+
})
|
|
63
|
+
const [builtA, builtB] = await Promise.all([a.next(), b.next()])
|
|
64
|
+
assert.notEqual(dirname(builtA.file), dirname(builtB.file), 'each run builds in a folder of its own')
|
|
65
|
+
|
|
66
|
+
/* Both have built: each page is still the one its run built, whichever finished last. */
|
|
67
|
+
a.child.stdin.write('\n')
|
|
68
|
+
b.child.stdin.write('\n')
|
|
69
|
+
assert.deepEqual(await Promise.all([a.next(), b.next()]), [{ intact: true }, { intact: true }])
|
|
70
|
+
|
|
71
|
+
/* One ends by itself, the other is stopped: neither leaves its folder behind. */
|
|
72
|
+
a.child.stdin.end()
|
|
73
|
+
b.child.kill('SIGTERM')
|
|
74
|
+
await Promise.all([a.ended, b.ended])
|
|
75
|
+
assert.equal(existsSync(dirname(builtA.file)), false, 'the folder of the run that ended')
|
|
76
|
+
assert.equal(existsSync(dirname(builtB.file)), false, 'the folder of the run that was stopped')
|
|
77
|
+
})
|
package/test/browser/build.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (its browser bundle, `things-scene.mjs`).
|
|
8
8
|
*/
|
|
9
9
|
import { execFileSync } from 'node:child_process'
|
|
10
|
-
import {
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
|
11
11
|
import { tmpdir } from 'node:os'
|
|
12
12
|
import { dirname, join, resolve } from 'node:path'
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
@@ -20,11 +20,30 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
20
20
|
const PACKAGE = resolve(HERE, '../..')
|
|
21
21
|
const ROOT = resolve(PACKAGE, '../..')
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/*
|
|
24
|
+
* Each build is made in a folder of its own and taken away when the process ends. A folder named by the page alone was
|
|
25
|
+
* removed and written again by every build of that page, so runs at the same time on one machine (sessions side by
|
|
26
|
+
* side, a server beside a check) could take away each other's page while it was being built or served.
|
|
27
|
+
*/
|
|
28
|
+
const made = []
|
|
29
|
+
const takeAway = () => {
|
|
30
|
+
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
|
|
31
|
+
}
|
|
32
|
+
process.on('exit', takeAway)
|
|
33
|
+
for (const signal of ['SIGINT', 'SIGTERM'])
|
|
34
|
+
process.once(signal, () => {
|
|
35
|
+
takeAway()
|
|
36
|
+
process.kill(process.pid, signal)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* `standIns` maps a module the page would import to a file beside this one: for a module that cannot run here (the
|
|
41
|
+
* assistant dock needs the whole app's tool bar), a stand-in that records what the product asked of it.
|
|
42
|
+
*/
|
|
43
|
+
export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {}) {
|
|
44
|
+
const out = mkdtempSync(join(tmpdir(), 'figure-ui-browser-' + page.replace(/\W+/g, '-') + '-'))
|
|
45
|
+
made.push(out)
|
|
25
46
|
const emitted = join(out, 'client')
|
|
26
|
-
rmSync(out, { recursive: true, force: true })
|
|
27
|
-
mkdirSync(out, { recursive: true })
|
|
28
47
|
|
|
29
48
|
/* Types are the unit suite's and tsc's business; here only what runs. */
|
|
30
49
|
execFileSync(join(ROOT, 'node_modules/.bin/tsc'), ['-p', join(PACKAGE, 'client/tsconfig.json'), '--outDir', emitted, '--noCheck', '--declaration', 'false', '--incremental', 'false'], {
|
|
@@ -47,6 +66,7 @@ export async function buildPage(page = 'modeller-page.js') {
|
|
|
47
66
|
name: 'figure-ui-client',
|
|
48
67
|
resolveId(id) {
|
|
49
68
|
if (id.startsWith('figure-ui-client/')) return join(emitted, id.slice('figure-ui-client/'.length))
|
|
69
|
+
if (standIns[id]) return join(HERE, standIns[id])
|
|
50
70
|
if (figureModel && id === '@hatiolab/figure-model') return figureModel
|
|
51
71
|
if (thingsScene && id === '@hatiolab/things-scene') return thingsScene
|
|
52
72
|
return null
|
|
@@ -75,4 +95,9 @@ export async function buildPage(page = 'modeller-page.js') {
|
|
|
75
95
|
return file
|
|
76
96
|
}
|
|
77
97
|
|
|
78
|
-
|
|
98
|
+
/* Run on its own it prints where the page is, and leaves it there for whoever asked. */
|
|
99
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
100
|
+
const file = await buildPage()
|
|
101
|
+
made.length = 0
|
|
102
|
+
console.log(file)
|
|
103
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The page `editing.check.mjs` drives: the editor of `modeller-page.js`, with the figures and the readings the editing
|
|
3
|
+
* checks need beyond it -- a part raised off the floor, tilted parts, a box beside a drum, two doors on clips -- and
|
|
4
|
+
* what a person sees of them: a part as the canvas draws it now, the bar's gizmo tools, the app's notices and prompts.
|
|
5
|
+
* Only reading: every change the check makes goes through the mouse and the keyboard.
|
|
6
|
+
*/
|
|
7
|
+
import './modeller-page.js'
|
|
8
|
+
import { i18next } from '@operato/i18n'
|
|
9
|
+
import { Raycaster, Vector2 } from 'three'
|
|
10
|
+
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
11
|
+
|
|
12
|
+
const PALETTE = {
|
|
13
|
+
'palette.dark': '#2b2b2b',
|
|
14
|
+
'palette.neutral': '#9a9a9a',
|
|
15
|
+
'palette.primary': '#477b9e',
|
|
16
|
+
'palette.accent': '#f79c40',
|
|
17
|
+
'palette.light': '#dddddd'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function figure(name) {
|
|
21
|
+
const a = createFigureAsset(name)
|
|
22
|
+
a.palette = { ...a.palette, ...PALETTE }
|
|
23
|
+
return a
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A 400 mm box, the figure placed so. */
|
|
27
|
+
function box(placement, position, rotation) {
|
|
28
|
+
const a = addFigureBox(figure(`Box ${placement}`), { id: 'box', size: { x: 400, y: 400, z: 400 }, position, ...(rotation ? { rotation } : {}) })
|
|
29
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement }))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
window.figures = {
|
|
33
|
+
/* 500 mm above the floor. */
|
|
34
|
+
raised: () => box('floor', { x: 0, y: 700, z: 0 }),
|
|
35
|
+
/* Turned 45 degrees about Z, off the floor: it meets the floor by an edge. */
|
|
36
|
+
tilted: () => box('floor', { x: 0, y: 400, z: 0 }, { x: 0, y: 0, z: 45 }),
|
|
37
|
+
/* Hung from the ceiling and tilted: the size gizmo is withheld; what the ceiling keeps is its highest point. */
|
|
38
|
+
'tilted-ceiling': () => box('ceiling', { x: 0, y: -400, z: 0 }, { x: 0, y: 0, z: 30 }),
|
|
39
|
+
/* A box and a drum: a round body has segments to choose, a box has none. */
|
|
40
|
+
shapes: () => {
|
|
41
|
+
let a = figure('Box and drum')
|
|
42
|
+
a = addFigureBox(a, { id: 'bin', size: { x: 600, y: 300, z: 400 }, position: { x: -400, y: 150, z: 0 } })
|
|
43
|
+
a = addFigurePrimitive(a, { id: 'drum', kind: 'cylinder', dimensions: { radius: 200, length: 500 }, position: { x: 400, y: 250, z: 0 } })
|
|
44
|
+
return serializeFigureAsset(a)
|
|
45
|
+
},
|
|
46
|
+
/* Two doors on a frame, each swinging on a clip of its own. */
|
|
47
|
+
doors: () => {
|
|
48
|
+
let a = figure('Doors')
|
|
49
|
+
a = addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
50
|
+
for (const [id, x] of [['left', -500], ['right', 500]]) {
|
|
51
|
+
a = addFigureBox(a, { id, size: { x: 900, y: 1800, z: 40 }, position: { x, y: 1000, z: 0 } })
|
|
52
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: id, face: 'bottom', to: { part: 'frame', face: 'top' } })
|
|
53
|
+
const j = a.joints.find(k => k.body1.part === id)
|
|
54
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: `${id}-open` } } })
|
|
55
|
+
a = editFigurePart(a, { kind: 'set-driver', state: `${id}-open`, clip: `${id}-swing`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
|
|
56
|
+
}
|
|
57
|
+
return serializeFigureAsset(a)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function deep(selector, root = document) {
|
|
62
|
+
const found = [...root.querySelectorAll(selector)]
|
|
63
|
+
for (const element of root.querySelectorAll('*')) if (element.shadowRoot) found.push(...deep(selector, element.shadowRoot))
|
|
64
|
+
return found
|
|
65
|
+
}
|
|
66
|
+
/** What a selector finds inside the shadow of each `host` element, or anywhere when there is no host. */
|
|
67
|
+
const within = (selector, host) => (host ? deep(host).flatMap(h => deep(selector, h.shadowRoot)) : deep(selector))
|
|
68
|
+
function centreOf(element) {
|
|
69
|
+
element.scrollIntoView({ block: 'center' })
|
|
70
|
+
const box = element.getBoundingClientRect()
|
|
71
|
+
return { x: box.left + box.width / 2, y: box.top + box.height / 2 }
|
|
72
|
+
}
|
|
73
|
+
const editor = () => document.querySelector('figure-editor')
|
|
74
|
+
const canvas = () => deep('figure-canvas')[0]
|
|
75
|
+
const three = () => canvas().scene.root._threeCapability
|
|
76
|
+
|
|
77
|
+
/* What the app's notification surface is handed (`notify`), in order. */
|
|
78
|
+
const told = []
|
|
79
|
+
document.addEventListener('notify', e => told.push(e.detail))
|
|
80
|
+
|
|
81
|
+
window.editing = {
|
|
82
|
+
/** A part as the canvas draws it now: where, how big, and its glow. */
|
|
83
|
+
look(id) {
|
|
84
|
+
const c = canvas().scene.root.components.find(c => c.state.name === id)
|
|
85
|
+
if (!c) return null
|
|
86
|
+
const { left, top, zPos, width, height, depth, rotation, rotationX, rotationY, glow } = c.state
|
|
87
|
+
return { at: [left, top, zPos].map(Math.round), size: [width, depth, height].map(Math.round), turn: [rotationX, rotation, rotationY].map(v => Math.round((v ?? 0) * 1000) / 1000), glow: glow ? glow.intensity : 0 }
|
|
88
|
+
},
|
|
89
|
+
/** The bar's move, turn and resize tools: pressed, offered, and what their tooltip says. */
|
|
90
|
+
tools() {
|
|
91
|
+
return deep('button[tool]').map(b => ({ label: b.getAttribute('aria-label'), title: b.title, disabled: b.disabled, pressed: b.getAttribute('aria-pressed') === 'true', ...centreOf(b) }))
|
|
92
|
+
},
|
|
93
|
+
/** The labels of the number fields on the properties tab. */
|
|
94
|
+
propertyInputs() {
|
|
95
|
+
const panel = editor().renderRoot.querySelector('div[panel]')
|
|
96
|
+
return [...panel.querySelectorAll('input')].map(i => i.getAttribute('aria-label'))
|
|
97
|
+
},
|
|
98
|
+
/** The page point of the first element a selector finds -- anywhere on the page, or inside the `host` elements -- and what it says. */
|
|
99
|
+
at(selector, host) {
|
|
100
|
+
const e = within(selector, host)[0]
|
|
101
|
+
return e ? { ...centreOf(e), disabled: !!e.disabled, text: e.textContent.replace(/\s+/g, ' ').trim(), title: e.getAttribute('title') ?? '' } : null
|
|
102
|
+
},
|
|
103
|
+
/** Every element a selector finds, anywhere or inside the `host` elements: its title and text. */
|
|
104
|
+
all(selector, host) {
|
|
105
|
+
return within(selector, host).map(e => ({ title: e.getAttribute('title') ?? '', text: e.textContent.replace(/\s+/g, ' ').trim() }))
|
|
106
|
+
},
|
|
107
|
+
/** The page point of a button by its words (the page loads no translations: the words are keys). */
|
|
108
|
+
button(words) {
|
|
109
|
+
const b = deep('button').find(b => b.textContent.includes(words))
|
|
110
|
+
return b ? { ...centreOf(b), disabled: b.disabled } : null
|
|
111
|
+
},
|
|
112
|
+
/** The first range input inside the `host` elements: where its thumb is and how many pixels one unit of it spans. */
|
|
113
|
+
range(host) {
|
|
114
|
+
const input = within('input[type=range]', host)[0]
|
|
115
|
+
if (!input) return null
|
|
116
|
+
input.scrollIntoView({ block: 'center' })
|
|
117
|
+
const box = input.getBoundingClientRect()
|
|
118
|
+
const min = Number(input.min),
|
|
119
|
+
max = Number(input.max),
|
|
120
|
+
value = Number(input.value)
|
|
121
|
+
const thumb = 8
|
|
122
|
+
return { x: box.left + thumb + ((value - min) / (max - min)) * (box.width - 2 * thumb), y: box.top + box.height / 2, pxPerUnit: (box.width - 2 * thumb) / (max - min), value }
|
|
123
|
+
},
|
|
124
|
+
/** The page point of a field's label: the axis letter of a row's cell, or a single field's name. */
|
|
125
|
+
fieldLabel(id) {
|
|
126
|
+
const input = deep('input').find(i => i.getAttribute('aria-label') === id)
|
|
127
|
+
const label = input?.closest('div[cell]')?.querySelector('span[axis]') ?? input?.previousElementSibling
|
|
128
|
+
return label ? centreOf(label) : null
|
|
129
|
+
},
|
|
130
|
+
/** What a field shows now. */
|
|
131
|
+
fieldValue(id) {
|
|
132
|
+
return deep('input').find(i => i.getAttribute('aria-label') === id)?.value ?? null
|
|
133
|
+
},
|
|
134
|
+
/** How a clip track's head is laid out: its name, the repeat choice and the remove button, as boxes on the page. */
|
|
135
|
+
trackHead() {
|
|
136
|
+
const head = deep('div[track-head]')[0]
|
|
137
|
+
if (!head) return null
|
|
138
|
+
head.scrollIntoView({ block: 'center' })
|
|
139
|
+
const box = e => {
|
|
140
|
+
const r = e.getBoundingClientRect()
|
|
141
|
+
return { top: r.top, bottom: r.bottom, width: r.width, height: r.height }
|
|
142
|
+
}
|
|
143
|
+
const name = head.querySelector('strong')
|
|
144
|
+
/* The lines the name's words take (its unit stands under it, a line of its own): one box per line of text. */
|
|
145
|
+
const range = document.createRange()
|
|
146
|
+
range.selectNodeContents(name)
|
|
147
|
+
if (name.querySelector('small')) range.setEndBefore(name.querySelector('small'))
|
|
148
|
+
let lines = 0
|
|
149
|
+
for (let at = -Infinity, tops = [...range.getClientRects()].filter(r => r.width > 0).map(r => r.top).sort((a, b) => a - b), i = 0; i < tops.length; i++)
|
|
150
|
+
if (tops[i] >= at + 8) (lines += 1), (at = tops[i])
|
|
151
|
+
return { head: box(head), name: { ...box(name), lines }, choice: box(head.querySelector('select')), remove: box(head.querySelector('button')) }
|
|
152
|
+
},
|
|
153
|
+
/** The app's prompt open now: what it asks, and where its two buttons are. */
|
|
154
|
+
prompt() {
|
|
155
|
+
const prompt = deep('ox-prompt').at(-1)
|
|
156
|
+
const button = id => {
|
|
157
|
+
const b = prompt?.shadowRoot?.querySelector(id)
|
|
158
|
+
if (!b) return null
|
|
159
|
+
const r = b.getBoundingClientRect()
|
|
160
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
|
|
161
|
+
}
|
|
162
|
+
return prompt?.isConnected ? { text: prompt.text, confirm: button('#confirm'), cancel: button('#cancel') } : null
|
|
163
|
+
},
|
|
164
|
+
/** What the app's notification surface was handed since the last call. */
|
|
165
|
+
told() {
|
|
166
|
+
return told.splice(0).map(n => ({ level: n.level, message: n.message }))
|
|
167
|
+
},
|
|
168
|
+
/** A part's material as the session holds it. */
|
|
169
|
+
material(id) {
|
|
170
|
+
return editor().session.read().geometry.find(g => g.id === id)?.material ?? null
|
|
171
|
+
},
|
|
172
|
+
/** The pose the session holds now, and the pose the figure stands in when placed (its saved defaults). */
|
|
173
|
+
pose() {
|
|
174
|
+
const fields = editor().session.read().fields.filter(f => f.role === 'state')
|
|
175
|
+
return { now: Object.fromEntries(fields.map(f => [f.id, f.value])), defaults: JSON.parse(editor().session.source()).stateDefaults ?? {} }
|
|
176
|
+
},
|
|
177
|
+
/** The clips of the figure as the session holds it, with their keys. */
|
|
178
|
+
clips() {
|
|
179
|
+
return (JSON.parse(editor().session.source()).drivers ?? []).map(d => ({ clip: d.clip, state: d.state, keys: d.keys.length }))
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* A page point on the joint fan with a part drawn behind it: the fan is the first thing under the pointer there, and
|
|
183
|
+
* the part the next. Null when the fan covers no part.
|
|
184
|
+
*/
|
|
185
|
+
fanOverPart() {
|
|
186
|
+
const camera = three().cameraManager.activeCamera
|
|
187
|
+
const dom = three().rendererManager.domElement
|
|
188
|
+
const box = dom.getBoundingClientRect()
|
|
189
|
+
const components = canvas().scene.root.components
|
|
190
|
+
const fan = components.find(c => c.state.name === '__joint-fan')?._realObject?.object3d
|
|
191
|
+
if (!fan) return null
|
|
192
|
+
let world = fan
|
|
193
|
+
while (world.parent) world = world.parent
|
|
194
|
+
world.updateMatrixWorld(true)
|
|
195
|
+
const nameOf = object => {
|
|
196
|
+
for (let o = object; o; o = o.parent) if (o.userData?.context?.component) return o.userData.context.component.state?.name ?? null
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
const raycaster = new Raycaster()
|
|
200
|
+
const meshes = []
|
|
201
|
+
fan.traverse(o => o.isMesh && meshes.push(o))
|
|
202
|
+
for (const mesh of meshes) {
|
|
203
|
+
const position = mesh.geometry.attributes.position
|
|
204
|
+
const v = mesh.position.clone()
|
|
205
|
+
const next = mesh.position.clone()
|
|
206
|
+
for (let i = 0; i + 2 < position.count; i += 3) {
|
|
207
|
+
/* The middle of a triangle of the fan, on the page. */
|
|
208
|
+
v.fromBufferAttribute(position, i).add(next.fromBufferAttribute(position, i + 1)).add(next.fromBufferAttribute(position, i + 2)).divideScalar(3).applyMatrix4(mesh.matrixWorld)
|
|
209
|
+
const p = v.clone().project(camera)
|
|
210
|
+
if (p.z > 1 || Math.abs(p.x) > 0.95 || Math.abs(p.y) > 0.95) continue
|
|
211
|
+
raycaster.setFromCamera(new Vector2(p.x, p.y), camera)
|
|
212
|
+
const names = raycaster.intersectObjects(world.children, true).filter(hit => hit.object.visible).map(hit => nameOf(hit.object)).filter(Boolean)
|
|
213
|
+
const behind = names.slice(1).find(n => !n.startsWith('__'))
|
|
214
|
+
if (names[0] === '__joint-fan' && behind) return { x: box.left + ((p.x + 1) / 2) * box.width, y: box.top + ((1 - p.y) / 2) * box.height, behind }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null
|
|
218
|
+
},
|
|
219
|
+
/** Words for these keys, as a translation would give them; `null` takes them away again. */
|
|
220
|
+
words(table) {
|
|
221
|
+
if (table) i18next.addResourceBundle('en', 'translations', table, true, true)
|
|
222
|
+
else i18next.removeResourceBundle('en', 'translations')
|
|
223
|
+
}
|
|
224
|
+
}
|