@@ -11,6 +11,7 @@ import { buildProjectPickerModel, type ProjectPickerFilter } from './projectLibr
1111import { exportYamlToDisk } from './yamlFileExport' ;
1212import { getOpenFilePicker , readFileHandleText } from './yamlFileHandles' ;
1313import { parseProjectYaml , serializeProjectToYaml } from '../model/serialization' ;
14+ import { deriveWorldUnitsFromNaturalPixels , getProjectPixelsPerUnit , normalizeProjectPixelsPerUnit } from '../model/projectPixelScale' ;
1415import { EventBus } from '../phaser/EventBus' ;
1516import {
1617 buildProjectHistoryViewModel ,
@@ -278,6 +279,8 @@ export function EntityListView({
278279 } | null > ( null ) ;
279280 const duplicateDialogRootRef = useRef < HTMLDivElement | null > ( null ) ;
280281 const [ copyRevisionName , setCopyRevisionName ] = useState ( '' ) ;
282+ const [ projectSettingsDialogOpen , setProjectSettingsDialogOpen ] = useState ( false ) ;
283+ const [ projectPixelsPerUnitDraft , setProjectPixelsPerUnitDraft ] = useState ( ( ) => String ( getProjectPixelsPerUnit ( project ) ) ) ;
281284 const [ expandedRevisionId , setExpandedRevisionId ] = useState < string | null > ( null ) ;
282285 const [ historyPaneMode , setHistoryPaneMode ] = useState < 'active' | 'archived' > ( 'active' ) ;
283286 const [ historySelectionMode , setHistorySelectionMode ] = useState < 'none' | 'archive' | 'delete' > ( 'none' ) ;
@@ -312,6 +315,11 @@ export function EntityListView({
312315 setCopyRevisionName ( buildCopyRevisionDefaultName ( project . title , revision ) ) ;
313316 } , [ archivedRevisions , project . title , revisionDialogs . copyRevisionId , revisions ] ) ;
314317
318+ useEffect ( ( ) => {
319+ if ( ! projectSettingsDialogOpen ) return ;
320+ setProjectPixelsPerUnitDraft ( String ( getProjectPixelsPerUnit ( project ) ) ) ;
321+ } , [ project , projectSettingsDialogOpen ] ) ;
322+
315323 useEffect ( ( ) => {
316324 const previousSidebarScope = previousSidebarScopeRef . current ;
317325 if ( normalizedSidebarScope === 'projectRevisions' && previousSidebarScope !== 'projectRevisions' ) {
@@ -347,6 +355,12 @@ export function EntityListView({
347355 setSelectedHistoryRevisionIds ( [ ] ) ;
348356 } ;
349357
358+ const saveProjectSettings = ( ) => {
359+ const pixelsPerUnit = normalizeProjectPixelsPerUnit ( Number ( projectPixelsPerUnitDraft ) ) ;
360+ dispatch ( { type : 'set-project-metadata' , pixelsPerUnit } ) ;
361+ setProjectSettingsDialogOpen ( false ) ;
362+ } ;
363+
350364 useEffect ( ( ) => {
351365 if ( ! menuOpen ) return ;
352366
@@ -1843,6 +1857,79 @@ export function EntityListView({
18431857 </ div >
18441858 ) : null }
18451859
1860+ { projectSettingsDialogOpen ? (
1861+ < div
1862+ className = "scene-graph-menu"
1863+ style = { { position : 'fixed' , left : '50%' , top : '20%' , transform : 'translateX(-50%)' , zIndex : 60 , minWidth : 420 } }
1864+ data-testid = "project-settings-dialog"
1865+ role = "dialog"
1866+ aria-label = "Project settings"
1867+ >
1868+ < div className = "scene-graph-menu-hint" > Project Settings</ div >
1869+ < div style = { { padding : '0.75rem' , display : 'grid' , gap : 10 } } >
1870+ < label className = "field" >
1871+ < span > Pixels Per Unit</ span >
1872+ < input
1873+ className = "text-input"
1874+ aria-label = "Pixels Per Unit"
1875+ data-testid = "project-settings-pixels-per-unit-input"
1876+ type = "text"
1877+ inputMode = "numeric"
1878+ value = { projectPixelsPerUnitDraft }
1879+ onChange = { ( event ) => setProjectPixelsPerUnitDraft ( event . target . value ) }
1880+ onKeyDown = { ( event ) => {
1881+ if ( event . key === 'Enter' ) {
1882+ event . preventDefault ( ) ;
1883+ saveProjectSettings ( ) ;
1884+ }
1885+ if ( event . key === 'Escape' ) {
1886+ event . preventDefault ( ) ;
1887+ setProjectSettingsDialogOpen ( false ) ;
1888+ }
1889+ } }
1890+ />
1891+ </ label >
1892+ < div className = "inspector-grid-3" >
1893+ { [ 1 , 2 , 4 ] . map ( ( value ) => {
1894+ const normalizedValue = normalizeProjectPixelsPerUnit ( Number ( projectPixelsPerUnitDraft ) ) ;
1895+ return (
1896+ < button
1897+ key = { value }
1898+ type = "button"
1899+ className = { `button button-compact ${ normalizedValue === value ? 'active' : '' } ` }
1900+ data-testid = { `project-settings-preset-${ value } ` }
1901+ onClick = { ( ) => setProjectPixelsPerUnitDraft ( String ( value ) ) }
1902+ >
1903+ { value }
1904+ </ button >
1905+ ) ;
1906+ } ) }
1907+ </ div >
1908+ < div className = "muted" >
1909+ 64px art becomes { deriveWorldUnitsFromNaturalPixels ( 64 , Number ( projectPixelsPerUnitDraft ) || 1 ) } world units at { normalizeProjectPixelsPerUnit ( Number ( projectPixelsPerUnitDraft ) || 1 ) } px/unit.
1910+ </ div >
1911+ </ div >
1912+ < div style = { { display : 'flex' , gap : 8 , justifyContent : 'flex-end' , padding : '0.75rem' } } >
1913+ < button
1914+ type = "button"
1915+ className = "button"
1916+ data-testid = "project-settings-cancel"
1917+ onClick = { ( ) => setProjectSettingsDialogOpen ( false ) }
1918+ >
1919+ Cancel
1920+ </ button >
1921+ < button
1922+ type = "button"
1923+ className = "button"
1924+ data-testid = "project-settings-save"
1925+ onClick = { saveProjectSettings }
1926+ >
1927+ Save
1928+ </ button >
1929+ </ div >
1930+ </ div >
1931+ ) : null }
1932+
18461933 { duplicateDialog ? (
18471934 < div
18481935 ref = { duplicateDialogRootRef }
@@ -2049,6 +2136,18 @@ export function EntityListView({
20492136 >
20502137 Export as YAML
20512138 </ button >
2139+ < button
2140+ type = "button"
2141+ className = "scene-graph-menu-item"
2142+ data-testid = "project-manage-settings"
2143+ onClick = { ( ) => {
2144+ setMenuOpen ( null ) ;
2145+ setProjectPixelsPerUnitDraft ( String ( getProjectPixelsPerUnit ( project ) ) ) ;
2146+ setProjectSettingsDialogOpen ( true ) ;
2147+ } }
2148+ >
2149+ Project Settings...
2150+ </ button >
20522151 < div className = "scene-graph-menu-divider" />
20532152 < button
20542153 type = "button"
0 commit comments