@things-factory/figure-ui 10.1.46 → 10.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/client/modeller/angle-dial.ts +70 -0
  2. package/client/modeller/figure-settings.ts +328 -0
  3. package/client/modeller/figure-v3-capability.ts +137 -0
  4. package/client/modeller/figure-v3-clips.ts +253 -0
  5. package/client/modeller/figure-v3-controls.ts +212 -0
  6. package/client/modeller/figure-v3-links.ts +61 -81
  7. package/client/modeller/figure-v3-material.ts +108 -35
  8. package/client/modeller/figure-v3-parts.ts +96 -3
  9. package/client/modeller/figure-v3-preview.ts +326 -0
  10. package/client/modeller/figure-v3-properties.ts +29 -57
  11. package/client/modeller/figure-v3-verdict.ts +598 -60
  12. package/client/modeller/figure-view.ts +152 -0
  13. package/client/modeller/side-panel-styles.ts +326 -0
  14. package/client/modeller/v3-canvas.ts +254 -262
  15. package/client/modeller/v3-clips.ts +89 -0
  16. package/client/modeller/v3-controls.ts +54 -0
  17. package/client/modeller/v3-editor.ts +566 -223
  18. package/client/modeller/v3-fields.ts +84 -0
  19. package/client/modeller/v3-help.ts +41 -0
  20. package/client/modeller/v3-joint-fan.ts +292 -0
  21. package/client/modeller/v3-keys.ts +34 -0
  22. package/client/modeller/v3-links.ts +101 -9
  23. package/client/modeller/v3-live.ts +79 -0
  24. package/client/modeller/v3-maturity.ts +415 -0
  25. package/client/modeller/v3-palette.ts +45 -0
  26. package/client/modeller/v3-preview-model.ts +54 -0
  27. package/client/modeller/v3-stage-model.ts +159 -0
  28. package/client/modeller/v3-thumbnail.ts +8 -4
  29. package/client/pages/figure-list-page.ts +4 -4
  30. package/client/pages/figure-v3-modeller-page.ts +66 -134
  31. package/dist-client/modeller/angle-dial.d.ts +32 -0
  32. package/dist-client/modeller/angle-dial.js +41 -0
  33. package/dist-client/modeller/angle-dial.js.map +1 -0
  34. package/dist-client/modeller/figure-settings.d.ts +39 -0
  35. package/dist-client/modeller/figure-settings.js +317 -0
  36. package/dist-client/modeller/figure-settings.js.map +1 -0
  37. package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
  38. package/dist-client/modeller/figure-v3-capability.js +141 -0
  39. package/dist-client/modeller/figure-v3-capability.js.map +1 -0
  40. package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
  41. package/dist-client/modeller/figure-v3-clips.js +268 -0
  42. package/dist-client/modeller/figure-v3-clips.js.map +1 -0
  43. package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
  44. package/dist-client/modeller/figure-v3-controls.js +226 -0
  45. package/dist-client/modeller/figure-v3-controls.js.map +1 -0
  46. package/dist-client/modeller/figure-v3-links.d.ts +4 -1
  47. package/dist-client/modeller/figure-v3-links.js +64 -82
  48. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  49. package/dist-client/modeller/figure-v3-material.d.ts +4 -1
  50. package/dist-client/modeller/figure-v3-material.js +109 -35
  51. package/dist-client/modeller/figure-v3-material.js.map +1 -1
  52. package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
  53. package/dist-client/modeller/figure-v3-parts.js +88 -3
  54. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  55. package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
  56. package/dist-client/modeller/figure-v3-preview.js +322 -0
  57. package/dist-client/modeller/figure-v3-preview.js.map +1 -0
  58. package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
  59. package/dist-client/modeller/figure-v3-properties.js +29 -57
  60. package/dist-client/modeller/figure-v3-properties.js.map +1 -1
  61. package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
  62. package/dist-client/modeller/figure-v3-verdict.js +579 -59
  63. package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
  64. package/dist-client/modeller/figure-view.d.ts +118 -0
  65. package/dist-client/modeller/figure-view.js +78 -0
  66. package/dist-client/modeller/figure-view.js.map +1 -0
  67. package/dist-client/modeller/side-panel-styles.d.ts +23 -0
  68. package/dist-client/modeller/side-panel-styles.js +326 -0
  69. package/dist-client/modeller/side-panel-styles.js.map +1 -0
  70. package/dist-client/modeller/v3-canvas.d.ts +44 -37
  71. package/dist-client/modeller/v3-canvas.js +230 -260
  72. package/dist-client/modeller/v3-canvas.js.map +1 -1
  73. package/dist-client/modeller/v3-clips.d.ts +39 -0
  74. package/dist-client/modeller/v3-clips.js +58 -0
  75. package/dist-client/modeller/v3-clips.js.map +1 -0
  76. package/dist-client/modeller/v3-controls.d.ts +28 -0
  77. package/dist-client/modeller/v3-controls.js +28 -0
  78. package/dist-client/modeller/v3-controls.js.map +1 -0
  79. package/dist-client/modeller/v3-editor.d.ts +73 -1
  80. package/dist-client/modeller/v3-editor.js +593 -218
  81. package/dist-client/modeller/v3-editor.js.map +1 -1
  82. package/dist-client/modeller/v3-fields.d.ts +39 -0
  83. package/dist-client/modeller/v3-fields.js +69 -0
  84. package/dist-client/modeller/v3-fields.js.map +1 -0
  85. package/dist-client/modeller/v3-help.d.ts +18 -0
  86. package/dist-client/modeller/v3-help.js +37 -0
  87. package/dist-client/modeller/v3-help.js.map +1 -0
  88. package/dist-client/modeller/v3-joint-fan.d.ts +9 -0
  89. package/dist-client/modeller/v3-joint-fan.js +264 -0
  90. package/dist-client/modeller/v3-joint-fan.js.map +1 -0
  91. package/dist-client/modeller/v3-keys.d.ts +14 -0
  92. package/dist-client/modeller/v3-keys.js +23 -0
  93. package/dist-client/modeller/v3-keys.js.map +1 -0
  94. package/dist-client/modeller/v3-links.d.ts +35 -1
  95. package/dist-client/modeller/v3-links.js +101 -8
  96. package/dist-client/modeller/v3-links.js.map +1 -1
  97. package/dist-client/modeller/v3-live.d.ts +31 -0
  98. package/dist-client/modeller/v3-live.js +58 -0
  99. package/dist-client/modeller/v3-live.js.map +1 -0
  100. package/dist-client/modeller/v3-maturity.d.ts +120 -0
  101. package/dist-client/modeller/v3-maturity.js +317 -0
  102. package/dist-client/modeller/v3-maturity.js.map +1 -0
  103. package/dist-client/modeller/v3-palette.d.ts +7 -0
  104. package/dist-client/modeller/v3-palette.js +42 -0
  105. package/dist-client/modeller/v3-palette.js.map +1 -0
  106. package/dist-client/modeller/v3-preview-model.d.ts +32 -0
  107. package/dist-client/modeller/v3-preview-model.js +48 -0
  108. package/dist-client/modeller/v3-preview-model.js.map +1 -0
  109. package/dist-client/modeller/v3-stage-model.d.ts +31 -0
  110. package/dist-client/modeller/v3-stage-model.js +121 -0
  111. package/dist-client/modeller/v3-stage-model.js.map +1 -0
  112. package/dist-client/modeller/v3-thumbnail.js +6 -3
  113. package/dist-client/modeller/v3-thumbnail.js.map +1 -1
  114. package/dist-client/pages/figure-list-page.js +4 -4
  115. package/dist-client/pages/figure-list-page.js.map +1 -1
  116. package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
  117. package/dist-client/pages/figure-v3-modeller-page.js +64 -136
  118. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  119. package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
  120. package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -1
  121. package/dist-client/tsconfig.tsbuildinfo +1 -1
  122. package/package.json +6 -6
  123. package/test/v3-clips.test.ts +70 -0
  124. package/test/v3-controls.test.ts +68 -0
  125. package/test/v3-fields.test.ts +71 -0
  126. package/test/v3-gizmo.test.ts +3 -5
  127. package/test/v3-joint-fan.test.ts +210 -0
  128. package/test/v3-keys.test.ts +37 -0
  129. package/test/v3-layout.test.ts +78 -0
  130. package/test/v3-links.test.ts +89 -6
  131. package/test/v3-live.test.ts +99 -0
  132. package/test/v3-material.test.ts +6 -0
  133. package/test/v3-maturity.test.ts +268 -0
  134. package/test/v3-palette-icons.test.ts +22 -0
  135. package/test/v3-parts-wiring.test.ts +3 -1
  136. package/test/v3-preview.test.ts +42 -0
  137. package/test/v3-properties.test.ts +4 -2
  138. package/test/v3-scene-canvas.test.ts +85 -0
  139. package/test/v3-stage-model.test.ts +25 -0
  140. package/test/v3-theme.test.ts +16 -25
  141. package/test/v3-thumbnail.test.ts +29 -0
  142. package/test/v3-verdict.test.ts +12 -6
  143. package/test/v3-view.test.ts +20 -0
  144. package/translations/en.json +90 -6
  145. package/translations/ja.json +92 -8
  146. package/translations/ko.json +90 -6
  147. package/translations/ms.json +92 -8
  148. package/translations/zh.json +92 -8
