cozyclay 3.0.3 → 3.1.1

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 (246) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/INSTALL_FOR_AGENTS.md +146 -0
  3. package/README.md +74 -192
  4. package/THIRD_PARTY_NOTICES.md +0 -21
  5. package/bin/agent/agent-routes.mjs +29 -62
  6. package/bin/agent/agent-runner.mjs +5 -3
  7. package/bin/agent/main-agent.mjs +70 -0
  8. package/bin/agent/main-prompt.mjs +17 -0
  9. package/bin/agent/plan-tool.mjs +114 -0
  10. package/bin/agent/script-tool.mjs +2 -0
  11. package/bin/agent/sculpt-sketch.mjs +190 -0
  12. package/bin/agent/session-store.mjs +4 -3
  13. package/bin/agent/sketch-code.mjs +151 -0
  14. package/bin/agent/sketch-sandbox.mjs +102 -0
  15. package/bin/agent/studio-prompt.mjs +1 -1
  16. package/bin/agent/studio-task-routes.mjs +185 -0
  17. package/bin/agent/studio-tasks.mjs +156 -0
  18. package/bin/agent/studio-tools.mjs +3 -2
  19. package/bin/agent/studio-turn-transaction.mjs +76 -0
  20. package/bin/agent/turn-frames.mjs +4 -0
  21. package/bin/cozyclay.mjs +5 -7
  22. package/dist/app/index.html +7 -6
  23. package/dist/assets/TaskBoard-ngdqiu7u.js +1 -0
  24. package/dist/assets/app-BlwOpest.css +1 -0
  25. package/dist/assets/app-DmR7jBkz.js +5144 -0
  26. package/dist/assets/compile-worker-D0pKioa0.js +5 -0
  27. package/dist/assets/landing-y3EqDgSZ.js +1 -0
  28. package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  29. package/dist/assets/motion3-compile-BB3_1-EB.js +1 -0
  30. package/dist/assets/scene-asset-sync-CBx7XRti.css +1 -0
  31. package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +13 -0
  32. package/dist/assets/task-board-ErQvfoaz.js +14 -0
  33. package/dist/assets/workflow-zQM-EKW1.js +179 -0
  34. package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
  35. package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
  36. package/dist/cozyclay-package.json +1 -1
  37. package/dist/greybox-to-video/index.html +2 -3
  38. package/dist/index.html +7 -57
  39. package/dist/llms.txt +2 -1
  40. package/dist/previs-software/index.html +2 -2
  41. package/dist/privacy/index.html +3 -7
  42. package/dist/seedance-camera-control/index.html +2 -2
  43. package/dist/sitemap.xml +5 -5
  44. package/dist/sw.js +5 -1
  45. package/dist/thumbs/blank-stage-dark.webp +0 -0
  46. package/dist/thumbs/blank-stage-light.webp +0 -0
  47. package/dist/workflow/index.html +5 -4
  48. package/mcp/mesh-file.mjs +3 -1
  49. package/mcp/server.mjs +3 -0
  50. package/mcp/tool-handlers.mjs +2 -2
  51. package/package.json +5 -3
  52. package/src/App.jsx +51 -282
  53. package/src/analytics.js +3 -148
  54. package/src/app-stage.jsx +1 -1
  55. package/src/asset-pane.jsx +5 -10
  56. package/src/auto-flat.js +10 -0
  57. package/src/bus-tasks.js +11 -5
  58. package/src/command-bus.js +1 -1
  59. package/src/commands/elements.js +4 -3
  60. package/src/commands/motion3-compile.js +51 -27
  61. package/src/commands/motion3.js +3 -3
  62. package/src/commands/objects.js +1 -1
  63. package/src/commands/project.js +1 -1
  64. package/src/commands/shot.js +16 -4
  65. package/src/controls.jsx +0 -11
  66. package/src/cuts.js +11 -0
  67. package/src/domains/cast.js +3 -2
  68. package/src/domains/motion.js +3 -10
  69. package/src/domains/playback.js +32 -0
  70. package/src/domains/scenes.js +3 -22
  71. package/src/domains/shots.js +5 -5
  72. package/src/dualview.jsx +11 -15
  73. package/src/hierarchy-panel.css +18 -4
  74. package/src/hierarchy-panel.jsx +12 -11
  75. package/src/kits/held.js +3 -2
  76. package/src/light-blockers.js +2 -2
  77. package/src/main.jsx +5 -0
  78. package/src/material-look.js +189 -0
  79. package/src/material-presets.js +78 -0
  80. package/src/material-textures.js +157 -0
  81. package/src/motion/ground.js +5 -14
  82. package/src/motion/playback-frame-store.js +79 -0
  83. package/src/motion/playback-probe.js +16 -0
  84. package/src/motion/playback-subject.js +7 -0
  85. package/src/motion/timeline.css +43 -5
  86. package/src/motion/timeline.jsx +4 -3
  87. package/src/motion3/body.js +37 -5
  88. package/src/motion3/carried.js +11 -3
  89. package/src/motion3/compile-job.js +1 -1
  90. package/src/motion3/compiler/compile.js +7 -1
  91. package/src/motion3/compiler/finger-close.js +137 -0
  92. package/src/motion3/compiler/limbs.js +18 -3
  93. package/src/motion3/compiler/pose.js +6 -0
  94. package/src/motion3/compiler/validate-objects.js +17 -2
  95. package/src/motion3/compiler/validate-parts.js +2 -1
  96. package/src/motion3/compiler/vocab.js +4 -4
  97. package/src/motion3/describe.js +2 -2
  98. package/src/motion3/fix/index.js +5 -3
  99. package/src/motion3/flight.js +38 -0
  100. package/src/motion3/free-motion.js +10 -7
  101. package/src/motion3/load.js +125 -0
  102. package/src/motion3/mass.js +23 -0
  103. package/src/motion3/object-keys.js +11 -6
  104. package/src/motion3/object-pose.js +11 -3
  105. package/src/motion3/pipeline.js +3 -0
  106. package/src/motion3/qa/explain.js +3 -1
  107. package/src/motion3/qa/grasp-checks.js +123 -0
  108. package/src/motion3/qa/hints-grasp.js +71 -0
  109. package/src/motion3/qa/index.js +3 -1
  110. package/src/motion3/qa/object-checks.js +7 -2
  111. package/src/motion3/qa/patch.js +2 -1
  112. package/src/motion3/qa/spec-checks.js +10 -1
  113. package/src/motion3/schema/fields.js +3 -3
  114. package/src/motion3/schema/topics.js +9 -8
  115. package/src/motion3/schema/types.js +4 -1
  116. package/src/motion3/seat.js +84 -0
  117. package/src/motion3/settle.js +11 -6
  118. package/src/motion3/shapes.js +70 -0
  119. package/src/motion3/skills/catch.js +71 -0
  120. package/src/motion3/skills/index.js +10 -3
  121. package/src/motion3/skills/info.js +13 -0
  122. package/src/motion3/skills/throw.js +72 -0
  123. package/src/object-catalog.jsx +0 -4
  124. package/src/object-gizmo.jsx +2 -1
  125. package/src/object-look.js +9 -5
  126. package/src/object-path.js +1 -1
  127. package/src/panels/ProjectPanel.jsx +42 -21
  128. package/src/panels/details.css +15 -4
  129. package/src/popover.js +37 -0
  130. package/src/posestudio.jsx +15 -10
  131. package/src/primitive-look.js +38 -0
  132. package/src/project-browser-art.jsx +19 -0
  133. package/src/project-browser.css +74 -121
  134. package/src/project-browser.jsx +36 -60
  135. package/src/props.jsx +24 -35
  136. package/src/render-passes.js +7 -1
  137. package/src/retired-kinds.js +37 -0
  138. package/src/scene-objects.js +5 -9
  139. package/src/scenes.js +4 -5
  140. package/src/sculpt-colours.js +65 -0
  141. package/src/sculpt-compare.js +13 -50
  142. package/src/sculpt-csg.js +112 -0
  143. package/src/sculpt-curve.js +123 -0
  144. package/src/sculpt-cutters.js +43 -0
  145. package/src/sculpt-detail.js +207 -0
  146. package/src/sculpt-geometry-loft-convex.js +100 -0
  147. package/src/sculpt-geometry-shapes.js +250 -0
  148. package/src/sculpt-geometry.js +33 -15
  149. package/src/sculpt-loft-rings.js +225 -0
  150. package/src/sculpt-noise.js +65 -0
  151. package/src/sculpt-part-fields.js +27 -0
  152. package/src/sculpt-recipe-read.js +65 -0
  153. package/src/sculpt-recipe.js +33 -75
  154. package/src/sculpt-run.js +1 -1
  155. package/src/sculpt-scatter-items.js +183 -0
  156. package/src/sculpt-scatter-shape.js +57 -0
  157. package/src/sculpt-scatter.js +124 -0
  158. package/src/sculpt-shapes-curved.js +211 -0
  159. package/src/sculpt-shapes-loft-convex.js +218 -0
  160. package/src/sculpt-shapes.js +138 -0
  161. package/src/sculpt-sketch-state.js +76 -0
  162. package/src/sculpt-sketch.jsx +124 -0
  163. package/src/sculpt-surface-fields.js +65 -0
  164. package/src/settings-menu.jsx +1 -20
  165. package/src/shell/BottomDock.jsx +9 -9
  166. package/src/shell/MenuBar.jsx +1 -9
  167. package/src/shell/PreferencesDialog.jsx +3 -4
  168. package/src/shell/ViewportToolbar.jsx +3 -3
  169. package/src/shell/add-camera-shot.js +11 -0
  170. package/src/shell/glass-regions.css +3 -2
  171. package/src/shell/glass.css +0 -5
  172. package/src/shell/lens-scrub-math.js +51 -0
  173. package/src/shell/lens-scrub.css +73 -0
  174. package/src/shell/lens-scrub.jsx +98 -0
  175. package/src/shell/mode.css +6 -5
  176. package/src/shell/preferences.css +1 -32
  177. package/src/shell/shell.css +18 -1
  178. package/src/shell/studio-shell-context.js +5 -0
  179. package/src/shot-frame.js +26 -0
  180. package/src/stage-canvas-rigs.jsx +7 -8
  181. package/src/stage-characters.jsx +26 -0
  182. package/src/stage-gizmo.js +43 -0
  183. package/src/stage-labels-ko.js +0 -6
  184. package/src/stage-startup-defaults.js +3 -9
  185. package/src/starter-scenes.js +5 -11
  186. package/src/studio-agent-commands.js +2 -2
  187. package/src/studio-agent-protocol.js +2 -7
  188. package/src/studio-app-binding.js +1 -1
  189. package/src/studio-elements.js +6 -2
  190. package/src/styles/themes.css +13 -0
  191. package/src/styles.css +6 -674
  192. package/src/text/text-object.jsx +1 -1
  193. package/src/ui.jsx +8 -16
  194. package/src/use-embed-bridge.js +3 -6
  195. package/src/use-render-activity.js +3 -1
  196. package/src/view-ghosts.jsx +5 -3
  197. package/src/viewport-visible-rect.js +37 -0
  198. package/src/workflow/AgentPanel.jsx +79 -46
  199. package/src/workflow/AgentPlanCard.jsx +41 -0
  200. package/src/workflow/AgentProgressCard.jsx +1 -1
  201. package/src/workflow/TaskBoard.jsx +102 -0
  202. package/src/workflow/TaskInline.jsx +114 -0
  203. package/src/workflow/agent-client.js +25 -36
  204. package/src/workflow/agent-panel.css +14 -1
  205. package/src/workflow/agent-plan.css +93 -0
  206. package/src/workflow/agent-plan.js +59 -0
  207. package/src/workflow/attachment-image.js +10 -3
  208. package/src/workflow/attachment-video.js +91 -0
  209. package/src/workflow/task-board.css +98 -0
  210. package/src/workflow/task-board.js +169 -0
  211. package/tools/check-eslint.mjs +2 -1
  212. package/tools/check-knip.mjs +2 -1
  213. package/tools/dev/pages/greybox-to-video.html +2 -3
  214. package/tools/dev/pages/previs-software.html +2 -2
  215. package/tools/dev/pages/privacy.html +3 -7
  216. package/tools/dev/pages/seedance-camera-control.html +2 -2
  217. package/tools/dev-full.mjs +11 -2
  218. package/tools/eslint-baseline.json +6 -6
  219. package/tools/knip-baseline.json +0 -11
  220. package/tools/loc-baseline.json +30 -31
  221. package/tools/metrics-snapshot.mjs +43 -5
  222. package/tools/render-start-thumbs.mjs +175 -0
  223. package/tools/run-tests.mjs +10 -17
  224. package/tools/sitemap.mjs +1 -1
  225. package/dist/assets/analytics-BibyE5ar.js +0 -6
  226. package/dist/assets/app-DhRJ-PBz.js +0 -5088
  227. package/dist/assets/app-f-fkV5uP.css +0 -1
  228. package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
  229. package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
  230. package/dist/assets/landing-DYg87MDY.js +0 -1
  231. package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
  232. package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
  233. package/dist/assets/workflow-BSA22Ofr.js +0 -179
  234. package/dist/scenes/city-block.cclayproject +0 -1
  235. package/src/burn-in.js +0 -91
  236. package/src/camera-tutorial.jsx +0 -520
  237. package/src/facing-marks.js +0 -88
  238. package/src/first-shot-handoff.js +0 -44
  239. package/src/panels/motion.css +0 -197
  240. package/src/set-pieces.jsx +0 -183
  241. package/src/tutorial-analytics.js +0 -86
  242. package/src/tutorial-walk.js +0 -17
  243. package/src/use-camera-tutorial.js +0 -50
  244. package/src/use-semantic-state.js +0 -12
  245. package/tools/metrics/archive-traffic.mjs +0 -138
  246. package/tools/perf/ik-camera-drag.mjs +0 -138
