@@ -4266,6 +4266,313 @@ class PhraseMaker {
42664266 }
42674267 }
42684268 }
4269+
4270+ this . _updateMeasureBoundaries ( ) ;
4271+ }
4272+
4273+ /**
4274+ * Updates visual barlines based on the active meter so measure starts are easy to spot.
4275+ * @private
4276+ */
4277+ _updateMeasureBoundaries ( ) {
4278+ const columnCount = this . _getColumnCount ( ) ;
4279+ if ( columnCount === 0 ) {
4280+ return ;
4281+ }
4282+
4283+ const meterInfo = this . _getMeterInfo ( ) ;
4284+ if ( ! meterInfo || ! meterInfo . beatUnit || meterInfo . beatUnit <= 0 ) {
4285+ return ;
4286+ }
4287+
4288+ const beatsPerMeasure = meterInfo . beatsPerMeasure ;
4289+ const beatUnit = meterInfo . beatUnit ;
4290+ const EPSILON = 1e-6 ;
4291+ let beatAccumulator = 0 ;
4292+ const measureBoundaries = new Set ( [ 0 ] ) ;
4293+
4294+ for ( let column = 0 ; column < columnCount ; column ++ ) {
4295+ const duration = this . _getColumnDuration ( column ) ;
4296+ if ( duration === null ) {
4297+ continue ;
4298+ }
4299+
4300+ beatAccumulator += duration / beatUnit ;
4301+
4302+ if ( beatAccumulator > beatsPerMeasure - EPSILON ) {
4303+ while ( beatAccumulator >= beatsPerMeasure - EPSILON ) {
4304+ measureBoundaries . add ( column + 1 ) ;
4305+ beatAccumulator -= beatsPerMeasure ;
4306+ }
4307+ }
4308+
4309+ if ( Math . abs ( beatAccumulator ) < EPSILON ) {
4310+ beatAccumulator = 0 ;
4311+ }
4312+ }
4313+
4314+ measureBoundaries . delete ( columnCount ) ;
4315+
4316+ const measureBorder = `2px solid ${ this . _resolveBarlineColor ( ) } ` ;
4317+
4318+ const applyBorder = ( row ) => {
4319+ if ( ! row || ! row . cells ) {
4320+ return ;
4321+ }
4322+
4323+ let columnIndex = 0 ;
4324+ for ( let cellIdx = 0 ; cellIdx < row . cells . length ; cellIdx ++ ) {
4325+ const cell = row . cells [ cellIdx ] ;
4326+ if ( ! cell ) {
4327+ columnIndex += 1 ;
4328+ continue ;
4329+ }
4330+
4331+ const span = cell . colSpan || 1 ;
4332+
4333+ cell . style . borderLeft = "" ;
4334+
4335+ if ( measureBoundaries . has ( columnIndex ) ) {
4336+ cell . style . borderLeft = measureBorder ;
4337+ }
4338+
4339+ columnIndex += span ;
4340+ }
4341+ } ;
4342+
4343+ this . _rows . forEach ( applyBorder ) ;
4344+ applyBorder ( this . _noteValueRow ) ;
4345+ applyBorder ( this . _tupletNoteValueRow ) ;
4346+ applyBorder ( this . _tupletValueRow ) ;
4347+
4348+ if ( this . lyricsON ) {
4349+ const lyricsRow = docById ( "lyricRow" ) ;
4350+ if ( lyricsRow ) {
4351+ const nestedTable = lyricsRow . querySelector ( "table" ) ;
4352+ if ( nestedTable && nestedTable . rows . length > 0 ) {
4353+ applyBorder ( nestedTable . rows [ 0 ] ) ;
4354+ }
4355+ }
4356+ }
4357+ }
4358+
4359+ /**
4360+ * Returns the number of rhythm columns currently displayed in the grid.
4361+ * @returns {number }
4362+ * @private
4363+ */
4364+ _getColumnCount ( ) {
4365+ for ( let i = 0 ; i < this . _rows . length ; i ++ ) {
4366+ const row = this . _rows [ i ] ;
4367+ if ( row && row . cells && row . cells . length > 0 ) {
4368+ return row . cells . length ;
4369+ }
4370+ }
4371+
4372+ if ( this . _noteValueRow && this . _noteValueRow . cells ) {
4373+ let total = 0 ;
4374+ for ( let i = 0 ; i < this . _noteValueRow . cells . length ; i ++ ) {
4375+ total += this . _noteValueRow . cells [ i ] . colSpan || 1 ;
4376+ }
4377+ return total ;
4378+ }
4379+
4380+ return 0 ;
4381+ }
4382+
4383+ /**
4384+ * Retrieves the duration of a column, expressed as a fraction of a whole note.
4385+ * @param {number } index - Column index.
4386+ * @returns {?number }
4387+ * @private
4388+ */
4389+ _getColumnDuration ( index ) {
4390+ for ( let i = 0 ; i < this . _rows . length ; i ++ ) {
4391+ const row = this . _rows [ i ] ;
4392+ if ( ! row || ! row . cells || row . cells . length <= index ) {
4393+ continue ;
4394+ }
4395+
4396+ const duration = this . _parseDurationFromCell ( row . cells [ index ] , false ) ;
4397+ if ( duration !== null ) {
4398+ return duration ;
4399+ }
4400+ }
4401+
4402+ if ( this . _noteValueRow && this . _noteValueRow . cells ) {
4403+ let columnPointer = 0 ;
4404+ for ( let cellIdx = 0 ; cellIdx < this . _noteValueRow . cells . length ; cellIdx ++ ) {
4405+ const cell = this . _noteValueRow . cells [ cellIdx ] ;
4406+ const span = cell . colSpan || 1 ;
4407+
4408+ if ( index >= columnPointer && index < columnPointer + span ) {
4409+ const duration = this . _parseDurationFromCell ( cell , true ) ;
4410+ if ( duration !== null ) {
4411+ return duration ;
4412+ }
4413+ break ;
4414+ }
4415+
4416+ columnPointer += span ;
4417+ }
4418+ }
4419+
4420+ return null ;
4421+ }
4422+
4423+ /**
4424+ * Parses a duration value from a cell element.
4425+ * @param {HTMLElement } cell - The cell to inspect.
4426+ * @param {boolean } fromNoteValueRow - Whether the cell comes from the note value row.
4427+ * @returns {?number }
4428+ * @private
4429+ */
4430+ _parseDurationFromCell ( cell , fromNoteValueRow ) {
4431+ if ( ! cell ) {
4432+ return null ;
4433+ }
4434+
4435+ const alt = cell . getAttribute ( "alt" ) ;
4436+ if ( alt !== null ) {
4437+ let parsed = parseFloat ( alt ) ;
4438+ if ( ! Number . isNaN ( parsed ) && parsed > 0 ) {
4439+ if ( fromNoteValueRow && parsed >= 1 ) {
4440+ parsed = 1 / parsed ;
4441+ }
4442+ if ( parsed > 0 ) {
4443+ return parsed ;
4444+ }
4445+ }
4446+
4447+ if ( typeof alt === "string" && alt . includes ( "/" ) ) {
4448+ const parts = alt . split ( "/" ) ;
4449+ if ( parts . length === 2 ) {
4450+ const numerator = parseFloat ( parts [ 0 ] ) ;
4451+ const denominator = parseFloat ( parts [ 1 ] ) ;
4452+ if (
4453+ ! Number . isNaN ( numerator ) &&
4454+ ! Number . isNaN ( denominator ) &&
4455+ denominator !== 0
4456+ ) {
4457+ return numerator / denominator ;
4458+ }
4459+ }
4460+ }
4461+ }
4462+
4463+ const id = cell . getAttribute ( "id" ) ;
4464+ if ( id !== null ) {
4465+ let parsed = parseFloat ( id ) ;
4466+ if ( ! Number . isNaN ( parsed ) && parsed > 0 ) {
4467+ if ( fromNoteValueRow && parsed >= 1 ) {
4468+ parsed = 1 / parsed ;
4469+ }
4470+ if ( parsed > 0 ) {
4471+ return parsed ;
4472+ }
4473+ }
4474+ }
4475+
4476+ return null ;
4477+ }
4478+
4479+ /**
4480+ * Retrieves meter information for the active turtle.
4481+ * @returns {{beatsPerMeasure: number, beatUnit: number} }
4482+ * @private
4483+ */
4484+ _getMeterInfo ( ) {
4485+ let beatsPerMeasure = 4 ;
4486+ let noteValuePerBeat = 4 ;
4487+
4488+ if (
4489+ this . activity &&
4490+ this . activity . turtles &&
4491+ typeof this . activity . turtles . ithTurtle === "function"
4492+ ) {
4493+ const turtle = this . activity . turtles . ithTurtle ( 0 ) ;
4494+ if ( turtle && turtle . singer ) {
4495+ if (
4496+ Number . isFinite ( turtle . singer . beatsPerMeasure ) &&
4497+ turtle . singer . beatsPerMeasure > 0
4498+ ) {
4499+ beatsPerMeasure = turtle . singer . beatsPerMeasure ;
4500+ }
4501+
4502+ if (
4503+ Number . isFinite ( turtle . singer . noteValuePerBeat ) &&
4504+ turtle . singer . noteValuePerBeat > 0
4505+ ) {
4506+ noteValuePerBeat = turtle . singer . noteValuePerBeat ;
4507+ }
4508+ }
4509+ }
4510+
4511+ beatsPerMeasure = Math . max ( 1 , Math . round ( beatsPerMeasure ) ) ;
4512+
4513+ let beatUnit = noteValuePerBeat >= 1 ? 1 / noteValuePerBeat : noteValuePerBeat ;
4514+ if ( ! Number . isFinite ( beatUnit ) || beatUnit <= 0 ) {
4515+ beatUnit = 1 / 4 ;
4516+ }
4517+
4518+ return {
4519+ beatsPerMeasure,
4520+ beatUnit
4521+ } ;
4522+ }
4523+
4524+ /**
4525+ * Determines the barline color that best contrasts with the current theme.
4526+ * @returns {string }
4527+ * @private
4528+ */
4529+ _resolveBarlineColor ( ) {
4530+ if ( ! platformColor || ! platformColor . background ) {
4531+ return "#303030" ;
4532+ }
4533+
4534+ return this . _isDarkColor ( platformColor . background ) ? "#E0E0E0" : "#303030" ;
4535+ }
4536+
4537+ /**
4538+ * Determines if a hex color string represents a dark tone.
4539+ * @param {string } color - Hex color string (e.g., #303030).
4540+ * @returns {boolean }
4541+ * @private
4542+ */
4543+ _isDarkColor ( color ) {
4544+ if ( typeof color !== "string" ) {
4545+ return false ;
4546+ }
4547+
4548+ let hex = color . trim ( ) ;
4549+ if ( hex . startsWith ( "#" ) ) {
4550+ hex = hex . slice ( 1 ) ;
4551+ } else {
4552+ return false ;
4553+ }
4554+
4555+ if ( hex . length === 3 ) {
4556+ hex = hex
4557+ . split ( "" )
4558+ . map ( ( char ) => char + char )
4559+ . join ( "" ) ;
4560+ }
4561+
4562+ if ( hex . length !== 6 ) {
4563+ return false ;
4564+ }
4565+
4566+ const r = parseInt ( hex . slice ( 0 , 2 ) , 16 ) ;
4567+ const g = parseInt ( hex . slice ( 2 , 4 ) , 16 ) ;
4568+ const b = parseInt ( hex . slice ( 4 , 6 ) , 16 ) ;
4569+
4570+ if ( [ r , g , b ] . some ( ( value ) => Number . isNaN ( value ) ) ) {
4571+ return false ;
4572+ }
4573+
4574+ const luminance = ( 0.299 * r + 0.587 * g + 0.114 * b ) / 255 ;
4575+ return luminance < 0.5 ;
42694576 }
42704577
42714578 /**
0 commit comments