@@ -0,0 +1,159 @@
1
+ import * as THREE from 'three'
2
+ import { buildV3Geometry } from '@hatiolab/things-scene'
3
+
4
+ import type { GizmoResult } from './v3-gizmo.js'
5
+ import type { V3RenderPart } from './v3-renderer.js'
6
+
7
+ /*
8
+ * V3 bodies <-> scene components, in one place (V2's `figure-source.ts` was that place for V2 parts).
9
+ *
10
+ * The modeller stands on a things-scene board in editing mode, as V2's did: each body figure-model evaluates is a
11
+ * `figure-v3-part` component, and what the scene's gizmo writes back comes here to become a change of the part's
12
+ * inputs. The two vocabularies:
13
+ *
14
+ * V3 (asset frame, mm, y up) scene component
15
+ * ──────────────────────────────── ─────────────────────────────────────────────
16
+ * body box extent x · y · z width · depth · height
17
+ * body box centre x left + width/2 - board width/2
18
+ * y zPos + depth/2
19
+ * z top + height/2 - board height/2
20
+ * body rotation (rigid@1, Rx·Ry·Rz) rotationX · -rotation · rotationY (radians; three 'XYZ')
21
+ *
22
+ * The box is the body's own bounding box in its local frame, so a turned body keeps its size and the gizmo's handles
23
+ * sit on its faces. Its centre is not always the body's origin (a portal rises from its bottom), so a turn about the
24
+ * centre moves the origin: the result gives the origin where the turned body puts it.
25
+ */
26
+
27
+ export interface Board {
28
+ width: number
29
+ height: number
30
+ }
31
+
32
+ export interface BodyFrame {
33
+ /** The body's box: its centre in the body's own frame, and its extent. */
34
+ centre: THREE.Vector3
35
+ extent: THREE.Vector3
36
+ rotation: THREE.Matrix4
37
+ origin: THREE.Vector3
38
+ }
39
+
40
+ const round = (n: number) => Math.round(n * 1e6) / 1e6
41
+
42
+ export function frameOf(part: V3RenderPart): BodyFrame | null {
43
+ /* The box things-scene's `figure-v3-part` centres the body on: the same builder, measured the same way. */
44
+ let built: THREE.BufferGeometry | undefined
45
+ try {
46
+ built = buildV3Geometry(part.shape as never, part.segments) as THREE.BufferGeometry | undefined
47
+ } catch {
48
+ return null
49
+ }
50
+ if (!built?.getAttribute('position')) return null
51
+ built.computeBoundingBox()
52
+ const box = built.boundingBox!
53
+ const unit = { centre: box.getCenter(new THREE.Vector3()), extent: box.getSize(new THREE.Vector3()) }
54
+ built.dispose()
55
+ const r = part.pose.r
56
+ const rotation = new THREE.Matrix4().set(r[0]![0]!, r[0]![1]!, r[0]![2]!, 0, r[1]![0]!, r[1]![1]!, r[1]![2]!, 0, r[2]![0]!, r[2]![1]!, r[2]![2]!, 0, 0, 0, 0, 1)
57
+ return { centre: unit.centre, extent: unit.extent, rotation, origin: new THREE.Vector3(...(part.pose.t as [number, number, number])) }
58
+ }
59
+
60
+ /** The colours a body is drawn in, from the palette the modeller resolved (the asset's, over which the theme wins). */
61
+ function colours(part: V3RenderPart, palette: Record<string, string>) {
62
+ const m = part.material
63
+ const glow = m.emissive?.on ? { color: palette[m.emissive.token] ?? palette[m.token] ?? '#9a9a9a', intensity: m.emissive.intensity } : undefined
64
+ return { color: palette[m.token] ?? '#9a9a9a', preset: m.preset, transparent: !!m.transparent, ...(glow ? { glow } : {}) }
65
+ }
66
+
67
+ /** The component state that draws a body where figure-model put it. */
68
+ export function componentOf(part: V3RenderPart, palette: Record<string, string>, board: Board): Record<string, unknown> | null {
69
+ const frame = frameOf(part)
70
+ if (!frame) return null
71
+ const { centre, extent, rotation, origin } = frame
72
+ const world = centre.clone().applyMatrix4(rotation).add(origin)
73
+ const euler = new THREE.Euler().setFromRotationMatrix(rotation, 'XYZ')
74
+ const width = extent.x || 1,
75
+ depth = extent.y || 1,
76
+ height = extent.z || 1
77
+ return {
78
+ type: 'figure-v3-part',
79
+ name: part.id,
80
+ shape: part.shape,
81
+ ...(part.segments !== undefined ? { segments: part.segments } : {}),
82
+ ...colours(part, palette),
83
+ width: round(width),
84
+ depth: round(depth),
85
+ height: round(height),
86
+ left: round(world.x + board.width / 2 - width / 2),
87
+ top: round(world.z + board.height / 2 - height / 2),
88
+ zPos: round(world.y - depth / 2),
89
+ rotationX: round(euler.x),
90
+ rotation: round(-euler.y),
91
+ rotationY: round(euler.z)
92
+ }
93
+ }
94
+
95
+ const num = (v: unknown, fallback = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback)
96
+
97
+ /**
98
+ * What the gizmo did, in the inputs' terms: where the body's origin is now, how it is turned (degrees, rigid@1's
99
+ * order) and how much each of its own axes grew. Only what changed is given; a component the gizmo did not move gives
100
+ * nothing.
101
+ */
102
+ export function gizmoResultOf(part: V3RenderPart, after: Record<string, unknown>, board: Board): GizmoResult {
103
+ const frame = frameOf(part)
104
+ if (!frame) return {}
105
+ const { centre, extent, rotation, origin } = frame
106
+ const width = num(after.width, extent.x),
107
+ depth = num(after.depth, extent.y),
108
+ height = num(after.height, extent.z)
109
+ const scale = new THREE.Vector3(width / (extent.x || 1), depth / (extent.y || 1), height / (extent.z || 1))
110
+ const world = new THREE.Vector3(num(after.left) + width / 2 - board.width / 2, num(after.zPos) + depth / 2, num(after.top) + height / 2 - board.height / 2)
111
+ const euler = new THREE.Euler(num(after.rotationX), -num(after.rotation), num(after.rotationY), 'XYZ')
112
+ const turnedTo = new THREE.Matrix4().makeRotationFromEuler(euler)
113
+
114
+ const before = new THREE.Quaternion().setFromRotationMatrix(rotation)
115
+ const now = new THREE.Quaternion().setFromEuler(euler)
116
+ const turned = before.angleTo(now) > 1e-4
117
+ const sized = [scale.x, scale.y, scale.z].some(s => Math.abs(s - 1) > 1e-4)
118
+ /* The origin that puts the (scaled, turned) box centre where the scene has it. */
119
+ const newOrigin = world.clone().sub(centre.clone().multiply(scale).applyMatrix4(turnedTo))
120
+ const moved = newOrigin.distanceTo(origin) > 0.5
121
+
122
+ const result: GizmoResult = {}
123
+ if (moved || turned) result.position = newOrigin.toArray() as [number, number, number]
124
+ if (turned) result.rotation = [euler.x, euler.y, euler.z].map(THREE.MathUtils.radToDeg) as [number, number, number]
125
+ if (sized) result.scale = scale.toArray() as [number, number, number]
126
+ return result
127
+ }
128
+
129
+ /** The keys of `next` whose values differ from `current` -- only those are set, so the scene raises no echo. */
130
+ export function changedKeys(current: Record<string, unknown>, next: Record<string, unknown>): Record<string, unknown> | null {
131
+ const diff: Record<string, unknown> = {}
132
+ for (const [key, value] of Object.entries(next)) {
133
+ if (key === 'type') continue
134
+ const was = current[key]
135
+ const same = typeof value === 'object' && value !== null ? JSON.stringify(was) === JSON.stringify(value) : was === value
136
+ if (!same) diff[key] = value
137
+ }
138
+ return Object.keys(diff).length ? diff : null
139
+ }
140
+
141
+ /** A board wide and deep enough for the figure, with room around it; its size only grows while a figure is edited. */
142
+ export function boardFor(parts: readonly V3RenderPart[], at: Board & { depth?: number } = { width: 0, height: 0 }): Board & { depth: number } {
143
+ const box = new THREE.Box3()
144
+ for (const part of parts) {
145
+ const frame = frameOf(part)
146
+ if (!frame) continue
147
+ const half = frame.extent.clone().multiplyScalar(0.5)
148
+ const corners = [-1, 1].flatMap(x => [-1, 1].flatMap(y => [-1, 1].map(z => new THREE.Vector3(x * half.x, y * half.y, z * half.z))))
149
+ for (const corner of corners) box.expandByPoint(corner.add(frame.centre).applyMatrix4(frame.rotation).add(frame.origin))
150
+ }
151
+ const size = box.isEmpty() ? new THREE.Vector3(1000, 1000, 1000) : box.getSize(new THREE.Vector3())
152
+ const reach = (axis: 'x' | 'z') => 2 * Math.max(Math.abs(box.isEmpty() ? 500 : box.min[axis]), Math.abs(box.isEmpty() ? 500 : box.max[axis]))
153
+ return {
154
+ width: Math.max(at.width, Math.ceil(reach('x') * 1.2), 200),
155
+ height: Math.max(at.height, Math.ceil(reach('z') * 1.2), 200),
156
+ /* Only grows, as the plan does: a raised arm made the board taller than it was, and a new board lost the camera. */
157
+ depth: Math.max(at.depth ?? 0, Math.ceil(size.y * 1.1), 200)
158
+ }
159
+ }
@@ -11,8 +11,9 @@ import type { V3RenderPart } from './v3-renderer.js'
11
11
  *