@@ -0,0 +1,19 @@
1
+ // Start-screen art: the brand mark and the template thumbnail that fills both
2
+ // the card and the larger preview. Thumbnails are real studio renders, one per
3
+ // theme, made by tools/render-start-thumbs.mjs into public/thumbs/. A template
4
+ // without a render (a starter added since the last run) keeps the plain tile
5
+ // its wrapper already paints.
6
+ import { useState } from "react";
7
+ import { useUiTheme } from "./theme.js";
8
+
9
+ export function TemplateThumb({ id }) {
10
+ const theme = useUiTheme();
11
+ const src = `/thumbs/${id}-${theme}.webp`;
12
+ const [failed, setFailed] = useState(null);
13
+ if (failed === src) return null;
14
+ return <img className="v2-start-art" src={src} alt="" aria-hidden="true" draggable="false" decoding="async" onError={() => setFailed(src)} />;
15
+ }
16
+
17
+ export function BrandMark() {
18
+ return <img className="v2-start-brand-mark" src="/icons/icon-192.png" alt="" aria-hidden="true" draggable="false" width="26" height="26" />;
19
+ }
@@ -102,11 +102,17 @@
102
102
  }
103
103
 
104
104
  .v2-start-screen .v2-start-brand-mark {
105
+ display: block;
106
+ box-sizing: border-box;
105
107
  width: var(--start-brand-mark, 26px);
106
108
  height: var(--start-brand-mark, 26px);
107
109
  flex: 0 0 var(--start-brand-mark, 26px);
108
- border-radius: var(--r-2, 6px);
109
- background: var(--select, #e8a33d);
110
+ padding: 1px;
111
+ border-radius: 7px;
112
+ /* The frog keeps its own blue on its own white tile in both themes. */
113
+ background: #fff;
114
+ box-shadow: 0 0 0 var(--start-border-width, 1px) color-mix(in srgb, var(--text-1, #e4e4e7) 10%, transparent);
115
+ object-fit: contain;
110
116
  }
111
117
 
112
118
  .v2-start-screen .v2-start-nav-list {
@@ -196,30 +202,7 @@
196
202
  color: var(--text-1, #e4e4e7);
197
203
  }
198
204
 
199
- .v2-start-screen .v2-start-tabs {
200
- display: flex;
201
- align-items: center;
202
- gap: var(--start-tab-gap, 20px);
203
- min-height: var(--start-tab-height, 24px);
204
- }
205
-
206
- .v2-start-screen .v2-start-tab {
207
- height: var(--start-tab-height, 24px);
208
- padding: 0 0 var(--start-tab-underline-space, 8px);
209
- border: 0;
210
- border-bottom: var(--start-active-width, 1.5px) solid var(--transparent, transparent);
211
- background: var(--transparent, transparent);
212
- color: var(--text-3, #636369);
213
- white-space: nowrap;
214
- }
215
-
216
- .v2-start-screen .v2-start-tab:hover,
217
- .v2-start-screen .v2-start-tab.active {
218
- border-bottom-color: var(--select, #e8a33d);
219
- color: var(--text-selected, #fff);
220
- }
221
-
222
- /* Project mode (previs flag): a two-up segmented switch above the tabs. Each
205
+ /* Project mode (previs flag): a two-up segmented switch above the cards. Each
223
206
  segment carries its label and the one-line promise of what the mode makes. */
224
207
  .v2-start-screen .v2-start-mode {
225
208
  display: grid;
@@ -291,69 +274,27 @@
291
274
  }
292
275
 
293
276
  .v2-start-screen .v2-start-template-art {
294
- position: relative;
277
+ display: block;
278
+ overflow: hidden;
295
279
  width: var(--start-template-art-width, 100%);
296
280
  aspect-ratio: 16 / 10;
297
- overflow: hidden;
298
281
  border-radius: var(--r-2, 6px);
282
+ outline: var(--start-border-width, 1px) solid color-mix(in srgb, var(--text-1, #e4e4e7) 9%, transparent);
283
+ outline-offset: -1px;
299
284
  background: var(--surface-raised, #1e1e21);
300
- box-shadow: inset 0 0 0 var(--start-border-width, 1px) var(--transparent, transparent);
301
- }
302
-
303
- .v2-start-screen .v2-start-template-art::before,
304
- .v2-start-screen .v2-start-template-art::after {
305
- content: "";
306
- position: absolute;
307
- display: block;
308
- background: var(--surface-active, #26262a);
309
- }
310
-
311
- .v2-start-screen .v2-start-template-art::before {
312
- left: var(--start-art-block-left, 12%);
313
- bottom: var(--start-art-block-bottom, 16%);
314
- width: var(--start-art-block-width, 36%);
315
- height: var(--start-art-block-height, 48%);
316
- }
317
-
318
- .v2-start-screen .v2-start-template-art::after {
319
- right: var(--start-art-block-right, 12%);
320
- bottom: var(--start-art-block-bottom, 16%);
321
- width: var(--start-art-small-width, 20%);
322
- height: var(--start-art-small-height, 24%);
323
- background: var(--field, #0f0f11);
324
- }
325
-
326
- .v2-start-screen .v2-start-template-art.sample::before {
327
- width: var(--start-sample-block-width, 54%);
328
- height: var(--start-sample-block-height, 28%);
329
- background: var(--primary, #7258a0);
330
- }
331
-
332
- .v2-start-screen .v2-start-template-art.sample::after {
333
- width: var(--start-sample-small-width, 10%);
334
- height: var(--start-sample-small-height, 52%);
335
- background: var(--camera, #a78bfa);
336
285
  }
337
286
 
338
287
  .v2-start-screen .v2-start-template.selected .v2-start-template-art {
339
- box-shadow: inset 0 0 0 var(--start-selected-width, 1.5px) var(--select, #e8a33d);
288
+ outline: var(--start-selected-width, 1.5px) solid var(--select, #e8a33d);
289
+ outline-offset: -1.5px;
340
290
  }
341
291
 
342
292
  .v2-start-screen .v2-start-template:hover .v2-start-template-art {
343
- box-shadow: inset 0 0 0 var(--start-hover-width, 1px) var(--text-3, #636369);
293
+ outline-color: var(--text-3, #636369);
344
294
  }
345
295
 
346
296
  .v2-start-screen .v2-start-template.selected:hover .v2-start-template-art {
347
- box-shadow: inset 0 0 0 var(--start-selected-width, 1.5px) var(--select, #e8a33d);
348
- }
349
-
350
- .v2-start-screen .v2-start-template-code {
351
- position: absolute;
352
- left: var(--sp-1, 8px);
353
- bottom: var(--sp-1, 8px);
354
- color: var(--text-off, #4f4f55);
355
- font-family: var(--mono, 'JetBrains Mono'), ui-monospace, monospace;
356
- font-size: var(--start-code-size, 10px);
297
+ outline-color: var(--select, #e8a33d);
357
298
  }
358
299
 
359
300
  .v2-start-screen .v2-start-template-copy {
@@ -382,6 +323,43 @@
382
323
  white-space: nowrap;
383
324
  }
384
325
 
326
+ /* Open-project card: same slot and caption as a template, dashed art. */
327
+ .v2-start-screen .v2-start-add-card {
328
+ min-width: var(--start-template-min-width, 0);
329
+ display: flex;
330
+ flex-direction: column;
331
+ gap: var(--sp-2, 12px);
332
+ padding: 0;
333
+ border: 0;
334
+ background: var(--transparent, transparent);
335
+ color: var(--text-1, #e4e4e7);
336
+ text-align: left;
337
+ }
338
+
339
+ .v2-start-screen .v2-start-add-art {
340
+ display: grid;
341
+ place-items: center;
342
+ width: var(--start-template-art-width, 100%);
343
+ aspect-ratio: 16 / 10;
344
+ border: var(--start-border-width, 1px) dashed var(--text-3, #636369);
345
+ border-radius: var(--r-2, 6px);
346
+ color: var(--text-3, #636369);
347
+ transition: border-color 120ms ease, color 120ms ease;
348
+ }
349
+
350
+ .v2-start-screen .v2-start-add-card:hover .v2-start-add-art,
351
+ .v2-start-screen .v2-start-add-card:focus-visible .v2-start-add-art {
352
+ border-color: var(--select, #e8a33d);
353
+ color: var(--select, #e8a33d);
354
+ }
355
+
356
+ .v2-start-screen .v2-start-add-card.selected .v2-start-add-art {
357
+ border-style: solid;
358
+ border-color: var(--select, #e8a33d);
359
+ box-shadow: inset 0 0 0 0.5px var(--select, #e8a33d);
360
+ color: var(--select, #e8a33d);
361
+ }
362
+
385
363
  /* --------------------------------------------------------------- recent --- */
386
364
 
387
365
  .v2-start-screen .v2-start-recent-block,
@@ -510,11 +488,6 @@
510
488
  padding: var(--sp-1, 8px) 0;
511
489
  }
512
490
 
513
- .v2-start-screen .v2-start-file-actions {
514
- margin-top: auto;
515
- padding-top: var(--sp-1, 8px);
516
- }
517
-
518
491
  .v2-start-screen .v2-start-learn {
519
492
  max-width: var(--start-learn-max-width, 520px);
520
493
  display: flex;
@@ -537,6 +510,11 @@
537
510
  line-height: 1.55;
538
511
  }
539
512
 
513
+ .v2-start-screen .v2-start-file-actions {
514
+ margin-top: auto;
515
+ padding-top: var(--sp-1, 8px);
516
+ }
517
+
540
518
  /* --------------------------------------------------------------- preview --- */
541
519
 
542
520
  .v2-start-screen .v2-start-preview {
@@ -550,51 +528,26 @@
550
528
  }
551
529
 
552
530
  .v2-start-screen .v2-start-preview-art {
553
- position: relative;
554
- width: var(--start-preview-art-width, 100%);
531
+ display: block;
532
+ flex: 0 0 auto;
533
+ /* A framed card inset to the copy column below it, not an edge-to-edge band. */
534
+ width: auto;
535
+ margin: var(--start-preview-copy-pad-x, 20px) var(--start-preview-copy-pad-x, 20px) 0;
555
536
  aspect-ratio: 16 / 9;
556
- display: flex;
557
- align-items: flex-end;
558
- justify-content: flex-start;
559
- padding: var(--sp-1, 8px);
537
+ overflow: hidden;
538
+ border-radius: var(--r-2, 6px);
539
+ outline: var(--start-border-width, 1px) solid color-mix(in srgb, var(--text-1, #e4e4e7) 9%, transparent);
540
+ outline-offset: -1px;
560
541
  background: var(--surface-raised, #1e1e21);
561
542
  }
562
543
 
563
- .v2-start-screen .v2-start-preview-art::before,
564
- .v2-start-screen .v2-start-preview-art::after {
565
- content: "";
566
- position: absolute;
544
+ /* Template art: a real studio render (public/thumbs, one per theme). The
545
+ wrapper paints a plain tile underneath for a template without one. */
546
+ .v2-start-screen .v2-start-art {
567
547
  display: block;
568
- background: var(--surface-active, #26262a);
569
- }
570
-
571
- .v2-start-screen .v2-start-preview-art::before {
572
- left: var(--preview-block-left, 14%);
573
- bottom: var(--preview-block-bottom, 16%);
574
- width: var(--preview-block-width, 34%);
575
- height: var(--preview-block-height, 50%);
576
- }
577
-
578
- .v2-start-screen .v2-start-preview-art::after {
579
- right: var(--preview-block-right, 14%);
580
- bottom: var(--preview-block-bottom, 16%);
581
- width: var(--preview-small-width, 18%);
582
- height: var(--preview-small-height, 28%);
583
- background: var(--primary, #7258a0);
584
- }
585
-
586
- .v2-start-screen .v2-start-preview-art.sample::before {
587
- width: var(--preview-sample-width, 58%);
588
- height: var(--preview-sample-height, 30%);
589
- background: var(--camera, #a78bfa);
590
- }
591
-
592
- .v2-start-screen .v2-start-preview-art span {
593
- position: relative;
594
- z-index: var(--start-art-label-layer, 1);
595
- color: var(--text-off, #4f4f55);
596
- font-family: var(--mono, 'JetBrains Mono'), ui-monospace, monospace;
597
- font-size: var(--start-preview-label-size, 10.5px);
548
+ width: 100%;
549
+ height: 100%;
550
+ object-fit: cover;
598
551
  }
599
552
 
600
553
  .v2-start-screen .v2-start-preview-copy {
@@ -14,21 +14,16 @@ import {
14
14
  storeProjectsDirectory,
15
15
  DEFAULT_PREVIS_MODE,
16
16
  } from "./project.js";
17
+ import { BrandMark, TemplateThumb } from "./project-browser-art.jsx";
17
18
  import "./project-browser.css";
18
19
 
19
20
  const APP_VERSION = packageInfo.version;
20
- const CATEGORIES = ["All", "Film", "Dialogue", "Action", "Vehicle"];
21
- const BLANK_TEMPLATE = Object.freeze({
22
- id: "blank-stage",
23
- name: "Blank Stage",
24
- category: "All",
25
- description: "An empty stage for blocking a new shot.",
26
- kind: "blank",
27
- code: "NEW",
28
- });
21
+ // The "+" card is the one blank entry: an empty stage under a fresh name (no
22
+ // starterId, so Create takes the blank path). Its preview is the blank-stage render.
23
+ const NEW_PROJECT = Object.freeze({ id: "new-project", thumbId: "blank-stage", kind: "blank", defaultName: "Untitled Project", get name() { return ko("New project", "새 프로젝트"); }, get description() { return ko("Start from an empty stage.", "빈 무대에서 시작합니다."); } });
29
24
 
30
25
  // The two project modes the New view offers.
31
- // Storyboard starts from an empty stage only; Animation keeps every template.
26
+ // Storyboard starts from an empty stage only; Animation also offers the starters.
32
27
  const MODE_OPTIONS = [
33
28
  {
34
29
  id: "storyboard",
@@ -46,6 +41,9 @@ const MODE_OPTIONS = [
46
41
  // path. The v2 chooser already has the name, so this short-lived handoff seeds
47
42
  // the existing ProjectNameDialog and lets its normal submit path commit it.
48
43
  let pendingProjectName = null;
44
+ function stagePendingProjectName(name) {
45
+ pendingProjectName = name;
46
+ }
49
47
 
50
48
  function formatDate(value) {
51
49
  const date = new Date(value);
@@ -58,10 +56,8 @@ function sampleTemplate(scene) {
58
56
  id: `sample-${scene.id}`,
59
57
  starterId: scene.id,
60
58
  name: scene.name,
61
- category: "Action",
62
59
  description: scene.blurb,
63
60
  kind: "sample",
64
- code: "SAMPLE",
65
61
  };
66
62
  }
67
63
 
@@ -103,20 +99,17 @@ export function ProjectNameDialog({ open, initialName = "My Project", onCancel,
103
99
 
104
100
  /**
105
101
  * v2 start / project screen. Recent projects still come from the browser-local
106
- * project store, while today's starter scenes become the template cards.
102
+ * project store, while the bundled starter scenes (none today) become template
103
+ * cards before the "+" card.
107
104
  */
108
105
  export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew, onClose, startup = false, starters = [], onStarter }) {
109
106
  const starterTemplates = useMemo(() => starters.map(sampleTemplate), [starters]);
110
107
  const [previsMode, setPrevisMode] = useState(DEFAULT_PREVIS_MODE);
111
- const templates = useMemo(
112
- () => (previsMode === "storyboard" ? [BLANK_TEMPLATE] : [BLANK_TEMPLATE, ...starterTemplates]),
113
- [previsMode, starterTemplates],
114
- );
115
- const defaultTemplate = starterTemplates[0] ?? BLANK_TEMPLATE;
108
+ const templates = previsMode === "storyboard" ? [] : starterTemplates;
109
+ const defaultTemplate = starterTemplates[0] ?? NEW_PROJECT;
116
110
  const [activeNav, setActiveNav] = useState(startup ? "new" : "recent");
117
- const [category, setCategory] = useState("All");
118
111
  const [selectedTemplateId, setSelectedTemplateId] = useState(defaultTemplate.id);
119
- const [name, setName] = useState(currentName || defaultTemplate.name || "My Project");
112
+ const [name, setName] = useState(currentName || defaultTemplate.defaultName || defaultTemplate.name || "My Project");
120
113
  const [frameRate, setFrameRate] = useState("24");
121
114
  const [units, setUnits] = useState("Meters");
122
115
  const [recents, setRecents] = useState([]);
@@ -156,15 +149,14 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
156
149
 
157
150
  // File › New reaches this screen with unsaved work, so Create can be refused
158
151
  // by the discard confirmation. A name left behind then must not seed a later
159
- // Save's name dialog once the screen closes.
152
+ // Save's name dialog once the screen closes. The clear waits for the end of
153
+ // the commit: a Create that closes this screen opens the name dialog in the
154
+ // same commit, and its effect takes the name first.
160
155
  useEffect(() => () => {
161
- pendingProjectName = null;
156
+ queueMicrotask(() => stagePendingProjectName(null));
162
157
  }, []);
163
158
 
164
- const selectedTemplate = templates.find((template) => template.id === selectedTemplateId) ?? templates[0];
165
- const visibleTemplates = category === "All"
166
- ? templates
167
- : templates.filter((template) => template.category === "All" || template.category === category);
159
+ const selectedTemplate = templates.find((template) => template.id === selectedTemplateId) ?? NEW_PROJECT;
168
160
  const canCreate = name.trim().length > 0;
169
161
  const createReason = ko("Enter a project name to create a project", "프로젝트를 만들려면 이름을 입력하세요");
170
162
  const locationLabel = folder?.name || ko("Browser project store", "브라우저 프로젝트 저장소");
@@ -201,17 +193,17 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
201
193
  const selectTemplate = (template) => {
202
194
  setSelectedTemplateId(template.id);
203
195
  // A name the previous card (or the open project) filled in follows the new card; a typed one stays.
204
- setName((current) => !current.trim() || current.trim() === selectedTemplate?.name || current.trim() === currentName ? template.name : current);
196
+ setName((current) => !current.trim() || [selectedTemplate?.name, selectedTemplate?.defaultName, currentName].includes(current.trim()) ? template.defaultName ?? template.name : current);
205
197
  };
206
198
 
207
199
  const choosePrevisMode = (mode) => {
208
200
  setPrevisMode(mode);
209
201
  // Storyboard offers the blank stage only, so a starter selection cannot
210
202
  // survive the switch.
211
- if (mode !== "storyboard" || selectedTemplate?.id === BLANK_TEMPLATE.id) return;
212
- setSelectedTemplateId(BLANK_TEMPLATE.id);
203
+ if (mode !== "storyboard" || !selectedTemplate?.starterId) return;
204
+ setSelectedTemplateId(NEW_PROJECT.id);
213
205
  // A name the starter card filled in would now describe the wrong stage.
214
- setName((current) => (current.trim() === selectedTemplate?.name ? BLANK_TEMPLATE.name : current));
206
+ setName((current) => (current.trim() === selectedTemplate?.name ? NEW_PROJECT.defaultName : current));
215
207
  };
216
208
 
217
209
  const createProject = (event) => {
@@ -222,7 +214,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
222
214
  onStarter?.(selectedTemplate.starterId, projectName, { previsMode: "animation" });
223
215
  return;
224
216
  }
225
- pendingProjectName = projectName;
217
+ stagePendingProjectName(projectName);
226
218
  onNew(projectName, { previsMode });
227
219
  };
228
220
 
@@ -234,7 +226,6 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
234
226
  const navItems = [
235
227
  { id: "recent", label: ko("Recent", "최근") },
236
228
  { id: "new", label: ko("New Project", "새 프로젝트") },
237
- { id: "samples", label: ko("Samples", "샘플") },
238
229
  { id: "learn", label: ko("Learn", "배우기") },
239
230
  ];
240
231
 
@@ -243,7 +234,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
243
234
  <div className={`v2-start-window project-browser${startup ? " startup" : ""}`} role="dialog" aria-modal="true" aria-label={ko("Choose project", "프로젝트 선택")}>
244
235
  <nav className="v2-start-nav" aria-label={ko("Project navigation", "프로젝트 탐색")}>
245
236
  <div className="v2-start-brand">
246
- <span className="v2-start-brand-mark" aria-hidden="true" />
237
+ <BrandMark />
247
238
  <strong>CozyClay</strong>
248
239
  </div>
249
240
  <div className="v2-start-nav-list">
@@ -265,7 +256,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
265
256
  <main className="v2-start-main">
266
257
  <div className="v2-start-main-head">
267
258
  <div className="v2-start-title-row">
268
- <h1>{activeNav === "recent" ? ko("Recent", "최근") : activeNav === "samples" ? ko("Samples", "샘플") : activeNav === "learn" ? ko("Learn", "배우기") : ko("New Project", "새 프로젝트")}</h1>
259
+ <h1>{activeNav === "recent" ? ko("Recent", "최근") : activeNav === "learn" ? ko("Learn", "배우기") : ko("New Project", "새 프로젝트")}</h1>
269
260
  {activeNav !== "new" && <button type="button" className="v2-start-close x" onClick={onClose} aria-label={ko("Close", "닫기")}>×</button>}
270
261
  </div>
271
262
  {activeNav === "new" && (
@@ -286,22 +277,6 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
286
277
  ))}
287
278
  </div>
288
279
  )}
289
- {(activeNav === "new" || activeNav === "samples") && (
290
- <div className="v2-start-tabs" role="tablist" aria-label={ko("Template categories", "템플릿 카테고리")}>
291
- {CATEGORIES.map((entry) => (
292
- <button
293
- type="button"
294
- key={entry}
295
- role="tab"
296
- aria-selected={category === entry}
297
- className={`v2-start-tab${category === entry ? " active" : ""}`}
298
- onClick={() => setCategory(entry)}
299
- >
300
- {ko(entry, entry === "All" ? "전체" : entry === "Film" ? "영화" : entry === "Dialogue" ? "대화" : entry === "Action" ? "액션" : "차량")}
301
- </button>
302
- ))}
303
- </div>
304
- )}
305
280
  </div>
306
281
 
307
282
  {activeNav === "learn" ? (
@@ -327,7 +302,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
327
302
  ) : (
328
303
  <>
329
304
  <div className="v2-start-template-grid" role="list" aria-label={ko("Project templates", "프로젝트 템플릿")}>
330
- {visibleTemplates.map((template) => (
305
+ {templates.map((template) => (
331
306
  <button
332
307
  type="button"
333
308
  role="listitem"
@@ -337,15 +312,20 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
337
312
  onClick={() => selectTemplate(template)}
338
313
  onDoubleClick={() => template.starterId && onStarter?.(template.starterId)}
339
314
  >
340
- <span className={`v2-start-template-art ${template.kind}`} aria-hidden="true">
341
- <span className="v2-start-template-code">{template.code}</span>
342
- </span>
315
+ <span className={`v2-start-template-art ${template.kind}`}><TemplateThumb id={template.starterId ?? template.id} /></span>
343
316
  <span className="v2-start-template-copy">
344
317
  <strong>{template.name}</strong>
345
318
  <span>{template.description}</span>
346
319
  </span>
347
320
  </button>
348
321
  ))}
322
+ <button type="button" role="listitem" className={`v2-start-add-card${selectedTemplate?.id === NEW_PROJECT.id ? " selected" : ""}`} data-testid="start-new-project-card" onClick={() => { selectTemplate(NEW_PROJECT); requestAnimationFrame(() => nameInputRef.current?.select()); }}>
323
+ <span className="v2-start-add-art" aria-hidden="true"><svg viewBox="0 0 20 20" width="20" height="20"><path d="M10 3.5v13M3.5 10h13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /></svg></span>
324
+ <span className="v2-start-template-copy">
325
+ <strong>{ko("New project", "새 프로젝트")}</strong>
326
+ <span>{ko("Start from an empty stage", "빈 무대에서 시작")}</span>
327
+ </span>
328
+ </button>
349
329
  </div>
350
330
 
351
331
  <div className="v2-start-recent-block">
@@ -357,15 +337,11 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
357
337
  </div>
358
338
  </>
359
339
  )}
360
- {activeNav !== "recent" && activeNav !== "learn" && (
361
- <div className="v2-start-file-actions">
362
- <button type="button" className="v2-start-file-button" onClick={onOpenFile}>{ko("Open project file…", "프로젝트 파일 열기…")}</button>
363
- </div>
364
- )}
340
+ {activeNav !== "recent" && activeNav !== "learn" && <div className="v2-start-file-actions"><button type="button" className="v2-start-file-button" onClick={onOpenFile}>{ko("Open project file…", "프로젝트 파일 열기…")}</button></div>}
365
341
  </main>
366
342
 
367
343
  <aside className="v2-start-preview" data-testid="start-project-preview" aria-label={ko("Project preview", "프로젝트 미리보기")}>
368
- <div className={`v2-start-preview-art ${selectedTemplate?.kind || "blank"}`} aria-hidden="true" />
344
+ <div className={`v2-start-preview-art ${selectedTemplate?.kind || "blank"}`}><TemplateThumb id={selectedTemplate.starterId ?? selectedTemplate.thumbId} /></div>
369
345
  <div className="v2-start-preview-copy">
370
346
  <strong>{selectedTemplate?.name}</strong>
371
347
  <span>{selectedTemplate?.description}</span>
package/src/props.jsx CHANGED
@@ -2,11 +2,11 @@
2
2
  * Set props: low-poly clay maquettes to block against.
3
3
  *
4
4
  * The set is a bare room and a figure; a shot that needs "something in the
5
- * world" (a car to lean on, to walk past, to frame behind glass) has nothing
5
+ * world" (a wall to lean on, to walk past, to frame behind) has nothing
6
6
  * to work with. These props are built from primitives in the same clay style
7
7
  * as the Room and the character tint, so the whole frame stays one
8
- * consistent maquette. The hand-built set pieces live in set-pieces.jsx;
9
- * practical lights and glow in scene-lights.jsx and object-glow.js.
8
+ * consistent maquette. Practical lights and glow live in scene-lights.jsx
9
+ * and object-glow.js.
10
10
  */
11
11
 
12
12
  import { useEffect, useMemo, useRef, useState } from "react";
@@ -19,40 +19,35 @@ import { CUTOUT_KIND, MESH_KIND, EMPTY_KIND, EMPTY_MARKER_SIZE, SCULPT_KIND } fr
19
19
  import { TEXT_KIND } from "./text/text-record.js";
20
20
  import { TextObject } from "./text/text-object.jsx";
21
21
  import { buildSculptGroup, disposeSculptGroup } from "./sculpt-geometry.js";
22
+ import { depthRankProps, usePrimitiveLook } from "./primitive-look.js";
22
23
  import { subscribeToAssetTexture } from "./scene-asset-cache.js";
23
24
  import { ImportedMesh } from "./imported-mesh.jsx";
24
25
  import { EffectMarker, EffectsLayer } from "./effects/EffectsLayer.jsx";
25
26
  import { EFFECT_KIND } from "./effects/effect-record.js";
26
27
  import { applyPoseFade } from "./pose-fade.js";
27
28
  import { coplanarDepthRanks } from "./coplanar-depth.js";
28
- import { autoFlat, Car, Chair, SmallPlane } from "./set-pieces.jsx";
29
+ import { autoFlat } from "./auto-flat.js";
29
30
  import { LIGHT_KIND, capPracticalShadows } from "./object-look.js";
30
31
  import { SceneLight, useLookRefresh } from "./scene-lights.jsx";
31
32
  import { placeObjectLook } from "./scene-light-place.js";
32
33
 
33
- // Parts of an assembly can share a plane with an earlier part (hood top and
34
- // grille top). The depth test can't order them and the winner flips as the
35
- // camera moves, so a ranked part is pulled toward the camera by `rank` steps
36
- // (negative = nearer). Rank 0 gets nothing: those materials stay untouched.
37
- function depthRankProps(rank) {
38
- return rank > 0 ? { polygonOffset: true, polygonOffsetFactor: -rank, polygonOffsetUnits: -rank } : null;
39
- }
40
-
41
34
  /**
42
35
  * The creatable primitives, Unity's 3D Object menu in clay. Each one is built
43
36
  * with its base on the local floor (y = 0), so an object's `y` reads as height
44
- * above the deck rather than "half of me is underground".
37
+ * above the deck rather than "half of me is underground". A surface preset
38
+ * (primitive-look.js) replaces the clay; glass casts no shadow.
45
39
  */
46
- function Primitive({ kind, color, autoColor, depthRank = 0 }) {
40
+ function Primitive({ kind, object, color, autoColor, depthRank = 0 }) {
47
41
  const side = kind === "plane" ? THREE.DoubleSide : THREE.FrontSide;
48
- const material = autoColor ? (
42
+ const look = usePrimitiveLook(object, kind, side, depthRank), cast = !(look?.transmission > 0);
43
+ const material = look ? <primitive object={look} attach="material" /> : autoColor ? (
49
44
  <meshStandardMaterial {...autoFlat(autoColor)} side={side} {...depthRankProps(depthRank)} />
50
45
  ) : (
51
46
  <meshStandardMaterial color={color} roughness={0.82} side={side} {...depthRankProps(depthRank)} />
52
47
  );
53
48
  if (kind === "sphere") {
54
49
  return (
55
- <mesh position={[0, 0.5, 0]} castShadow receiveShadow>
50
+ <mesh position={[0, 0.5, 0]} castShadow={cast} receiveShadow>
56
51
  <sphereGeometry args={[0.5, 28, 18]} />
57
52
  {material}
58
53
  </mesh>
@@ -60,7 +55,7 @@ function Primitive({ kind, color, autoColor, depthRank = 0 }) {
60
55
  }
61
56
  if (kind === "capsule") {
62
57
  return (
63
- <mesh position={[0, 0.7, 0]} castShadow receiveShadow>
58
+ <mesh position={[0, 0.7, 0]} castShadow={cast} receiveShadow>
64
59
  <capsuleGeometry args={[0.35, 0.7, 6, 18]} />
65
60
  {material}
66
61
  </mesh>
@@ -68,7 +63,7 @@ function Primitive({ kind, color, autoColor, depthRank = 0 }) {
68
63
  }
69
64
  if (kind === "cylinder") {
70
65
  return (
71
- <mesh position={[0, 0.5, 0]} castShadow receiveShadow>
66
+ <mesh position={[0, 0.5, 0]} castShadow={cast} receiveShadow>
72
67
  <cylinderGeometry args={[0.5, 0.5, 1, 26]} />
73
68
  {material}
74
69
  </mesh>
@@ -76,7 +71,7 @@ function Primitive({ kind, color, autoColor, depthRank = 0 }) {
76
71
  }
77
72
  if (kind === "cone") {
78
73
  return (
79
- <mesh position={[0, 0.5, 0]} castShadow receiveShadow>
74
+ <mesh position={[0, 0.5, 0]} castShadow={cast} receiveShadow>
80
75
  <coneGeometry args={[0.5, 1, 26]} />
81
76
  {material}
82
77
  </mesh>
@@ -91,7 +86,7 @@ function Primitive({ kind, color, autoColor, depthRank = 0 }) {
91
86
  );
92
87
  }
93
88
  return (
94
- <mesh position={[0, 0.5, 0]} castShadow receiveShadow>
89
+ <mesh position={[0, 0.5, 0]} castShadow={cast} receiveShadow>
95
90
  <boxGeometry args={[1, 1, 1]} />
96
91
  {material}
97
92
  </mesh>
@@ -162,20 +157,17 @@ function Cutout({ object }) {
162
157
 
163
158
  /**
164
159
  * A sculpt: the clay parts its recipe describes, rebuilt only when the recipe
165
- * or the look changes. Each part keeps its own clay colour; auto-colour paints
166
- * the whole sculpt flat like any other prop, because it is one object.
160
+ * or the look changes. The shaded look is buildSculptGroup's own clay (part
161
+ * colours, gradients, scatter tints); auto-colour paints the whole sculpt flat
162
+ * like any other prop, because it is one object. Depth rank rides on either.
167
163
  */
168
164
  function Sculpt({ object, depthRank = 0 }) {
169
165
  const { recipe, autoColor } = object;
170
- const group = useMemo(
171
- () => buildSculptGroup(recipe, {
172
- materialFor: (part) => new THREE.MeshStandardMaterial({
173
- ...(autoColor ? autoFlat(autoColor) : { color: part.color, roughness: 0.82, metalness: 0 }),
174
- ...depthRankProps(depthRank),
175
- }),
176
- }),
177
- [recipe, autoColor, depthRank],
178
- );
166
+ const group = useMemo(() => {
167
+ const built = buildSculptGroup(recipe, autoColor ? { materialFor: () => new THREE.MeshStandardMaterial(autoFlat(autoColor)) } : {});
168
+ built.traverse((node) => node.material && Object.assign(node.material, depthRankProps(depthRank)));
169
+ return built;
170
+ }, [recipe, autoColor, depthRank]);
179
171
  useEffect(() => () => disposeSculptGroup(group), [group]);
180
172
  return <primitive object={group} />;
181
173
  }
@@ -242,10 +234,7 @@ function SceneObjectContent({ object, depthRank = 0 }) {
242
234
  if (renderer === MESH_KIND) return <ImportedMesh object={object} />;
243
235
  if (renderer === SCULPT_KIND) return <Sculpt object={object} depthRank={depthRank} />;
244
236
  if (renderer === TEXT_KIND) return <TextObject object={object} />;
245
- if (renderer === "car") return <Car color={autoColor ?? color} autoColor={autoColor} />;
246
- if (renderer === "small-plane") return <SmallPlane autoColor={autoColor} />;
247
- if (renderer === "chair") return <Chair autoColor={autoColor} />;
248
- if (PRIMITIVE_KINDS.has(renderer)) return <Primitive kind={renderer} color={color} autoColor={autoColor} depthRank={depthRank} />;
237
+ if (PRIMITIVE_KINDS.has(renderer)) return <Primitive kind={renderer} object={object} color={color} autoColor={autoColor} depthRank={depthRank} />;
249
238
  return null;
250
239
  }
251
240
 
@@ -47,10 +47,16 @@ export function passFileName(kind) {
47
47
  // it spreads the value over four channels and reads as colour noise). This
48
48
  // shader is the plain depth-to-grey the deliverable wants, and the range it
49
49
  // normalises over is the stage's, not the projection's.
50
+ // An InstancedMesh (a sculpt's scatter) draws every instance through its
51
+ // instanceMatrix; without it all of them would land on the mesh's origin.
50
52
  const DEPTH_VERTEX = /* glsl */ `
51
53
  varying float vViewDepth;
52
54
  void main() {
53
- vec4 viewPosition = modelViewMatrix * vec4(position, 1.0);
55
+ vec4 local = vec4(position, 1.0);
56
+ #ifdef USE_INSTANCING
57
+ local = instanceMatrix * local;
58
+ #endif
59
+ vec4 viewPosition = modelViewMatrix * local;
54
60
  vViewDepth = -viewPosition.z;
55
61
  gl_Position = projectionMatrix * viewPosition;
56
62
  }