@things-factory/figure-ui 10.1.208 → 10.1.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/data-interface.ts +5 -5
- package/client/modeller/editor.ts +74 -57
- package/client/modeller/fasten-geometry.ts +2 -1
- package/client/modeller/figure-links.ts +26 -120
- package/client/modeller/figure-size-editor.ts +196 -0
- package/client/modeller/joint-drag.ts +25 -21
- package/client/modeller/joint-fan.ts +4 -3
- package/client/modeller/links.ts +36 -24
- package/client/modeller/motion-ends.ts +10 -6
- package/client/modeller/size-response.ts +95 -0
- package/dist-client/modeller/data-interface.d.ts +1 -1
- package/dist-client/modeller/data-interface.js +5 -5
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +9 -1
- package/dist-client/modeller/editor.js +80 -58
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.js +1 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -4
- package/dist-client/modeller/figure-links.js +26 -107
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.d.ts +27 -0
- package/dist-client/modeller/figure-size-editor.js +221 -0
- package/dist-client/modeller/figure-size-editor.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +1 -1
- package/dist-client/modeller/joint-drag.js +27 -22
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/joint-fan.js +4 -3
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +9 -6
- package/dist-client/modeller/links.js +33 -21
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +4 -1
- package/dist-client/modeller/motion-ends.js +10 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/size-response.d.ts +29 -0
- package/dist-client/modeller/size-response.js +99 -0
- package/dist-client/modeller/size-response.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/evidence/2026-10-24/current-size-panel.png +0 -0
- package/docs/evidence/2026-10-24/integrated-size.png +0 -0
- package/docs/evidence/2026-10-24/multipart-response.png +0 -0
- package/docs/evidence/2026-10-24/repeat-layout.png +0 -0
- package/docs/evidence/2026-10-24/repeat-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/size-in-layout.png +0 -0
- package/docs/evidence/2026-10-24/size-shared-style.png +0 -0
- package/docs/evidence/2026-10-24/width-preview.png +0 -0
- package/docs/multipart-response-review-2026-10-24.md +77 -0
- package/docs/response-authoring-2026-10-24.md +46 -0
- package/docs/size-and-repeat-ux-2026-10-24.md +67 -0
- package/package.json +7 -7
- package/test/attachment-size-contract.test.ts +47 -0
- package/test/browser/acceptance-page.js +1 -1
- package/test/browser/detail-page.js +1 -1
- package/test/browser/detail-tabs-page.js +1 -1
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +28 -14
- package/test/browser/links.check.mjs +41 -22
- package/test/browser/modeller-page.js +1 -1
- package/test/browser/modeller-revision.check.mjs +21 -7
- package/test/browser/modeller.check.mjs +51 -39
- package/test/browser/multipart-response-page.js +8 -0
- package/test/browser/parts.check.mjs +2 -1
- package/test/browser/product-page.js +1 -1
- package/test/browser/props.check.mjs +17 -14
- package/test/browser/response-page.js +12 -0
- package/test/browser/row-preview.check.mjs +3 -3
- package/test/browser/screen.check.mjs +57 -13
- package/test/browser/serve-response.mjs +24 -0
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +2 -2
- package/test/command-preview.test.ts +7 -7
- package/test/joint-drag.test.ts +6 -6
- package/test/joint-frames.test.ts +33 -32
- package/test/joints.ts +1 -6
- package/test/links.test.ts +21 -20
- package/test/multipart-response-fixture.js +17 -0
- package/test/multipart-response.test.ts +37 -0
- package/test/refusals.test.ts +2 -2
- package/test/resize-with-placement.test.ts +23 -1
- package/test/size-response.test.ts +49 -0
- package/test/size-source-pairing.test.ts +66 -0
- package/translations/en.json +44 -10
- package/translations/es.json +44 -10
- package/translations/ja.json +44 -10
- package/translations/ko.json +49 -15
- package/translations/ms.json +44 -10
- package/translations/zh.json +44 -10
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
> 후속 구현: [크기와 반복 배치 UX](size-and-repeat-ux-2026-10-24.md). 이 문서의 이전 화면 제안은 후속 구현 문서와 구분해 읽어야 합니다.
|
|
2
|
+
|
|
3
|
+
# 여러 부품의 관계에서 반응형 UI 점검
|
|
4
|
+
|
|
5
|
+
## 범위
|
|
6
|
+
|
|
7
|
+
사용자의 요청에 따라 단일 부품 검증의 한계를 보완했다. 제품 UI는 추가 수정하지 않았다. 상판(tabletop), 보강대(brace), 다리(legLeft/legRight) 네 부품을 figure-model API로 구성한 재현 fixture를 실제 figure-ui 편집기에 열었다. 이는 관계의 표현과 조작을 점검한 것으로, 사용자가 빈 피규어에서 이 조립체를 쉽게 제작할 수 있다는 검증은 아니다. 저장소는 메모리다. 운영 데이터 변경 및 배포는 없다.
|
|
8
|
+
|
|
9
|
+
figure-model 0.10.0, things-scene 10.1.99는 operato-application 설치본을 사용했다. figure-ui / figure-viewer는 things-factory 현재 소스를 빌드했다.
|
|
10
|
+
|
|
11
|
+
## 모델의 의미와 측정
|
|
12
|
+
|
|
13
|
+
| 부품 | 치수 규칙 | 위치 규칙 | 피규어 너비 1500 → 3000 mm |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| 상판 | 너비는 피규어 너비, 두께 80·깊이 600 고정 | 기준 위치 유지 | 너비 1500 → 3000 |
|
|
16
|
+
| 보강대 | 너비 = 상판 너비 − 200, 높이 60·깊이 40 고정 | 상판 아랫면, Z 최소 가장자리 | 너비 1300 → 2800 |
|
|
17
|
+
| 왼쪽 다리 | 80 × 700 × 80 고정 | 상판 아랫면, X 최소·Z 최대 가장자리 | 중심 X −710 → −1460 |
|
|
18
|
+
| 오른쪽 다리 | 80 × 700 × 80 고정 | 상판 아랫면, X 최대·Z 최대 가장자리 | 중심 X 710 → 1460 |
|
|
19
|
+
|
|
20
|
+
자동 검사 3개 통과. 브라우저에서 보강대/다리 선택, 반응형 및 속성 탭 확인, 전체 적용과 undo, 피규어 너비 미리보기 3000을 조작했다. 숫자별 geometry/pose는 자동 검사에서 검증했다. 브라우저 캔버스에서는 조립체 렌더링을 확인했다.
|
|
21
|
+
|
|
22
|
+
## 발견한 문제
|
|
23
|
+
|
|
24
|
+
### 1. 참조 대상과 연산은 별개의 정보다
|
|
25
|
+
|
|
26
|
+
반응형 탭 상단 문장은 보강대를 ‘tabletop의 너비에 -200 mm를 더한 값’이라고 정확히 설명한다. 반면 같은 화면의 치수 행은 ‘tabletop의 X’만 표시한다. 상세를 펼쳐야 -200 입력을 수정할 수 있다. `followWordsOf`는 part의 times/plus를 생략하고, `renderDimensions`의 following 판정도 같은 축의 instance이면 배율과 오프셋을 생략해 ‘함께 변경’으로 표시한다.
|
|
27
|
+
|
|
28
|
+
정보가 전혀 없는 것이 아니라, 상단 문장·치수 요약·상세 입력이 각기 다른 수준으로 관계를 표현한다. 고정/비례만으로 치환하면 ‘어느 부품의 어떤 치수를, 어느 규칙으로 따르는가’를 잃는다.
|
|
29
|
+
|
|
30
|
+
### 2. 전체 적용이 기존 관계를 덮어쓴다
|
|
31
|
+
|
|
32
|
+
보강대에서 기존 ‘도형 크기에 맞추기’를 클릭했다. 화면은 너비를 피규어 너비의 86.7%로, 높이를 피규어 높이의 75%로, 깊이를 피규어 깊이의 6.7%로 변경했고, -200 입력은 0이 됐다. 실행 취소로 원래 관계를 복원했다.
|
|
33
|
+
|
|
34
|
+
`figure-links.ts`가 axes 없는 follow-size를 보내고, figure-model `authoring-actions.ts`의 followSize가 replace:true로 모든 편집 치수를 연결한다. 의도된 명령 동작이지만 UI에서 참조 대상 교체와 기존 규칙 제거가 분명하지 않다. 피규어 너비 3000에서 원래 보강대는 2800, 버튼 적용 뒤는 약 2600이다. 따라서 단순히 ‘같은 설정을 한 번에 하는 중복 버튼’이라고 설명한 이전 판단은 불충분했다.
|
|
35
|
+
|
|
36
|
+
### 3. 크기 고정과 위치 고정은 다르다
|
|
37
|
+
|
|
38
|
+
다리의 반응형 탭에는 세 치수가 모두 고정으로 나온다. 그러나 다리는 상판 가장자리를 따라 이동한다. 부품 목록에는 상판 아랫면 부착이 표시되고, 속성 탭에는 부착 설정이 있지만, 반응형 탭 자체에는 이 예의 부착에 따른 위치 반응이 나오지 않는다.
|
|
39
|
+
|
|
40
|
+
`render()`의 response는 positionsWorkedOut가 반환하는 위치 계산식만 표시하고, renderFastening은 shape(속성)에 둔다. ‘반응형’이 치수만 뜻하는지 부품의 전체 반응을 뜻하는지 사용자에게 명확하지 않다. 고정을 위치까지 고정한다는 의미로 읽지 않도록 해야 한다.
|
|
41
|
+
|
|
42
|
+
### 4. 기준 치수와 미리보기 결과가 함께 나오지만 구분 표시가 없다
|
|
43
|
+
|
|
44
|
+
미리보기의 피규어 너비는 3000인데 오른쪽 보강대 치수는 기준값 1300으로 남는다. 평가 결과의 보강대 너비는 2800이다. 반응형 패널은 편집 revision을 읽으며 미리보기의 trying 값을 반영하지 않는다. 화면에 기준 치수라는 표기가 없으므로 변경이 적용되지 않은 것처럼 읽힐 수 있다.
|
|
45
|
+
|
|
46
|
+
### 5. 선택 대상과 관계 상대를 찾는 부담
|
|
47
|
+
|
|
48
|
+
오른쪽 상단은 선택된 부품 이름 brace 대신 형태 이름 ‘상자’를 표시한다. 목록에서 선택을 추적해야 한다. 상세 선택지는 다른 모든 부품의 X/Y/Z를 평면적으로 나열한다. 이 예는 3개 상대 × 3축에 피규어 6개 선택지와 고정을 더해 축마다 16개 항목이다. 부품이 늘 때 사용자가 관계 대상을 찾는 부담이 커진다.
|
|
49
|
+
|
|
50
|
+
## UI 판단
|
|
51
|
+
|
|
52
|
+
현재 UI는 기존 관계를 편집할 수는 있지만, 여러 부품의 관계를 이해하고 유지하며 변경하기에는 불충분하다. 단순 고정/비례 토글은 치수가 자기 값을 유지하거나 피규어를 단순 비례로 따르는 경우에만 적합하다. 다른 부품 참조, 일정 차이, 수식에 같은 토글을 강제로 적용해서는 안 된다.
|
|
53
|
+
|
|
54
|
+
다음 설계의 검증 기준:
|
|
55
|
+
|
|
56
|
+
1. 선택한 부품 이름을 표시한다.
|
|
57
|
+
2. 크기 관계는 ‘기준 대상 → 대상 치수 → 규칙’으로 읽히게 한다. 예: 보강대 너비 = 상판 너비 − 200 mm. 기존 관계를 다른 대상으로 바꾸는 조작은 단순 상태 변경과 구별한다.
|
|
58
|
+
3. 위치 관계는 따로 명시한다. 예: 다리 치수 고정 / 상판 아랫면의 가장자리 따라가기. 부착 편집을 중복 구현하지 않고 기존 편집으로 이어지게 한다.
|
|
59
|
+
4. 기준 치수와 미리보기 결과를 구분한다. 가능하면 ‘1300 → 2800 mm’처럼 결과를 함께 보여준다.
|
|
60
|
+
5. 부품 참조를 ‘사용자 설정’ 하나로 숨기지 않는다. 무엇을 따르는지 기본 화면에서 읽을 수 있어야 한다.
|
|
61
|
+
|
|
62
|
+
이번 점검으로 반복 개수, 회전된 로컬 축, 관절 및 홀딩의 UI까지 검증했다고 보지 않는다.
|
|
63
|
+
|
|
64
|
+
## 재현
|
|
65
|
+
|
|
66
|
+
figure-ui 패키지에서:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
PORT=4392 \
|
|
70
|
+
FIGURE_MODEL=/Users/super/Documents/GitHub/operato-application/node_modules/@hatiolab/figure-model \
|
|
71
|
+
THINGS_SCENE=/Users/super/Documents/GitHub/operato-application/node_modules/@hatiolab/things-scene \
|
|
72
|
+
node test/browser/serve-response.mjs multipart-response-page.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
http://127.0.0.1:4392/ 에서 brace 선택 → 반응형 → 상세 펼치기. 전체 적용은 관계를 바꾸므로 확인 후 undo한다. legLeft 선택 → 반응형과 속성을 비교한다. 미리보기 너비를 3000으로 바꾼다. 새로고침은 fixture 초기 상태로 돌아간다.
|
|
76
|
+
|
|
77
|
+
검사: `test/multipart-response.test.ts`, 모델: `test/multipart-response-fixture.js`, 화면: `evidence/2026-10-24/multipart-response.png`.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
> 후속 구현: [크기와 반복 배치 UX](size-and-repeat-ux-2026-10-24.md). 이 문서의 이전 화면 제안은 후속 구현 문서와 구분해 읽어야 합니다.
|
|
2
|
+
|
|
3
|
+
# 반응형 제작 첫 단계 — 축별 크기 반응
|
|
4
|
+
|
|
5
|
+
## 확인한 문제와 코드 근거
|
|
6
|
+
|
|
7
|
+
figure.hatiolab.com의 새 Figure에서 상자를 직접 추가하고 1500 × 80 × 600 mm로 편집했다. 반응형 → 크기 반응 정하기를 먼저 열었을 때, 도형의 size 입력이 아직 없으면 축별 선택지는 고정뿐이었다. 별도의 ‘도형 크기에 맞추기’를 사용하면 모든 축이 함께 연결되어, 너비만 반응시키려는 작업에도 불필요한 해제가 필요했다.
|
|
8
|
+
|
|
9
|
+
- `client/modeller/figure-links.ts`: 기존 renderDimensions는 모두 고정이면 치수 요약을 숨겼고, 전체 축 follow-size만 노출했다. 고급 선택지는 hasInstanceSize로 제한했다.
|
|
10
|
+
- figure-model `src/authoring-actions.ts`의 followSize: 이미 axes를 지원한다. 필요한 size 입력을 선언하고 선택한 축의 현재 치수/도형 크기 비율로 link-dimension을 만든다. 따라서 새 모델 표현이나 렌더링 구현은 필요하지 않았다.
|
|
11
|
+
- 브라우저 자동화의 fill로 입력한 숫자가 실제 모델에 반영되지 않은 현상은 native 키 입력에서는 재현되지 않았다. 제품 입력 결함으로 판단하지 않았다.
|
|
12
|
+
|
|
13
|
+
## 반영한 변경
|
|
14
|
+
|
|
15
|
+
실제 figure-ui 편집기의 반응형 패널에 너비·높이·깊이의 현재 치수와 고정 여부를 처음부터 표시한다. 각 고정 치수에 ‘비례 변경’을 제공하고 기존 follow-size 명령에 해당 축만 전달한다. 설정 직후 치수와 위치를 유지하며 한 번의 undo로 복구된다. 기존 전체 축 설정, 고급 수식/다른 부품 연결, 고정으로 되돌리기는 유지한다. 잠긴 모델과 uniform 크기 방식에서는 새 설정 버튼도 비활성화한다. 여섯 언어의 문구를 추가했다.
|
|
16
|
+
|
|
17
|
+
비례 변경은 해당 도형 축을 따라가는 치수 규칙이다. 부품을 면에 붙이거나 위치를 비례 이동시키는 설정은 아니다. 원형 부품은 기존 provider가 제공하는 편집 치수를 사용하며, 회전된 부품의 방향 대응을 새로 추정하지 않는다.
|
|
18
|
+
|
|
19
|
+
## 검증 결과
|
|
20
|
+
|
|
21
|
+
- 관련 단위 테스트 56개 통과: 기존 설치 figure-model 0.9.6 및 앱의 0.10.0 각각 실행.
|
|
22
|
+
- 추가 회귀 검증: 새 1500 × 80 × 600 mm 프레임에 X만 설정. 설정 직후 geometry/pose 동일. 도형 세 축 입력을 모두 두 배로 주면 geometry는 3000 × 80 × 600 mm. 한 번의 undo로 size 선언까지 복구, redo 동일.
|
|
23
|
+
- 실제 editor 소스를 빌드한 로컬 fixture를 Chrome에서 조작: 너비 비례 변경 → 미리보기 너비 3000 → 편집 → undo → redo 확인. 별도 샘플 모델러가 아닌 제품 editor와 things-scene을 사용한다. fixture의 저장소는 메모리이며 운영 저장·재접속 검증은 하지 않았다.
|
|
24
|
+
- 캡처: `evidence/2026-10-24/width-preview.png`. 왼쪽 3000은 미리보기 입력, 오른쪽 1500은 편집 기준 치수다.
|
|
25
|
+
|
|
26
|
+
## 검증 환경의 불일치
|
|
27
|
+
|
|
28
|
+
이 날짜의 things-factory node_modules는 figure-model 0.9.6 / things-scene 10.1.95, operato-application node_modules는 0.10.0 / 10.1.99였다. 전자의 조합으로 만든 로컬 미리보기에서 `SCHEMA: input: missing min`이 발생했다. 후자의 두 패키지로 빌드하면 발생하지 않았다. 구버전 내부의 정확한 오류 발생 위치까지 규명한 것은 아니며, 이 UI 변경으로 스키마를 완화하지 않았다.
|
|
29
|
+
|
|
30
|
+
기본 tsc는 mountingFace 타입 및 설치된 figure-viewer의 bodyAtSize/sizeSpanOf export 불일치 세 건으로 실패했다. 임시 tsconfig에서 figure-model을 앱의 0.10.0 선언으로, figure-viewer를 현재 소스로 연결하면 전체 client noEmit 검사가 통과했다. 설치 의존성이나 lockfile을 변경하지 않았다. 따라서 기본 설치 환경의 빌드 문제는 여전히 별도 정리가 필요하다.
|
|
31
|
+
|
|
32
|
+
## 화면 재현
|
|
33
|
+
|
|
34
|
+
figure-ui 패키지에서 실행한다. 경로는 현재 로컬 체크아웃 기준이며, 같은 버전이 설치된 환경에서는 override를 생략할 수 있다.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
FIGURE_MODEL=/Users/super/Documents/GitHub/operato-application/node_modules/@hatiolab/figure-model \
|
|
38
|
+
THINGS_SCENE=/Users/super/Documents/GitHub/operato-application/node_modules/@hatiolab/things-scene \
|
|
39
|
+
node test/browser/serve-response.mjs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
http://127.0.0.1:4391/ 에서 frame 선택 → 반응형 → 너비 비례 변경 → 미리보기 → 너비 슬라이더 끝(3000). 저장소가 메모리이므로 새로고침하면 초기 상태로 돌아간다. UI 자체의 저장 버튼도 운영 저장은 하지 않는다.
|
|
43
|
+
|
|
44
|
+
## 범위와 다음 점검
|
|
45
|
+
|
|
46
|
+
운영 사이트에는 배포하지 않았다. 이번 확인으로 컨베이어 제작 전체가 쉬워졌다고 판단하지 않는다. 다음 순서는 원기둥 하나를 실제로 추가해 프레임 면에 붙이고, 간격과 길이 변화에 따른 반복 개수 및 끝 여백을 설정하는 과정이다. 그 과정에서 발견한 혼란도 figure-model의 기존 규칙과 편집기의 노출 방식을 구분해 고친다. 관절·애니메이션·홀딩은 아직 이 변경의 검증 범위 밖이다.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# 크기와 반복 배치 편집 개선 (2026-10-24)
|
|
2
|
+
|
|
3
|
+
상태: 실제 figure-ui 소스에 구현, 로컬 브라우저 검증 완료. 운영 배포·커밋 없음.
|
|
4
|
+
이 문서는 response-authoring-2026-10-24.md의 축별 버튼/고급 설정 제안을 대체한다.
|
|
5
|
+
|
|
6
|
+
## 크기
|
|
7
|
+
|
|
8
|
+
배치 > 크기에서 축별 크기(mm), 가변 여부, 상대 부품, 상대 축을 함께 설정한다.
|
|
9
|
+
가변 축만 상대와 비례 유지/차이 유지 선택을 표시한다. 비율·차이 수치를 따로 편집하지 않는다.
|
|
10
|
+
상대 또는 방식을 바꾸면 현재 크기를 보존해 규칙을 계산한다. 크기 입력을 바꾸면 선택한 규칙을 갱신한다.
|
|
11
|
+
붙이기의 상대와 크기의 상대는 독립적이다. 새 가변 설정에서 붙인 상대를 제안할 뿐, 재부착으로 크기 상대를 바꾸지 않는다.
|
|
12
|
+
원기둥 등은 shape provider의 측정 배율을 적용한다. 기존 복합 수식은 자동 변환하지 않는다.
|
|
13
|
+
자체 입력은 figureSizeEditOf를 통해 편집해 부착/크기 편집의 기존 위치 계약을 보존한다.
|
|
14
|
+
기존 전체 크기 맞춤 버튼, 별도 축별 되돌리기, 고급 크기 설정은 제거했다.
|
|
15
|
+
미리보기의 크기 설정 이동도 배치 탭으로 연결했다.
|
|
16
|
+
|
|
17
|
+
## 반복 배치
|
|
18
|
+
|
|
19
|
+
- 명칭: 줄로 배열 → 반복 배치. 부품의 이전 반응형 탭은 배치로 변경.
|
|
20
|
+
- 적용 전: 접힌 상태에서도 미적용 표시. 펼치면 방향, 간격 유지/개수 유지, 해당 입력과 적용 버튼.
|
|
21
|
+
- 적용 후: 적용됨 표시, 상대 부품, 방향, 현재 개수, 중심 간격. 방향/방식/유효한 입력을 직접 수정 가능.
|
|
22
|
+
- 간격 유지: 중심 간격을 입력하고 개수는 자동 계산.
|
|
23
|
+
- 개수 유지: 개수를 입력하고 중심 간격은 자동 계산하여 읽기 전용 표시.
|
|
24
|
+
- 중심 간격은 표면 사이 빈틈이 아니라 중심과 다음 중심의 거리이다.
|
|
25
|
+
- 해제는 숨은 메뉴 대신 직접 버튼으로 제공. 원본 부품 하나가 남는다는 설명 포함.
|
|
26
|
+
- 기존 고정 개수 UI가 layout.args[4](부품 extent)를 피치로 표시하던 문제 수정: figureRowOf의 실제 pitch 사용.
|
|
27
|
+
- 기존 반복 배치를 수정할 때 set-joint로 repeat만 변경해 연결·관절의 나머지 기록을 보존한다.
|
|
28
|
+
- 적용 후와 부품 전환 시 이전 미리보기 후보가 남지 않도록 정리.
|
|
29
|
+
- 부착으로 위치가 정해진 부품에는 별도의 계산 위치 고정/비례 버튼을 중복 표시하지 않는다.
|
|
30
|
+
|
|
31
|
+
## 검증
|
|
32
|
+
|
|
33
|
+
figure-model 0.10.0, things-scene 10.1.99에 맞춰 타입 검사 및 브라우저 빌드.
|
|
34
|
+
기존 things-factory node_modules의 figure-model 0.9.6은 사용하지 않고 명시적 경로를 지정했다.
|
|
35
|
+
의존성 설치나 lockfile 변경 없음.
|
|
36
|
+
|
|
37
|
+
관련 Node 테스트 66개 통과: size-response, multipart-response, attachment-size-contract, links, resize-with-placement.
|
|
38
|
+
실제 브라우저에서 확인:
|
|
39
|
+
|
|
40
|
+
1. brace의 상대 tabletop이 처음부터 정확히 표시됨.
|
|
41
|
+
2. 1300 → 1250 크기 입력, 비례/차이 전환 시 1250 유지.
|
|
42
|
+
3. legLeft를 X 방향 200 mm 간격으로 배치하면 8개 표시.
|
|
43
|
+
4. 적용 후 개수 유지로 바꾸고 5개 입력 → 중심 간격 355 mm.
|
|
44
|
+
5. 방향 Z로 변경 → 중심 간격 130 mm. 되돌리기 → X, 355 mm 복원.
|
|
45
|
+
6. 해제하면 미적용 상태와 단일 부품으로 복귀.
|
|
46
|
+
|
|
47
|
+
증거: evidence/2026-10-24/integrated-size.png, repeat-layout.png.
|
|
48
|
+
로컬 확인 URL: http://127.0.0.1:4393/ (in-memory fixture, 실제 편집기 컴포넌트).
|
|
49
|
+
|
|
50
|
+
## 범위
|
|
51
|
+
|
|
52
|
+
figure-model 명세와 저장 형식은 변경하지 않았다. 복합 수식의 범용 편집 UI를 추가한 것은 아니다.
|
|
53
|
+
원점 부착·곡면 부착 등 커널이 지원하지 않는 반복 배치는 기존 제한과 이유를 유지한다.
|
|
54
|
+
|
|
55
|
+
## 공통 스타일 정합성 보완
|
|
56
|
+
|
|
57
|
+
크기 편집기는 별도 입력 스타일 대신 figure-viewer의 SidePanelStyles를 직접 사용한다.
|
|
58
|
+
기존 패널과 동일한 26px 높이, 밑줄 입력, 글꼴·색상 토큰, 포커스 표시를 적용했다.
|
|
59
|
+
축마다 추가했던 구분선과 박스형 입력, 반복 배치의 둥근 상태 배지와 박스형 해제 버튼을 제거했다.
|
|
60
|
+
상태 문구와 라디오 선택 행은 공통 스타일에 이름 있는 규칙(section-state, choice-row)으로 정의했다.
|
|
61
|
+
해제는 기존 quiet 버튼 규칙을 사용한다. 적용 전후 라디오 정렬·간격도 일치시켰다.
|
|
62
|
+
타입 검사와 실제 화면 확인 완료. 최신 스타일 확인 URL은 http://127.0.0.1:4394/ 이다.
|
|
63
|
+
증거: evidence/2026-10-24/size-shared-style.png, repeat-shared-style.png.
|
|
64
|
+
|
|
65
|
+
## 크기 설정 위치 변경
|
|
66
|
+
|
|
67
|
+
사용자 요청으로 통합 크기 설정을 배치 탭 맨 위로 이동했다. 속성의 중복 크기 구역과 빈 구역은 제거했다. 형상 고유 치수는 필요한 부품에서만 속성에 유지한다. 크기를 편집할 때 유지되는 위치 설명도 배치의 크기 구역으로 이동했다. 타입 검사 및 실제 브라우저에서 속성의 중복 제거와 배치의 설정 표시를 확인했다. 증거: evidence/2026-10-24/size-in-layout.png.
|
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.210",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -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.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.11.0",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.100",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.29.43",
|
|
34
34
|
"@operato/graphql": "^10.29.43",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"@operato/slot-layout-editor": "^10.29.43",
|
|
43
43
|
"@operato/styles": "^10.0.0",
|
|
44
44
|
"@operato/utils": "^10.29.43",
|
|
45
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
46
|
-
"@things-factory/figure-service": "^10.1.
|
|
47
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
45
|
+
"@things-factory/ai-assistant": "^10.1.210",
|
|
46
|
+
"@things-factory/figure-service": "^10.1.210",
|
|
47
|
+
"@things-factory/figure-viewer": "^10.1.210",
|
|
48
48
|
"@things-factory/setting-base": "^10.1.208",
|
|
49
49
|
"three": "^0.185.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "89fbda12d43d8532ba4e95ba6d6d7e05e2da2502"
|
|
52
52
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { addFigureBox, applyFigureAuthoring, applyFigureProposal, compileFigureAsset, figureFollowOf, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
4
|
+
import { multipartResponseFixture } from './multipart-response-fixture.js'
|
|
5
|
+
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
6
|
+
|
|
7
|
+
const rules = (parent: string, replace = true) => [
|
|
8
|
+
{ kind: 'link-dimension' as const, part: 'brace', axis: 'x' as const, source: { of: 'part' as const, part: parent, axis: 'x' as const }, plus: -300, replace },
|
|
9
|
+
{ kind: 'attach' as const, part: 'brace', to: { part: parent, face: 'bottom' as const }, align: { x: { mm: -50 }, z: 'min' as const }, replace }
|
|
10
|
+
]
|
|
11
|
+
const brace = (a: ReturnType<typeof multipartResponseFixture>) => compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'brace')!
|
|
12
|
+
|
|
13
|
+
test('asymmetric face margins survive serialization using existing dimension and attachment commands', () => {
|
|
14
|
+
const a = parseFigureAsset(serializeFigureAsset(applyFigureProposal(multipartResponseFixture(), rules('tabletop'))))
|
|
15
|
+
for (const width of [1500, 2000, 3000]) {
|
|
16
|
+
const g = brace({ ...a, designInputs: { ...a.designInputs, 'size.x': width } })
|
|
17
|
+
const size = g.shape.dimensions['size.x']!
|
|
18
|
+
assert.equal(size, width - 300)
|
|
19
|
+
assert.equal(g.pose.t[0] - size / 2 + width / 2, 100)
|
|
20
|
+
assert.equal(width / 2 - (g.pose.t[0] + size / 2), 200)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('reattachment alone leaves dimension reference unchanged; combined recipe retargets both with one undo', async () => {
|
|
25
|
+
const original = addFigureBox(multipartResponseFixture(), { id: 'otherTop', size: { x: 2000, y: 80, z: 600 }, position: { x: 0, y: 1000, z: 0 } })
|
|
26
|
+
const onlyAttached = applyFigureAuthoring(original, { kind: 'attach', part: 'brace', to: { part: 'otherTop', face: 'bottom' }, replace: true })
|
|
27
|
+
assert.equal((figureFollowOf(onlyAttached, 'brace', 'x') as { part: string }).part, 'tabletop')
|
|
28
|
+
const source = serializeFigureAsset(original)
|
|
29
|
+
const session = createFigureProductSession({ load: async () => ({ id: 'audit', source }), save: async q => ({ id: 'audit', source: q.source }) })
|
|
30
|
+
await session.open('audit')
|
|
31
|
+
const before = session.source()
|
|
32
|
+
session.applyRecipe(rules('otherTop'))
|
|
33
|
+
const after = session.source()
|
|
34
|
+
const result = parseFigureAsset(after)
|
|
35
|
+
assert.equal((figureFollowOf(result, 'brace', 'x') as { part: string }).part, 'otherTop')
|
|
36
|
+
assert.equal(brace(result).shape.dimensions['size.x'], 1700)
|
|
37
|
+
assert.equal(result.joints!.find(j => j.body1.part === 'brace')!.body0.part, 'otherTop')
|
|
38
|
+
session.undo(); assert.equal(session.source(), before)
|
|
39
|
+
session.redo(); assert.equal(session.source(), after)
|
|
40
|
+
assert.throws(() => session.applyRecipe(rules('missing')))
|
|
41
|
+
assert.equal(session.source(), after, 'failed recipe changes nothing')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('detach preserves dimension references: editor must explicitly choose how its managed size rules end', () => {
|
|
45
|
+
const a = applyFigureAuthoring(multipartResponseFixture(), { kind: 'detach', part: 'brace' })
|
|
46
|
+
assert.equal((figureFollowOf(a, 'brace', 'x') as { part: string }).part, 'tabletop')
|
|
47
|
+
})
|
|
@@ -112,7 +112,7 @@ export const MODELS = {
|
|
|
112
112
|
a = addFigureBox(a, { id, size: { x: 900, y: 1800, z: 40 }, position: { x, y: 1000, z: 0 } })
|
|
113
113
|
a = applyFigureAuthoring(a, { kind: 'attach', part: id, to: { part: 'frame', face: 'top' } })
|
|
114
114
|
const j = a.joints.find(k => k.body1.part === id)
|
|
115
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: `${id}-open` } } })
|
|
115
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', state: { id: `${id}-open` } }] } })
|
|
116
116
|
a = editFigurePart(a, { kind: 'set-driver', state: `${id}-open`, clip: `${id}-swing`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
|
|
117
117
|
}
|
|
118
118
|
return serializeFigureAsset(a)
|
|
@@ -11,7 +11,7 @@ async function start() {
|
|
|
11
11
|
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 50, max: 400 }, y: { min: 50, max: 400 }, z: { min: 50, max: 400 } }, at: { x: 100, y: 100, z: 100 } })
|
|
12
12
|
asset = applyFigureAuthoring(asset, { kind: 'follow-size', part: 'block', axes: ['x', 'y', 'z'] })
|
|
13
13
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: {
|
|
14
|
-
id: 'block',
|
|
14
|
+
id: 'block', body0: { plane: 'mounting-plane' }, body1: { part: 'block' }, motion: [{ type: 'prismatic', axis: 'X', lowerLimit: 0, upperLimit: 200, state: { id: 'shift', start: 0 } }]
|
|
15
15
|
} })
|
|
16
16
|
asset = editFigurePart(asset, { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 200 }, { seconds: 4, value: 0 }] })
|
|
17
17
|
const source = serializeFigureAsset(asset)
|
|
@@ -13,7 +13,7 @@ for (let i = 0; i < PARTS; i++) {
|
|
|
13
13
|
const id = `slide-${i}`
|
|
14
14
|
asset = addFigureBox(asset, { id, size: { x: 40, y: 40, z: 40 }, position: { x: i * 60, y: 20, z: 0 } })
|
|
15
15
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: {
|
|
16
|
-
id,
|
|
16
|
+
id, body0: { plane: 'mounting-plane' }, body1: { part: id }, motion: [{ type: 'prismatic', axis: 'Z', lowerLimit: 0, upperLimit: 100, state: { id: `${id}-z`, start: 0 } }]
|
|
17
17
|
} })
|
|
18
18
|
asset = editFigurePart(asset, { kind: 'set-driver', state: `${id}-z`, clip: `cycle-${i}`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 1, value: 100 }, { seconds: 2, value: 0 }] })
|
|
19
19
|
}
|
|
@@ -52,7 +52,7 @@ window.figures = {
|
|
|
52
52
|
a = addFigureBox(a, { id, size: { x: 900, y: 1800, z: 40 }, position: { x, y: 1000, z: 0 } })
|
|
53
53
|
a = applyFigureAuthoring(a, { kind: 'attach', part: id, to: { part: 'frame', face: 'top' } })
|
|
54
54
|
const j = a.joints.find(k => k.body1.part === id)
|
|
55
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', state: { id: `${id}-open` } } })
|
|
55
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', state: { id: `${id}-open` } }] } })
|
|
56
56
|
a = editFigurePart(a, { kind: 'set-driver', state: `${id}-open`, clip: `${id}-swing`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 80 }] })
|
|
57
57
|
}
|
|
58
58
|
return serializeFigureAsset(a)
|
|
@@ -313,28 +313,42 @@ describe('the material: colours by purpose, one step each, a glow drawn while dr
|
|
|
313
313
|
assert.ok(offered.folded && !offered.folded.open && offered.folded.count > 0, JSON.stringify(offered.folded))
|
|
314
314
|
})
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
/*
|
|
317
|
+
A round body's measures are the 「배치」 tab's size editor (2026-10-24): its across said as a diameter, written as the
|
|
318
|
+
radius it halves to. The editor writes on change; it draws nothing while a number is typed.
|
|
319
|
+
*/
|
|
320
|
+
test('a diameter typed on the arrangement tab is written once when it is left, as the radius it halves to', async () => {
|
|
321
|
+
await click(await read(() => probe.tab('figure.label.tab-arrangement')))
|
|
317
322
|
const start = await revision()
|
|
318
|
-
await type('drum.
|
|
319
|
-
assert.equal((await look('drum')).size[0], 600, 'drawn 600 across while typed')
|
|
323
|
+
await type('drum X · figure.label.size-diameter figure.label.size-measure', '600')
|
|
320
324
|
assert.equal(await revision(), start, 'nothing written yet')
|
|
321
325
|
await page().keyboard.press('Enter')
|
|
322
326
|
await sleep(400)
|
|
323
327
|
assert.equal(await revision(), start + 1)
|
|
324
328
|
assert.equal(await read(() => probe.value('drum.dimension.radius')), 300)
|
|
329
|
+
assert.equal((await look('drum')).size[0], 600, 'drawn 600 across')
|
|
330
|
+
await click(await read(() => probe.tab('figure.label.tab-short-properties')))
|
|
325
331
|
})
|
|
326
332
|
})
|
|
327
333
|
|
|
334
|
+
/*
|
|
335
|
+
A box's size is the 「배치」 tab's size editor now (2026-10-24); the numbers in hand on the properties tab are its place,
|
|
336
|
+
in the position row.
|
|
337
|
+
*/
|
|
328
338
|
describe('values in hand on the properties tab: drawn at once, written on change (user 2026-09-24)', () => {
|
|
339
|
+
/* Where the canvas is told to draw the box across: its pose as given to it. */
|
|
340
|
+
const across = async () => Math.round((await read(() => probe.drawnPose('box'))).t[0])
|
|
341
|
+
let was
|
|
329
342
|
before(async () => {
|
|
330
343
|
await mount('box')
|
|
331
344
|
await pick('box')
|
|
332
345
|
})
|
|
333
346
|
|
|
334
|
-
test('a
|
|
347
|
+
test('a place typed is drawn while it is typed, and written once on Enter', async () => {
|
|
348
|
+
was = await across()
|
|
335
349
|
const start = await revision()
|
|
336
|
-
await type('box.
|
|
337
|
-
assert.equal(
|
|
350
|
+
await type('box.position.x', '300')
|
|
351
|
+
assert.equal(await across(), was + 300, 'drawn 300 over while typed')
|
|
338
352
|
assert.equal(await revision(), start, 'nothing written yet')
|
|
339
353
|
await page().keyboard.press('Enter')
|
|
340
354
|
await sleep(400)
|
|
@@ -343,20 +357,20 @@ describe('values in hand on the properties tab: drawn at once, written on change
|
|
|
343
357
|
|
|
344
358
|
test('once written, the value in hand is let go: undone, the canvas draws the figure as it stands', async () => {
|
|
345
359
|
await press(...UNDO)
|
|
346
|
-
assert.equal(await read(() => probe.value('box.
|
|
347
|
-
assert.equal(
|
|
360
|
+
assert.equal(await read(() => probe.value('box.position.x')), 0, 'undone')
|
|
361
|
+
assert.equal(await across(), was, 'drawn as it stands, not as last typed')
|
|
348
362
|
})
|
|
349
363
|
|
|
350
364
|
test('a label dragged sideways scrubs the value: drawn while held, one step on release', async () => {
|
|
351
365
|
const start = await revision()
|
|
352
|
-
const label = await read(() => editing.fieldLabel('box.
|
|
353
|
-
assert.ok(label, 'the X of the
|
|
354
|
-
const held = await drag(label, 60, async () => ({ drawn:
|
|
355
|
-
assert.equal(held.drawn,
|
|
356
|
-
assert.equal(held.shown, '
|
|
366
|
+
const label = await read(() => editing.fieldLabel('box.position.x'))
|
|
367
|
+
assert.ok(label, 'the X of the position row')
|
|
368
|
+
const held = await drag(label, 60, async () => ({ drawn: await across(), shown: await read(() => editing.fieldValue('box.position.x')), revision: await revision() }))
|
|
369
|
+
assert.equal(held.drawn, was + 60, 'one millimetre a pixel near 0 mm')
|
|
370
|
+
assert.equal(held.shown, '60', 'the field follows')
|
|
357
371
|
assert.equal(held.revision, start, 'nothing written while held')
|
|
358
372
|
assert.equal(await revision(), start + 1, 'the release is one step')
|
|
359
|
-
assert.equal(await read(() => probe.value('box.
|
|
373
|
+
assert.equal(await read(() => probe.value('box.position.x')), 60)
|
|
360
374
|
})
|
|
361
375
|
})
|
|
362
376
|
|
|
@@ -88,7 +88,9 @@ async function pick(part) {
|
|
|
88
88
|
await click(await read(id => probe.row(id), part), `the row of ${part}`)
|
|
89
89
|
await until(id => probe.state().selected === id, [part], `${part} selected`)
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
/* A side tab by its short name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
|
|
92
|
+
const tab = name =>
|
|
93
|
+
read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name).then(point => click(point, `the ${name} tab`))
|
|
92
94
|
/** A fastening the part has is said in one line; 「바꾸기」 opens the fastening window on it. */
|
|
93
95
|
async function changeFastening() {
|
|
94
96
|
await until(() => look.all('button[change-fastening]').length === 1, [], 'the fastening said in one line')
|
|
@@ -100,7 +102,15 @@ const targets = () => read(() => probe.fastenTargets())
|
|
|
100
102
|
const windowOpen = () => read(() => !!probe.fastenTargets())
|
|
101
103
|
/** A row of the window's list pressed, as a hand does, and waited for as the one chosen. */
|
|
102
104
|
async function aimAt(part) {
|
|
103
|
-
|
|
105
|
+
/* The window is placed after it opens: its chip is pressed where it stands still, not where it was a frame ago. */
|
|
106
|
+
let point = await read(p => probe.fastenTarget(p), part)
|
|
107
|
+
for (let i = 0; i < 20; i++) {
|
|
108
|
+
await page().evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))))
|
|
109
|
+
const now = await read(p => probe.fastenTarget(p), part)
|
|
110
|
+
if (now && point && now.x === point.x && now.y === point.y) break
|
|
111
|
+
point = now
|
|
112
|
+
}
|
|
113
|
+
await click(point, `the row of ${part} in the window`)
|
|
104
114
|
await until(p => probe.fastenTargets()?.some(t => t.target === p && t.selected), [part], `${part} chosen in the window`)
|
|
105
115
|
}
|
|
106
116
|
/** An element handle found through every shadow root. */
|
|
@@ -189,13 +199,15 @@ describe('a size rule that would follow itself, and a fastening that would hang
|
|
|
189
199
|
await mount('linked', fasten(a, 'b', 'a'))
|
|
190
200
|
})
|
|
191
201
|
|
|
202
|
+
/* b stands on a's top: made to follow, its width takes a's width (ADR-0159 3), which follows b's. */
|
|
192
203
|
test('b made to follow a\'s width: refused, said as a loop, nothing written', async () => {
|
|
193
204
|
await pick('b')
|
|
194
|
-
await
|
|
205
|
+
await tab('response')
|
|
195
206
|
const start = await state()
|
|
196
|
-
await
|
|
207
|
+
await press('input[aria-label="b X figure.label.size-follows"]', undefined, 'b\'s width made to follow')
|
|
197
208
|
await until(() => probe.state().error === 'figure.text.follow-would-loop', [], 'the loop said')
|
|
198
209
|
assert.equal((await state()).revision, start.revision, 'nothing written')
|
|
210
|
+
await tab('properties')
|
|
199
211
|
})
|
|
200
212
|
|
|
201
213
|
test('a fastened to b, which hangs from it: refused, said as a loop, nothing written', async () => {
|
|
@@ -213,30 +225,36 @@ describe('a part made to follow the figure size, and let go of it', () => {
|
|
|
213
225
|
a = applyFigureAuthoring(a, { kind: 'declare-instance-size', range: SIZE, at: { x: 400, y: 400, z: 400 } })
|
|
214
226
|
await mount('sized', applyFigureAuthoring(a, { kind: 'link-dimension', part: 'box', axis: 'x', source: { of: 'instance', axis: 'x' } }))
|
|
215
227
|
await pick('box')
|
|
216
|
-
/* How a part answers a size is the 「반응형」 tab's (user 2026-10-17). */
|
|
228
|
+
/* How a part answers a size is the 「반응형」 tab's (user 2026-10-17), its size editor one row a side (ADR-0159 3). */
|
|
217
229
|
await tab('response')
|
|
218
230
|
})
|
|
231
|
+
/* What each side's row says it follows: the source said in one line, or nothing. */
|
|
232
|
+
const sources = () => all('div.dimension[data-axis] div.source span')
|
|
233
|
+
const follows = axis => `input[aria-label="box ${axis} figure.label.size-follows"]`
|
|
219
234
|
|
|
220
|
-
|
|
235
|
+
/*
|
|
236
|
+
The one-step follow-size button is gone (2026-10-24): each side is made to follow by its own 'follows', and a box on
|
|
237
|
+
nothing follows the figure's own side.
|
|
238
|
+
*/
|
|
239
|
+
test('on a figure with a size, each side made to follow is one step, follows the figure\'s own side, and says nothing of a size declared', async () => {
|
|
221
240
|
await read(() => look.hear('notify'))
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
assert.
|
|
228
|
-
assert.equal((await all('div[resize-now] span', 'figure.label.resize-following')).length, 3, 'every side said to follow, once')
|
|
241
|
+
for (const axis of ['Y', 'Z']) {
|
|
242
|
+
const start = await state()
|
|
243
|
+
await press(follows(axis), undefined, `${axis} made to follow`)
|
|
244
|
+
await oneStep(start)
|
|
245
|
+
}
|
|
246
|
+
assert.deepEqual((await sources()).map(s => s.text), ['X', 'Y', 'Z'].map(axis => `figure.label.size-figure ${axis}`), 'every side said to follow the figure, once')
|
|
229
247
|
const told = (await heard('notify')).filter(n => n?.level === 'info' && String(n.message).includes('figure.text.size-declared-to-follow'))
|
|
230
248
|
assert.deepEqual(told, [], 'the figure had its size: no line says it was declared')
|
|
231
249
|
})
|
|
232
250
|
|
|
233
251
|
test('a side let go is one step and keeps the size it had', async () => {
|
|
234
252
|
const start = await state()
|
|
235
|
-
await
|
|
253
|
+
await press(follows('X'), undefined, 'let go of X')
|
|
236
254
|
await oneStep(start)
|
|
237
255
|
assert.equal(await read(() => probe.value('box.size.x')), 400, 'its own width is the 400 it was')
|
|
238
256
|
assert.equal(Math.round((await read(() => probe.body('box'))).dimensions['size.x']), 400)
|
|
239
|
-
assert.
|
|
257
|
+
assert.deepEqual((await sources()).map(s => s.text), ['Y', 'Z'].map(axis => `figure.label.size-figure ${axis}`), 'X follows nothing')
|
|
240
258
|
})
|
|
241
259
|
})
|
|
242
260
|
|
|
@@ -251,7 +269,7 @@ function sizedPair(turning) {
|
|
|
251
269
|
if (!turning) return a
|
|
252
270
|
a = fasten(fasten(a, 'base', 'plane'), 'flange', 'base')
|
|
253
271
|
const j = a.joints.find(o => o.body1.part === 'flange')
|
|
254
|
-
return applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: 0, upperLimit: 90, state: { id: 'flange-turn' } } })
|
|
272
|
+
return applyFigureAuthoring(a, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: 0, upperLimit: 90, state: { id: 'flange-turn' } }] } })
|
|
255
273
|
}
|
|
256
274
|
|
|
257
275
|
describe('the sizes a figure may be placed at, and how it grows', () => {
|
|
@@ -309,7 +327,7 @@ describe('a row\'s pitch', () => {
|
|
|
309
327
|
await pick('roller')
|
|
310
328
|
/* The row is the 「반응형」 tab's. */
|
|
311
329
|
await tab('response')
|
|
312
|
-
assert.deepEqual((await all('input[aria-label="
|
|
330
|
+
assert.deepEqual((await all('input[aria-label="figure.label.repeat-gap"]')).map(i => i.value), ['200'], 'a tenth of 2000')
|
|
313
331
|
assert.ok((await all('ul[row-reading] li')).length > 0, 'the row said as it stands')
|
|
314
332
|
})
|
|
315
333
|
|
|
@@ -323,7 +341,7 @@ describe('a row\'s pitch', () => {
|
|
|
323
341
|
await pick('roller')
|
|
324
342
|
await tab('response')
|
|
325
343
|
const start = await state()
|
|
326
|
-
await type(await handle('input[aria-label="
|
|
344
|
+
await type(await handle('input[aria-label="figure.label.repeat-gap"]'), '300')
|
|
327
345
|
await oneStep(start)
|
|
328
346
|
const now = await source()
|
|
329
347
|
assert.equal(now.designInputs.pitch, 300, 'the input is 300')
|
|
@@ -508,7 +526,7 @@ describe('past the edge of the face a part is fastened to', () => {
|
|
|
508
526
|
a = fasten(box(a, 'out', [100, 100, 100]), 'out', 'frame', { align: { x: { mm: 480 } } })
|
|
509
527
|
const mesh = { asset: 'a'.repeat(64), format: 'glb', unit: 'mm', bounds: { min: { x: -50, y: -50, z: -50 }, max: { x: 50, y: 50, z: 50 } }, triangles: 12, primitives: 1 }
|
|
510
528
|
a = addFigurePrimitive(a, { id: 'tilted', kind: 'mesh', dimensions: { size: 100 }, mesh, position: { x: 0, y: 0, z: 0 } })
|
|
511
|
-
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'tilted',
|
|
529
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'tilted', body0: { part: 'frame', face: 'top' }, body1: { part: 'tilted' }, origin: { align: { x: { mm: 460 } }, childRotate: { y: 30 } } } })
|
|
512
530
|
a = fasten(addFigurePrimitive(a, { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 200 }, position: { x: 0, y: 0, z: 0 } }), 'drum', 'frame')
|
|
513
531
|
a = fasten(box(a, 'cap', [50, 50, 50]), 'cap', 'drum', { align: { x: { mm: 70 }, z: { mm: 70 } } })
|
|
514
532
|
await mount('overhangs', fasten(box(a, 'plate', [1600, 20, 200]), 'plate', 'frame', { align: { x: { mm: 200 } } }))
|
|
@@ -593,11 +611,12 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
|
|
|
593
611
|
assert.deepEqual(await all('[detach-words]'), [])
|
|
594
612
|
})
|
|
595
613
|
|
|
596
|
-
|
|
614
|
+
/* A row that cannot be made says why open, under a head that says it is not made (2026-10-24). */
|
|
615
|
+
test('a part fastened to nothing: the row stands open under its head and says why', async () => {
|
|
597
616
|
await pick('loose')
|
|
598
617
|
await tab('response')
|
|
599
|
-
assert.deepEqual((await all('details[row-fold] > summary h3')).map(h => h.text), ['figure.label.repeat'])
|
|
600
|
-
assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [
|
|
618
|
+
assert.deepEqual((await all('details[row-fold] > summary h3')).map(h => h.text), ['figure.label.repeat figure.label.repeat-inactive'])
|
|
619
|
+
assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [true], 'open')
|
|
601
620
|
assert.deepEqual((await all('p[row-blocked]')).map(p => p.text), ['figure.text.row-blocked-unfastened'])
|
|
602
621
|
})
|
|
603
622
|
|
|
@@ -83,7 +83,7 @@ function sourceOf(kind, text) {
|
|
|
83
83
|
/* On the roller's side at 0° round its own Y -- its own +X (ADR-0140 3.9). */
|
|
84
84
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'mark', to: { part: 'roller', surface: { around: 0, along: 'centre' } } })
|
|
85
85
|
const j = asset.joints.find(o => o.body1.part === 'roller')
|
|
86
|
-
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } } })
|
|
86
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -360, upperLimit: 360, state: { id: 'spin' } }] } })
|
|
87
87
|
return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 }))
|
|
88
88
|
}
|
|
89
89
|
throw Error(`no figure for ${kind}`)
|