12
12
  * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있다. 그것을 찍으면 카드마다 프레임이
13
13
  * 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이 된다. 목록은 견주는 자리이므로 그림은
14
- * **정해진 크기와 정해진 카메라**로 찍는다. `figure-v3-canvas` 의 `thumbnail()` 은 보이는 캔버스를
15
- * 그대로 뽑으므로 카드용이 아니다 — 여기서는 새로 세운 캔버스가 스스로 `fit()` 한 자리에서 찍는다.
14
+ * **정해진 크기와 정해진 카메라**로 찍는다 — 새로 세운 캔버스(`viewOnly`)가 스스로 `fit()` 한 자리에서
15
+ * `snapshot()` 으로 찍는다. 캔버스가 씬 위로 옮겨 가면서 찍는 길이 빠졌고, 그동안 저장은 그림 없이 갔다
16
+ * (사용자 2026-09-24).
16
17
  *
17
18
  * ## 왜 서버가 아니라 브라우저인가
18
19
  *
@@ -37,7 +38,8 @@ interface Shooter extends HTMLElement {
37
38
  parts?: V3RenderPart[]
38
39
  palette?: Record<string, string>
39
40
  context?: string
40
- thumbnail?: () => string | undefined
41
+ viewOnly?: boolean
42
+ snapshot?: (width?: number, height?: number) => Promise<string | undefined>
41
43
  fit?: () => void
42
44
  }
43
45
 
@@ -74,6 +76,7 @@ export async function captureV3Thumbnail(
74
76
  shooter.parts = parts
75
77
  shooter.palette = palette
76
78
  shooter.context = 'thumbnail'
79
+ shooter.viewOnly = true
77
80
  shooter.style.cssText = 'display: block; width: 100%; height: 100%;'
78
81
 
79
82
  try {
@@ -82,7 +85,8 @@ export async function captureV3Thumbnail(
82
85
  (async () => {
83
86
  await settle()
84
87
  shooter.fit?.()
85
- return shooter.thumbnail?.()
88
+ await settle()
89
+ return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined
86
90
  })(),
87
91
  PATIENCE
88
92
  )
@@ -428,12 +428,12 @@ export class FigureListPage extends FigureListPageBase {
428
428
  카드에 남긴 하나뿐인 숫자. **이름표를 달지 않으므로** 무엇인지는 손이 갔을 때 뜨는
429
429
  말이 알려 준다.
430
430
 
431
- 색칠한 배지로 두지 않는다. 지금 카탈로그의 열넷 중 열셋이 0 점이라(선언된 묶음 한도
432
- 3 을 넘긴다) 붉은 덩어리가 카드마다 서면 목록이 시끄럽고, 그러면서 아무것도 가르지
433
- 못한다. 숫자는 조용히 적고 낮은 것에만 **점 하나**를 붙인다.
431
+ 색칠한 배지로 두지 않는다. 옛 draw call 한도 3 으로 재던 때는 카탈로그 거의 모두가 0 점이라
432
+ 붉은 덩어리가 카드마다 섰고 아무것도 가르지 못했다. V3 는 「한 화면 N 대」 목표에 견준
433
+ 여유라(`v3ScoreOf`, 2026-09-24) 퍼지지만, 숫자는 조용히 적고 낮은 것에만 **점 하나**를 붙인다.
434
434
 
435
435
  아직 재지 않은 것을 0 으로 보여 주지 않는다 — 0 은 「가볍다」는 뜻이 되어 버린다.
436
- 등급 문자(A~D)도 여기서 만들지 않는다: 경계는 `figure-model` 의 `scoreOf` 가 정하고,
436
+ 등급 문자(A~D)도 여기서 만들지 않는다: 경계는 `figure-model` 의 `scoreOf` · `v3ScoreOf` 가 정하고,
437
437
  화면이 그것을 베끼면 두 벌이 되어 목록과 저작면이 다른 등급을 말한다.
438
438
  */
439
439
  renderer: (value: number | null) =>
@@ -12,6 +12,7 @@ import '@material/web/icon/icon.js'
12
12
  import '../modeller/figure-history-panel.js'
13
13
  import '../modeller/figure-v3-verdict.js'
14
14
  import '../modeller/figure-v3-properties.js'
15
+ import type { V3BudgetLine } from '../modeller/v3-maturity.js'
15
16
 
16
17
  /**
17
18
  * The V3 product page has exactly one authoritative workspace for UI and AI input transactions.
@@ -39,31 +40,25 @@ export class FigureV3ModellerPage extends PageView {
39
40
  min-width: 0;
40
41
  min-height: 0;
41
42
  }
42
- aside {
43
- width: 320px;
44
- flex: none;
45
- min-height: 0;
46
- overflow: auto;
47
- display: flex;
48
- flex-direction: column;
49
- border-left: 1px solid var(--md-sys-color-outline-variant);
50
- }
43
+ /* The first field of the properties tab, in the side panel's look (side-panel-styles): a label above the field. */
51
44
  label[type-field] {
52
45
  display: grid;
53
- gap: 4px;
54
- padding: 12px 12px 8px;
55
- font-size: 12px;
56
- border-bottom: 1px solid var(--md-sys-color-outline-variant);
46
+ gap: 2px;
47
+ margin: 0 0 5px;
48
+ font-size: 0.68rem;
49
+ color: var(--md-sys-color-on-surface-variant);
57
50
  }
58
51
  label[type-field] input,
59
52
  label[type-field] code {
60
- font: inherit;
61
- font-family: monospace;
62
- padding: 6px 8px;
63
- border: 1px solid var(--md-sys-color-outline-variant);
64
- border-radius: 4px;
65
- background: var(--md-sys-color-surface);
53
+ box-sizing: border-box;
54
+ min-height: 30px;
55
+ padding: 4px 7px;
56
+ font-family: var(--mono-font, monospace);
57
+ font-size: 0.76rem;
66
58
  color: var(--md-sys-color-on-surface);
59
+ background: var(--md-sys-color-surface-container-lowest);
60
+ border: 1px solid var(--md-sys-color-outline-variant);
61
+ border-radius: 6px;
67
62
  }
68
63
  label[type-field] small {
69
64
  color: var(--md-sys-color-on-surface-variant);
@@ -77,57 +72,9 @@ export class FigureV3ModellerPage extends PageView {
77
72
  label[type-field] small[role='alert'] {
78
73
  color: var(--md-sys-color-error);
79
74
  }
80
- /* The V2 side panel's tabs, drawn the same way: one icon per tab, the name in title and aria-label. */
81
- div[tabs] {
82
- display: flex;
83
- flex: none;
84
- border-bottom: 1px solid var(--md-sys-color-outline-variant);
85
- }
86
- button[tab] {
87
- flex: 1;
88
- position: relative;
89
- display: flex;
90
- align-items: center;
91
- justify-content: center;
92
- padding: 7px 6px;
93
- color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
94
- background: none;
95
- border: none;
96
- border-bottom: 2px solid transparent;
97
- cursor: pointer;
98
- }
99
- button[tab]:hover:not([on]) {
100
- background-color: var(--md-sys-color-surface-container);
101
- }
102
- button[tab][on] {
103
- color: var(--md-sys-color-primary);
104
- border-bottom-color: var(--md-sys-color-primary);
105
- }
106
- button[tab] md-icon {
107
- font-size: 20px;
108
- width: 20px;
109
- height: 20px;
110
- }
111
- span[badge] {
112
- position: absolute;
113
- top: 5px;
114
- right: 2px;
115
- padding: 0 5px;
116
- font-size: 0.62rem;
117
- font-weight: 700;
118
- line-height: 1.5;
119
- color: var(--md-sys-color-on-primary-container);
120
- background-color: var(--md-sys-color-primary-container);
121
- border-radius: 999em;
122
- pointer-events: none;
123
- }
124
- /* Hidden, not removed: the verdict keeps its last answer and the panel its list while the other tab is open. */
125
- [off] {
126
- display: none;
127
- }
128
75
  .error {
129
76
  padding: 16px;
130
- color: #922716;
77
+ color: var(--md-sys-color-error);
131
78
  }
132
79
  `
133
80
  private readonly session = createV3ProductSession(v3ProductStore)
@@ -149,12 +96,9 @@ export class FigureV3ModellerPage extends PageView {
149
96
  @state() private typeError = ''
150
97
  /** Why a command from the side did nothing. Shown beside the tabs — the editor stays, since nothing in it was lost. */
151
98
  @state() private commandError = ''
152
- /**
153
- * Which tab the side shows. The verdict comes first: it is what the author needs while editing, and the count on
154
- * its tab stays in sight while the history is open — as on the V2 page.
155
- */
156
- @state() private tab: 'properties' | 'verdict' | 'history' = 'verdict'
99
+ /** The verdict's count, handed to the editor's verdict tab. */
157
100
  @state() private instances?: number
101
+ @state() private budget?: V3BudgetLine
158
102
  /**
159
103
  * The source the verdict judges. The session refuses to export while it loads or saves, so the last one read is
160
104
  * kept rather than blanked — the verdict would otherwise flicker to 「waiting」 on every save.
@@ -364,66 +308,54 @@ export class FigureV3ModellerPage extends PageView {
364
308
  render() {
365
309
  if (this.error) return html`<p role="alert" class="error">${this.error}</p>`
366
310
  const view = this.session.read()
367
- return html`<figure-v3-editor
368
- .session=${this.session}
369
- .revision=${this.revision}
370
- @v3-changed=${this.changed}
371
- ></figure-v3-editor>
372
- <aside>
373
- <label type-field>
374
- <span>${i18next.t('figure.label.figure-type')}</span>
375
- ${this.row.version > 0
376
- ? html`<code>${this.row.type}</code><small>${i18next.t('figure.text.type-name-cannot-change')}</small>`
377
- : html`<input
378
- .value=${this.row.type ?? ''}
379
- placeholder=${i18next.t('figure.text.figure-type-placeholder')}
380
- ?disabled=${!view.id}
381
- @change=${this.renameType}
382
- />`}
383
- ${this.typeError ? html`<small role="alert">${this.typeError}</small>` : ''}
384
- </label>
385
- ${this.commandError ? html`<p fit-error role="alert">${this.commandError}</p>` : ''}
386
- <div tabs>
387
- ${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
388
- ${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'), this.instances)}
389
- ${this.renderTab('history', 'history', i18next.t('figure.label.history'))}
390
- </div>
391
- <figure-v3-properties
392
- ?off=${this.tab !== 'properties'}
393
- .source=${this.draftSource()}
394
- .figureId=${view.id}
395
- .figureCategory=${this.row.category}
396
- .figureTags=${this.row.tags}
397
- @v3-command=${this.onCommand}
398
- @v3-catalog=${this.onCatalog}
399
- ></figure-v3-properties>
400
- <figure-v3-verdict
401
- ?off=${this.tab !== 'verdict'}
402
- .source=${this.draftSource()}
403
- .figureId=${view.id}
404
- .figureType=${this.row.type}
405
- @capacity-changed=${(event: CustomEvent) => (this.instances = event.detail.instances)}
406
- @fit-base-to-parts=${this.fitToParts}
407
- ></figure-v3-verdict>
408
- <figure-history-panel
409
- ?off=${this.tab !== 'history'}
410
- format="v3"
411
- .figureId=${view.id}
412
- .figureState=${this.row.state}
413
- .figureVersion=${this.row.version}
414
- .figureType=${this.row.type}
415
- .dirty=${!!view.dirty}
416
- @fit-base-to-parts=${this.fitToParts}
417
- @figure-released=${this.released}
418
- @figure-reverted=${this.reverted}
419
- ></figure-history-panel>
420
- </aside>`
421
- }
422
- private renderTab(tab: 'properties' | 'verdict' | 'history', icon: string, label: string, badge?: number) {
423
- return html`
424
- <button tab ?on=${this.tab === tab} title=${label} aria-label=${label} @click=${() => (this.tab = tab)}>
425
- <md-icon>${icon}</md-icon>${badge == null ? '' : html`<span badge>${badge.toLocaleString()}</span>`}
426
- </button>
427
- `
311
+ return html`<figure-v3-editor .session=${this.session} .revision=${this.revision} .instances=${this.instances} .budget=${this.budget} @v3-changed=${this.changed}>
312
+ ${this.commandError ? html`<p slot="notice" fit-error role="alert">${this.commandError}</p>` : ''}
313
+ <figure-v3-properties
314
+ slot="figure"
315
+ .source=${this.draftSource()}
316
+ .figureId=${view.id}
317
+ .figureCategory=${this.row.category}
318
+ .figureTags=${this.row.tags}
319
+ @v3-command=${this.onCommand}
320
+ @v3-catalog=${this.onCatalog}
321
+ >
322
+ <label slot="type" type-field>
323
+ <span>${i18next.t('figure.label.figure-type')}</span>
324
+ ${this.row.version > 0
325
+ ? html`<code>${this.row.type}</code><small>${i18next.t('figure.text.type-name-cannot-change')}</small>`
326
+ : html`<input
327
+ .value=${this.row.type ?? ''}
328
+ placeholder=${i18next.t('figure.text.figure-type-placeholder')}
329
+ ?disabled=${!view.id}
330
+ @change=${this.renameType}
331
+ />`}
332
+ ${this.typeError ? html`<small role="alert">${this.typeError}</small>` : ''}
333
+ </label>
334
+ </figure-v3-properties>
335
+ <figure-v3-verdict
336
+ slot="verdict"
337
+ .source=${this.draftSource()}
338
+ .figureId=${view.id}
339
+ .figureType=${this.row.type}
340
+ @capacity-changed=${(event: CustomEvent) => {
341
+ this.instances = event.detail.instances
342
+ this.budget = event.detail.budget
343
+ }}
344
+ @fit-base-to-parts=${this.fitToParts}
345
+ ></figure-v3-verdict>
346
+ <figure-history-panel
347
+ slot="history"
348
+ format="v3"
349
+ .figureId=${view.id}
350
+ .figureState=${this.row.state}
351
+ .figureVersion=${this.row.version}
352
+ .figureType=${this.row.type}
353
+ .dirty=${!!view.dirty}
354
+ @fit-base-to-parts=${this.fitToParts}
355
+ @figure-released=${this.released}
356
+ @figure-reverted=${this.reverted}
357
+ ></figure-history-panel>
358
+ </figure-v3-editor>`
428
359
  }
360
+
429
361
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 막대 옆에 서는 **각도 눈**. 지금 각이 범위의 어디인지를 한 눈에 보인다.
3
+ *
4
+ * ## 왜 3D 로는 모자랐나
5
+ *
6
+ * 관절이 돌아도 화면이 그대로인 경우가 있다 — 제 축에서 대칭인 부품이 그렇다. 그래서 관절이
7
+ * 모는 부품에 바늘을 달아 미리보기에서 함께 돌게 했는데(`joint-gizmo.ts` 의 `jointNeedles`),
8
+ * **미리보기에서는 그 바늘이 4 × 2 픽셀이다**(잰 값: 부품 100mm · 바늘 60 × 12mm · 여덟 대를
9
+ * 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가
10
+ * 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.
11
+ *
12
+ * 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 도형이 아무리
13
+ * 작게 서 있어도 같은 크기로 읽힌다.
14
+ */
15
+ export interface DialShape {
16
+ /** 가동 범위를 채운 부채꼴. SVG path 의 `d`. */
17
+ sector: string;
18
+ /** 지금 값이 가리키는 끝점. */
19
+ needle: {
20
+ x: number;
21
+ y: number;
22
+ };
23
+ centre: {
24
+ x: number;
25
+ y: number;
26
+ };
27
+ radius: number;
28
+ }
29
+ /**
30
+ * 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.
31
+ */
32
+ export declare function dialShape(min: number, max: number, value: number, radius?: number): DialShape;
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ /** 0 도가 위를 가리키고, 값이 커지면 시계 방향으로 돈다 — 화면의 각과 같은 방향이다. */
5
+ function at(centre, radius, degrees) {
6
+ const rad = (degrees * Math.PI) / 180;
7
+ return {
8
+ x: +(centre.x + radius * Math.sin(rad)).toFixed(2),
9
+ y: +(centre.y - radius * Math.cos(rad)).toFixed(2)
10
+ };
11
+ }
12
+ /** 한 바퀴에 가까우면 부채꼴 대신 온전한 원이다. */
13
+ const FULL = 359.9;
14
+ /**
15
+ * 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.
16
+ */
17
+ export function dialShape(min, max, value, radius = 9) {
18
+ const centre = { x: radius + 1, y: radius + 1 };
19
+ const span = Math.abs(max - min);
20
+ const held = Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max));
21
+ if (span >= FULL) {
22
+ const top = at(centre, radius, 0);
23
+ const bottom = at(centre, radius, 180);
24
+ return {
25
+ sector: `M ${top.x} ${top.y} A ${radius} ${radius} 0 1 1 ${bottom.x} ${bottom.y} A ${radius} ${radius} 0 1 1 ${top.x} ${top.y} Z`,
26
+ needle: at(centre, radius, held),
27
+ centre,
28
+ radius
29
+ };
30
+ }
31
+ const from = at(centre, radius, min);
32
+ const to = at(centre, radius, max);
33
+ const large = span > 180 ? 1 : 0;
34
+ return {
35
+ sector: `M ${centre.x} ${centre.y} L ${from.x} ${from.y} A ${radius} ${radius} 0 ${large} 1 ${to.x} ${to.y} Z`,
36
+ needle: at(centre, radius, held),
37
+ centre,
38
+ radius
39
+ };
40
+ }
41
+ //# sourceMappingURL=angle-dial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"angle-dial.js","sourceRoot":"","sources":["../../client/modeller/angle-dial.ts"],"names":[],"mappings":"AAAA;;GAEG;AA0BH,wDAAwD;AACxD,SAAS,EAAE,CAAC,MAAgC,EAAE,MAAc,EAAE,OAAe;IAC3E,MAAM,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAA;IACrC,OAAO;QACL,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QAClD,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;KACnD,CAAA;AACH,CAAC;AAED,iCAAiC;AACjC,MAAM,IAAI,GAAG,KAAK,CAAA;AAElB;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,GAAW,EAAE,KAAa,EAAE,MAAM,GAAG,CAAC;IAC3E,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,CAAA;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,CAAA;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;IAE9E,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;QACjC,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;QACtC,OAAO;YACL,MAAM,EAAE,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,UAAU,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI;YACjI,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC;YAChC,MAAM;YACN,MAAM;SACP,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEhC,OAAO;QACL,MAAM,EAAE,KAAK,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI;QAC9G,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC;QAChC,MAAM;QACN,MAAM;KACP,CAAA;AACH,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\n/**\n * 막대 옆에 서는 **각도 눈**. 지금 각이 범위의 어디인지를 한 눈에 보인다.\n *\n * ## 왜 3D 로는 모자랐나\n *\n * 관절이 돌아도 화면이 그대로인 경우가 있다 — 제 축에서 대칭인 부품이 그렇다. 그래서 관절이\n * 모는 부품에 바늘을 달아 미리보기에서 함께 돌게 했는데(`joint-gizmo.ts` 의 `jointNeedles`),\n * **미리보기에서는 그 바늘이 4 × 2 픽셀이다**(잰 값: 부품 100mm · 바늘 60 × 12mm · 여덟 대를\n * 한 화면에 세운 상태). 미리보기는 크기 반응을 보려고 여덟 대를 나란히 놓는 자리라 한 대가\n * 20 픽셀쯤이고, 그 위의 어떤 표시도 몇 픽셀을 넘지 못한다.\n *\n * 각을 읽는 일은 3D 의 배율에서 떼어 놓는다. 눈은 막대 옆 24 픽셀 자리에 서고, 도형이 아무리\n * 작게 서 있어도 같은 크기로 읽힌다.\n */\n\nexport interface DialShape {\n /** 가동 범위를 채운 부채꼴. SVG path 의 `d`. */\n sector: string\n /** 지금 값이 가리키는 끝점. */\n needle: { x: number; y: number }\n centre: { x: number; y: number }\n radius: number\n}\n\n/** 0 도가 위를 가리키고, 값이 커지면 시계 방향으로 돈다 — 화면의 각과 같은 방향이다. */\nfunction at(centre: { x: number; y: number }, radius: number, degrees: number) {\n const rad = (degrees * Math.PI) / 180\n return {\n x: +(centre.x + radius * Math.sin(rad)).toFixed(2),\n y: +(centre.y - radius * Math.cos(rad)).toFixed(2)\n }\n}\n\n/** 한 바퀴에 가까우면 부채꼴 대신 온전한 원이다. */\nconst FULL = 359.9\n\n/**\n * 눈의 모양. 값이 범위를 벗어나면 범위 끝에 세운다 — 밖을 가리키는 바늘은 없는 값을 말한다.\n */\nexport function dialShape(min: number, max: number, value: number, radius = 9): DialShape {\n const centre = { x: radius + 1, y: radius + 1 }\n const span = Math.abs(max - min)\n const held = Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max))\n\n if (span >= FULL) {\n const top = at(centre, radius, 0)\n const bottom = at(centre, radius, 180)\n return {\n sector: `M ${top.x} ${top.y} A ${radius} ${radius} 0 1 1 ${bottom.x} ${bottom.y} A ${radius} ${radius} 0 1 1 ${top.x} ${top.y} Z`,\n needle: at(centre, radius, held),\n centre,\n radius\n }\n }\n\n const from = at(centre, radius, min)\n const to = at(centre, radius, max)\n const large = span > 180 ? 1 : 0\n\n return {\n sector: `M ${centre.x} ${centre.y} L ${from.x} ${from.y} A ${radius} ${radius} 0 ${large} 1 ${to.x} ${to.y} Z`,\n needle: at(centre, radius, held),\n centre,\n radius\n }\n}\n"]}
@@ -0,0 +1,39 @@
1
+ import { LitElement } from 'lit';
2
+ import type { ViewSettings } from './figure-view.js';
3
+ declare const FigureSettings_base: (new (...args: any[]) => LitElement) & typeof LitElement;
4
+ /**
5
+ * 저작 화면의 **보기 설정**.
6
+ *
7
+ * ## 자산이 아니라 작업대다
8
+ *
9
+ * 여기서 고치는 것은 저장되지 않는다. 조명을 밝게 하고 바닥을 켠 것은 지금 보기 편한
10
+ * 상태일 뿐이다. 그래서 「저장 안 됨」을 화면에 적어 둔다 — 안 적으면 저작자가 조명을
11
+ * 맞춰 두고 그것이 자산에 딸려 간다고 여긴다.
12
+ *
13
+ * ## 값을 들고 있지 않는다
14
+ *
15
+ * 이 패널은 받은 것을 그리고 바뀐 것을 알린다. 정본은 모델러 페이지가 갖는다 — 속성
16
+ * 패널과 같은 방식이다. 여기서 들고 있으면 페이지가 다시 그릴 때 두 벌이 갈린다.
17
+ */
18
+ export declare class FigureSettings extends FigureSettings_base {
19
+ static styles: import("lit").CSSResult[];
20
+ view: ViewSettings;
21
+ render(): import("lit-html").TemplateResult<1>;
22
+ private toggle;
23
+ private slider;
24
+ /** 처음 값으로 되돌린다. 만지다 길을 잃었을 때 돌아올 자리가 있어야 한다. */
25
+ private reset;
26
+ private put;
27
+ /**
28
+ * 바뀐 것을 알린다.
29
+ *
30
+ * **알리기 전에 내 것도 바꿔 둔다.** Lit 은 속성을 다음 렌더에 넣으므로, 같은 프레임에
31
+ * 두 번 고치면 두 번째가 `this.view` 의 옛 값을 읽어 첫 번째를 덮는다. 실제로 그랬다 —
32
+ * 축 가이드를 끄고 바닥판을 켰더니 축 가이드가 도로 켜져 있었고, 화면의 체크 상자와
33
+ * 씬의 상태가 서로 다른 말을 했다.
34
+ *
35
+ * 정본은 여전히 모델러 페이지가 갖는다. 여기 값은 다음 렌더에 같은 것으로 덮인다.
36
+ */
37
+ private tell;
38
+ }
39
+ export {};