import _ from './util' ; import LDParse from './ld_parse' ; import store from './store' ; import { tr } from './translations' ; import { isBox , isNotNull } from './type_helpers' ; const emptyCalloutSize = 50 ; const rotateIconAspectRatio = 0.94 ; const qtyLabelOffset = 5 ; export interface LayoutInterface { book ( item ) : void ; titlePage ( item ) : void ; allInventoryPages ( ) : void ; inventoryPage ( page , box ) : void ; page ( page , layout ? ) : void ; pageNumber ( page ) : void ; step ( step , box , pageMargin ? ) : void ; submodelImage ( submodelImage , box ) : void ; csi ( csi , box ) : void ; pli ( pli ) : void ; pliItem ( pliItem ) : void ; callout ( callout , box ) : void ; calloutArrow ( callout ) : void ; subSteps ( step , stepBox ) : void ; templatePageDividers ( page , box ) : void ; dividers ( target , layoutDirection , rows , cols , box ) : void ; label ( label ) : void ; quantityLabel ( label , font , text ) : void ; mergeSteps ( stepsToMerge , progressCallback ) : void ; adjustBoundingBox : { item ( item , boxes , template ) : void ; stepUnused ( item ) : void ; pli ( item ) : void ; pliItem ( item ) : void ; callout ( item ) : void ; quantityLabel ( item ) : void ; } ; } const Layout = { book ( item ) { const book = store . get . book ( item ) ; for ( let i = 0 ; i < book . pages . length ; i ++ ) { const page = store . get . page ( book . pages [ i ] ) ; if ( page ) { Layout . page ( page ) ; } } } , titlePage ( item ) { const page = store . get . page ( item ) ; const pageSize = store . state . template . page ; const step = store . get . step ( page . steps [ 0 ] ) ; if ( step . csiID == null ) { return ; } const csi = store . get . csi ( step . csiID ) ; const box = { x : 0 , y : 0 , width : pageSize . width , height : pageSize . height } ; store . mutations . step . layout ( { step , box } ) ; step . width = csi . width + 40 ; step . height = csi . height + 40 ; step . x = ( pageSize . width - step . width ) / 2 ; step . y = ( pageSize . height - step . height ) / 2 ; csi . x = csi . y = 20 ; const annotations = page . annotations . map ( store . get . annotation ) . filter ( isNotNull ) ; let titleAnnotation = annotations . find ( a => a . meta . type === 'title-page-model-name' ) ; if ( titleAnnotation == null ) { titleAnnotation = store . mutations . titlePage . addTitleLabel ( page ) ; } Layout . label ( titleAnnotation ) ; titleAnnotation . x = ( pageSize . width - titleAnnotation . width ) / 2 ; titleAnnotation . y = ( step . y - titleAnnotation . height ) / 2 ; let pageCountAnnotation = annotations . find ( a => a . meta . type === 'title-page-page-count' ) ; if ( pageCountAnnotation == null ) { pageCountAnnotation = store . mutations . titlePage . addPageCountLabel ( page ) ; } Layout . label ( pageCountAnnotation ) ; pageCountAnnotation . x = ( pageSize . width - pageCountAnnotation . width ) / 2 ; pageCountAnnotation . y = step . y + step . height + ( ( step . y - pageCountAnnotation . height ) / 2 ) ; delete page . needsLayout ; } , allInventoryPages ( ) { const pages = store . get . inventoryPages ( ) ; const firstPage = pages [ 0 ] ; if ( pages . length > 1 ) { for ( let i = 1 ; i < pages . length ; i ++ ) { const pageToRemove = pages [ i ] ; while ( pageToRemove . pliItems . length ) { const id = pageToRemove . pliItems [ 0 ] ; store . mutations . item . reparent ( { item : { type : 'pliItem' , id } , newParent : firstPage } ) ; } store . mutations . inventoryPage . delete ( { page : pageToRemove } ) ; } } const allPLIItems = firstPage . pliItems . map ( store . get . pliItem ) . filter ( isNotNull ) ; Layout . page ( firstPage ) ; let nextPage = firstPage ; function unplaced ( item ) { return ! nextPage . pliItems . includes ( item . id ) ; } function reparent ( item ) { store . mutations . item . reparent ( { item , newParent : nextPage } ) ; } let unplacedPLIItems = allPLIItems . filter ( unplaced ) ; while ( unplacedPLIItems . length > 0 ) { const opts = { subtype : 'inventoryPage' as PageSubtypes , pageNumber : nextPage . number + 1 , } ; nextPage = store . mutations . page . add ( opts ) ; unplacedPLIItems . forEach ( reparent ) ; Layout . page ( nextPage ) ; unplacedPLIItems = unplacedPLIItems . filter ( unplaced ) ; } } , inventoryPage ( page , box ) { const pliItems = page . pliItems . map ( store . get . pliItem ) ; pliItems . forEach ( Layout . pliItem ) ; pliItems . sort ( function ( a , b ) { if ( a . colorCode < b . colorCode ) { return - 1 ; } else if ( a . colorCode > b . colorCode ) { return 1 ; } else if ( a . width < b . width ) { return - 1 ; } else if ( a . width > b . width ) { return 1 ; } else if ( a . quantity < b . quantity ) { return - 1 ; } else if ( a . quantity > b . quantity ) { return 1 ; } return 0 ; } ) ; page . pliItems = pliItems . map ( el => el . id ) ; const margin = ( getMargin ( store . state . template . page . innerMargin ) + qtyLabelOffset ) * 1.2 ; let colWidth = 0 , x = margin , y = margin ; let columns = [ [ ] ] ; box . width -= margin + margin ; box . height -= margin ; for ( let i = 0 ; i < pliItems . length ; i ++ ) { const pliItem = pliItems [ i ] ; if ( y + pliItem . height > box . height ) { x += colWidth + margin ; y = margin ; colWidth = 0 ; columns . push ( [ ] ) ; } if ( x + pliItem . width > box . width ) { page . pliItems = page . pliItems . slice ( 0 , i ) ; break ; } columns [ columns . length - 1 ] . push ( pliItem ) ; pliItem . x = x ; pliItem . y = y ; y += pliItem . height + margin ; colWidth = Math . max ( colWidth , pliItem . width ) ; } columns = columns . filter ( el => el . length ) ; if ( columns . length === 2 ) { if ( columns [ 1 ] [ 0 ] . x < box . width / 2 ) { columns [ 1 ] . forEach ( item => ( item . x = box . width / 2 ) ) ; } } else if ( columns . length > 2 ) { const lastCol = _ . last ( columns ) ; if ( lastCol != null ) { const remainingSpace = box . width - lastCol [ 0 ] . x - Math . max ( ... ( lastCol . map ( el => el . width ) ) ) ; const spaceToAdd = Math . max ( 0 , remainingSpace / ( columns . length - 1 ) ) ; columns . forEach ( ( col , idx ) => { col . forEach ( item => { item . x += spaceToAdd * idx ; } ) ; } ) ; } } columns . forEach ( col => { if ( col . length > 1 ) { const lastItem = _ . last ( col ) ?? { y : 0 , height : 0 } ; const remainingSpace = box . height - lastItem . y - lastItem . height ; const spaceToAdd = Math . max ( 0 , remainingSpace / ( col . length - 1 ) ) ; col . forEach ( ( item , idx ) => { item . y += spaceToAdd * idx ; } ) ; } } ) ; delete page . needsLayout ; } , page ( page , layout = 'horizontal' ) { if ( page . subtype === 'titlePage' ) { Layout . titlePage ( page ) ; return ; } const template = store . state . template . page ; const margin = getMargin ( template . innerMargin ) ; const borderWidth = template . border . width ; const pageSize = { width : template . width - borderWidth - borderWidth , height : template . height - borderWidth - borderWidth , x : 0 , y : 0 , } ; page . innerContentOffset = { x : borderWidth , y : borderWidth } ; if ( page . numberLabelID != null ) { Layout . pageNumber ( page ) ; const lbl = store . get . numberLabel ( page . numberLabelID ) ; pageSize . height -= lbl . height + ( margin / 2 ) ; } if ( page . subtype === 'inventoryPage' ) { Layout . inventoryPage ( page , pageSize ) ; return ; } const stepCount = page . steps . length ; let cols = Math . ceil ( Math . sqrt ( stepCount ) ) ; let rows = Math . ceil ( stepCount / cols ) ; let layoutDirection ; if ( layout === 'horizontal' || layout === 'vertical' ) { layoutDirection = layout ; if ( layout === 'vertical' ) { [ cols , rows ] = [ rows , cols ] ; } } else { if ( layout . rows === 'auto' && layout . cols !== 'auto' ) { cols = layout . cols ; rows = Math . ceil ( stepCount / cols ) ; } else if ( layout . rows !== 'auto' && layout . cols === 'auto' ) { rows = layout . rows ; cols = Math . ceil ( stepCount / rows ) ; } else if ( layout . rows !== 'auto' && layout . cols !== 'auto' ) { rows = layout . rows ; cols = layout . cols ; } layoutDirection = layout . direction || ( pageSize . width > pageSize . height ? 'horizontal' : 'vertical' ) ; } const colSize = Math . floor ( pageSize . width / cols ) ; const rowSize = Math . floor ( pageSize . height / rows ) ; const box = { x : 0 , y : 0 , width : colSize , height : rowSize } ; for ( let i = 0 ; i < stepCount ; i ++ ) { if ( layoutDirection === 'vertical' ) { box . x = colSize * Math . floor ( i / rows ) ; box . y = rowSize * ( i % rows ) ; } else { box . x = colSize * ( i % cols ) ; box . y = rowSize * Math . floor ( i / cols ) ; } const step = store . get . step ( page . steps [ i ] ) ; Layout . step ( step , box ) ; } if ( stepCount < ( rows * cols ) ) { const emptySlots = ( rows * cols ) - stepCount ; if ( layoutDirection === 'vertical' ) { const stepsInLastCol = rows - emptySlots ; box . width = colSize ; box . height = Math . floor ( pageSize . height / stepsInLastCol ) ; box . x = ( cols - 1 ) * colSize ; for ( let i = 0 ; i < stepsInLastCol ; i ++ ) { box . y = box . height * i ; const stepIndex = ( ( cols - 1 ) * rows ) + i ; const step = store . get . step ( page . steps [ stepIndex ] ) ; Layout . step ( step , box ) ; } } else { const stepsInLastRow = cols - emptySlots ; box . width = Math . floor ( pageSize . width / stepsInLastRow ) ; box . height = rowSize ; box . y = ( rows - 1 ) * rowSize ; for ( let i = 0 ; i < stepsInLastRow ; i ++ ) { box . x = box . width * i ; const stepIndex = ( ( rows - 1 ) * cols ) + i ; const step = store . get . step ( page . steps [ stepIndex ] ) ; Layout . step ( step , box ) ; } } } page . layout = layout ; page . actualLayout = ( rows > 1 || cols > 1 ) ? { rows , cols , direction : layoutDirection } : 'horizontal' ; Layout . dividers ( page , layoutDirection , rows , cols , pageSize ) ; if ( 0 && store . state . plisVisible ) { alignStepContent ( page ) ; } delete page . needsLayout ; } , pageNumber ( page ) { if ( page . numberLabelID == null ) { return ; } const template = store . state . template . page ; const margin = getMargin ( template . innerMargin ) ; const borderWidth = template . border . width ; const pageSize = { width : template . width - borderWidth - borderWidth , height : template . height - borderWidth - borderWidth , } ; const lblSize = _ . measureLabel ( template . numberLabel . font , page . number . toString ( ) ) ; const lbl = store . get . numberLabel ( page . numberLabelID ) ; lbl . width = lblSize . width ; lbl . height = lblSize . height ; lbl . y = pageSize . height - margin ; let position = template . numberLabel . position ; if ( position === 'even-left' ) { position = _ . isEven ( page . number ) ? 'left' : 'right' ; } else if ( position === 'even-right' ) { position = _ . isEven ( page . number ) ? 'right' : 'left' ; } if ( position === 'left' ) { lbl . x = margin ; lbl . align = 'left' ; } else { lbl . x = pageSize . width - margin ; lbl . align = 'right' ; } } , step ( step , box , pageMargin ) { const csi = ( step . csiID == null ) ? null : store . get . csi ( step . csiID ) ; if ( step . stretchedPages . length ) { const pageWidth = store . state . template . page . width ; box . width *= ( step . stretchedPages . length + 1 ) ; step . stretchedPages . forEach ( ( pageID , idx ) => { const page = store . get . page ( pageID ) ; if ( page . stretchedStep != null ) { page . stretchedStep . leftOffset = - ( idx + 1 ) * pageWidth ; } } ) ; } const template = ( step . parent . type === 'callout' ) ? store . state . template . callout . step : store . state . template . step ; pageMargin = ( pageMargin == null ) ? getMargin ( store . state . template . page . innerMargin ) : pageMargin ; const margin = getMargin ( template . innerMargin ) ; step . x = box . x + pageMargin ; step . y = box . y + pageMargin ; step . width = box . width - pageMargin - pageMargin ; step . height = box . height - pageMargin - pageMargin ; box = _ . cloneDeep ( box ) ; box . x = box . y = 0 ; box . width = step . width ; box . height = step . height ; step . submodelImages . forEach ( submodelImageID => { const submodelImage = store . get . submodelImage ( submodelImageID ) ; Layout . submodelImage ( submodelImage , box ) ; _ . geom . moveBoxEdge ( box , 'top' , submodelImage . height + margin ) ; } ) ; const pli = ( step . pliID != null && store . state . plisVisible ) ? store . get . pli ( step . pliID ) : null ; if ( pli ) { Layout . pli ( pli ) ; pli . y = box . y ; _ . geom . moveBoxEdge ( box , 'top' , pli . height + margin ) ; } if ( step . numberLabelID != null ) { const lblSize = _ . measureLabel ( template . numberLabel . font , step . number . toString ( ) ) ; const lbl = store . get . numberLabel ( step . numberLabelID ) ; lbl . x = 0 ; lbl . y = box . y ; lbl . width = lblSize . width ; lbl . height = lblSize . height ; _ . geom . moveBoxEdge ( box , 'top' , lbl . height + margin ) ; } step . callouts . forEach ( calloutID => { const callout = store . get . callout ( calloutID ) ; Layout . callout ( callout , box ) ; if ( callout . position === 'left' ) { _ . geom . moveBoxEdge ( box , 'left' , callout . width + margin ) ; } else if ( callout . position === 'right' ) { _ . geom . moveBoxEdge ( box , 'right' , - ( callout . width + margin ) ) ; } else if ( callout . position === 'top' ) { _ . geom . moveBoxEdge ( box , 'top' , callout . height + margin ) ; } else if ( callout . position === 'bottom' ) { _ . geom . moveBoxEdge ( box , 'bottom' , - ( callout . height + margin ) ) ; } } ) ; if ( step . csiID == null && step . steps . length ) { Layout . subSteps ( step , box ) ; } else if ( csi != null ) { Layout . csi ( csi , box ) ; } step . callouts . forEach ( calloutID => { const callout = store . get . callout ( calloutID ) ; Layout . calloutArrow ( callout ) ; } ) ; if ( step . rotateIconID != null && csi != null ) { const icon = store . get . rotateIcon ( step . rotateIconID ) ; icon . width = store . state . template . rotateIcon . size ; icon . height = icon . width * rotateIconAspectRatio ; icon . x = csi . x - icon . width - margin ; icon . y = csi . y - icon . height ; if ( icon . x < 0 ) { csi . x -= icon . x ; icon . x = 0 ; } if ( icon . y < 0 ) { csi . y -= icon . y ; icon . y = 0 ; } if ( step . numberLabelID != null ) { const stepLabel = store . get . numberLabel ( step . numberLabelID ) ; if ( icon . y >= stepLabel . y && icon . y <= stepLabel . y + stepLabel . height + margin && icon . x >= stepLabel . x && icon . x <= stepLabel . x + stepLabel . width + margin ) { icon . y = stepLabel . y + stepLabel . height + margin ; } } } if ( store . get . stepHasSubmodel ( step ) ) { const tagName = `submodel_arrow_step_${ step . id }` ; let annotation = store . state . annotations . find ( a => a . tagName === tagName ) ; if ( store . state . template . pli . includeSubmodels ) { if ( annotation ) { store . mutations . annotation . delete ( { annotation } ) ; } } else if ( csi != null ) { if ( annotation == null ) { annotation = store . mutations . annotation . add ( { annotationType : 'arrow' , properties : { direction : 'right' , tagName } , parent : step , x : 0 , y : 0 , } ) ; } annotation . direction = 'right' ; const base = store . get . point ( annotation . points [ 0 ] ) ; const prevStep = store . get . prevStep ( step ) ; if ( prevStep && prevStep . parent . id === step . parent . id ) { while ( annotation . points . length < 4 ) { store . mutations . calloutArrow . addPoint ( { arrow : annotation } ) ; } base . relativeTo = { type : 'step' , id : prevStep . id } ; base . x = step . width ; base . y = 0 ; const p1 = store . get . point ( annotation . points [ 1 ] ) ; p1 . relativeTo = { type : 'step' , id : prevStep . id } ; p1 . x = step . width ; p1 . y = step . height ; const p2 = store . get . point ( annotation . points [ 2 ] ) ; p2 . relativeTo = { type : 'step' , id : prevStep . id } ; p2 . x = step . width ; p2 . y = csi . y + ( csi . height / 2 ) ; } else { while ( annotation . points . length > 2 ) { store . mutations . item . delete ( { item : { type : 'point' , id : annotation . points [ 2 ] } } ) ; } base . relativeTo = { type : 'page' , id : step . parent . id } ; base . y = store . get . coords . pointToPage ( 0 , csi . height / 2 , csi ) . y ; base . x = 0 ; } const lastIdx = _ . last ( annotation . points ) ; if ( lastIdx != null ) { const tip = store . get . point ( lastIdx ) ; tip . relativeTo = { type : 'csi' , id : csi . id } ; tip . x = - _ . geom . arrow ( ) . head . length - 10 ; tip . y = csi . height / 2 ; } } } } , submodelImage ( submodelImage , box ) { const template = store . state . template . submodelImage ; const margin = getMargin ( store . state . template . submodelImage . innerMargin ) ; if ( submodelImage . csiID == null ) { return ; } const csi = store . get . csi ( submodelImage . csiID ) ; const part = LDParse . model . get . abstractPart ( submodelImage . modelFilename ) ; let csiSize ; csi . isDirty = true ; if ( csi . scale != null ) { csiSize = store . render . pli ( part . colorCode , part . filename , csi ) ; } else { csi . autoScale = template . csi . scale ; csiSize = store . render . pli ( part . colorCode , part . filename , csi ) ; if ( csiSize != null && csiSize . height > box . height * template . maxHeight ) { csi . autoScale = ( csi . autoScale ?? 1 ) * ( box . height * template . maxHeight ) / csiSize . height ; csi . isDirty = true ; csiSize = store . render . pli ( part . colorCode , part . filename , csi ) ; } else { csi . autoScale = null ; } } if ( csiSize == null ) { return ; } csi . x = csi . y = margin ; csi . width = csiSize . width ; csi . height = csiSize . height ; const borderWidth = template . border . width ; submodelImage . innerContentOffset = { x : borderWidth , y : borderWidth } ; submodelImage . x = box . x ; submodelImage . y = box . y ; submodelImage . width = borderWidth + margin + csiSize . width + margin + borderWidth ; submodelImage . height = borderWidth + margin + csiSize . height + margin + borderWidth ; if ( submodelImage . quantityLabelID != null ) { const font = template . quantityLabel . font ; const lblSize = _ . measureLabel ( font , 'x' + submodelImage . quantity ) ; submodelImage . width += lblSize . width + margin ; const lbl = store . get . quantityLabel ( submodelImage . quantityLabelID ) ; lbl . x = submodelImage . width - borderWidth - borderWidth - margin ; lbl . y = submodelImage . height - borderWidth - borderWidth - margin ; _ . assign ( lbl , lblSize ) ; } } , csi ( csi , box ) { const step = store . get . parent ( csi ) as Step ; const localModel = LDParse . model . get . abstractPart ( step . model . filename ) ; const csiSize = store . render . csi ( localModel , step , csi ) || { width : 0 , height : 0 } ; csi . x = box . x + ( ( box . width - csiSize . width ) / 2 ) ; csi . y = box . y + ( ( box . height - csiSize . height ) / 2 ) ; csi . width = csiSize . width ; csi . height = csiSize . height ; } , pli ( pli ) { let pliItems = pli . pliItems ; if ( ! store . state . template . pli . includeSubmodels ) { pliItems = pliItems . filter ( id => { return ! store . get . pliItemIsSubmodel ( { id , type : 'pliItem' } ) ; } ) ; } const borderWidth = store . state . template . pli . border . width ; pli . innerContentOffset = { x : borderWidth , y : borderWidth } ; pli . borderOffset . x = pli . borderOffset . y = 0 ; if ( _ . isEmpty ( pliItems ) ) { pli . x = pli . y = pli . width = pli . height = 0 ; return ; } const margin = getMargin ( store . state . template . pli . innerMargin ) ; let maxHeight = 0 , left = margin + qtyLabelOffset ; for ( let i = 0 ; i < pliItems . length ; i ++ ) { const pliItem = store . get . pliItem ( pliItems [ i ] ) ; Layout . pliItem ( pliItem ) ; pliItem . x = left ; pliItem . y = margin ; left += pliItem . width + margin ; if ( pliItem . quantityLabelID != null ) { const quantityLabel = store . get . quantityLabel ( pliItem . quantityLabelID ) ; maxHeight = Math . max ( maxHeight , pliItem . height - qtyLabelOffset + quantityLabel . height ) ; } } pli . x = pli . y = 0 ; pli . width = borderWidth + left + borderWidth ; pli . height = borderWidth + margin + maxHeight + margin + borderWidth ; } , pliItem ( pliItem ) { const pliSize = store . render . pli ( pliItem . colorCode , pliItem . filename , pliItem ) ; if ( pliSize != null ) { pliItem . x = pliItem . y = 0 ; pliItem . width = pliSize . width ; pliItem . height = pliSize . height ; const font = store . state . template . pliItem . quantityLabel . font ; const lblSize = _ . measureLabel ( font , 'x' + pliItem . quantity ) ; if ( pliItem . quantityLabelID != null ) { const quantityLabel = store . get . quantityLabel ( pliItem . quantityLabelID ) ; quantityLabel . x = - qtyLabelOffset ; quantityLabel . y = pliSize . height ; quantityLabel . width = lblSize . width ; quantityLabel . height = lblSize . height ; } } } , callout ( callout , box ) { const borderWidth = store . state . template . callout . border . width ; const margin = getMargin ( store . state . template . callout . innerMargin ) ; const calloutBox = { x : 0 , y : 0 , width : 0 , height : margin } ; const pos = callout . position ; const isOnSide = ( pos === 'left' || pos === 'right' ) ; callout . borderOffset . x = callout . borderOffset . y = 0 ; const stepSizes = callout . steps . map ( stepID => { const step = store . get . step ( stepID ) ; return { step , ... ( measureStep ( step ) ) } ; } ) ; if ( callout . layout === 'horizontal' ) { const maxCalloutWidth = isOnSide ? box . width * 0.75 : box . width ; const tallestStep = Math . max ( ... stepSizes . map ( el => el . height ) ) ; const stepBox = { x : margin , y : margin , width : 0 , height : tallestStep , } ; calloutBox . height = margin + tallestStep + margin ; const rows = [ [ ] ] ; stepSizes . forEach ( ( entry , idx ) => { const stepWidth = borderWidth + stepBox . x + entry . width + margin + borderWidth ; if ( idx > 0 && ( stepWidth > maxCalloutWidth ) ) { rows . push ( [ ] ) ; stepBox . x = margin ; stepBox . y += tallestStep + margin ; calloutBox . height += tallestStep + margin ; } stepBox . width = entry . width ; rows [ rows . length - 1 ] . push ( { step : entry . step , box : _ . cloneDeep ( stepBox ) } ) ; Layout . step ( entry . step , stepBox , 0 ) ; stepBox . x += stepBox . width + margin ; calloutBox . width = Math . max ( calloutBox . width , stepBox . x ) ; } ) ; const cols = _ . unzip ( rows ) ; cols . forEach ( col => { col = col . filter ( el => el ) ; const maxX = Math . max ( ... col . map ( c => c . box . x ) ) ; col . forEach ( el => ( el . step . x = maxX ) ) ; } ) ; } else { const maxCalloutHeight = isOnSide ? box . height : box . height * 0.5 ; const widestStep = Math . max ( ... stepSizes . map ( el => el . width ) ) ; const stepBox = { x : margin , y : margin , width : widestStep , height : 0 , } ; calloutBox . width = margin + widestStep + margin ; const columns = [ [ ] ] ; stepSizes . forEach ( ( entry , idx ) => { const stepWidth = borderWidth + stepBox . y + entry . height + margin + borderWidth ; if ( idx > 0 && ( stepWidth > maxCalloutHeight ) ) { columns . push ( [ ] ) ; stepBox . y = margin ; stepBox . x += widestStep + margin ; calloutBox . width += widestStep + margin ; } stepBox . height = entry . height ; columns [ columns . length - 1 ] . push ( { step : entry . step , box : _ . cloneDeep ( stepBox ) , } ) ; Layout . step ( entry . step , stepBox , 0 ) ; stepBox . y += stepBox . height + margin ; calloutBox . height = Math . max ( calloutBox . height , stepBox . y ) ; } ) ; const rows = _ . unzip ( columns ) ; rows . forEach ( row => { row = row . filter ( el => el ) ; const maxY = Math . max ( ... row . map ( c => c . box . y ) ) ; row . forEach ( el => ( el . step . y = maxY ) ) ; } ) ; } callout . innerContentOffset = { x : borderWidth , y : borderWidth } ; callout . width = borderWidth + calloutBox . width + borderWidth ; callout . height = borderWidth + calloutBox . height + borderWidth ; if ( isOnSide ) { callout . x = ( pos === 'left' ) ? box . x : box . x + box . width - callout . width ; callout . y = box . y + ( ( box . height - callout . height ) / 2 ) ; } else { callout . x = box . x + ( ( box . width - callout . width ) / 2 ) ; callout . y = ( pos === 'top' ) ? box . y : box . y + box . height - callout . height ; } } , calloutArrow ( callout ) { const arrow = store . get . calloutArrow ( callout . calloutArrows [ 0 ] ) ; while ( callout . calloutArrows . length > 1 ) { store . mutations . item . delete ( { item : { type : 'calloutArrow' , id : callout . calloutArrows [ 1 ] } } ) ; } const calloutPos = callout . position ; const isOnSide = ( calloutPos === 'left' || calloutPos === 'right' ) ; arrow . direction = { left : 'right' as Directions , top : 'down' as Directions , right : 'left' as Directions , bottom : 'up' as Directions , } [ calloutPos ] ; while ( arrow . points . length > 2 ) { store . mutations . item . delete ( { item : { type : 'point' , id : arrow . points [ 1 ] } } ) ; } let lastStep ; if ( callout . steps . length > 1 ) { const lastStepId = _ . last ( callout . steps ) ; lastStep = ( lastStepId == null ) ? null : store . get . step ( lastStepId ) ; const margin = getMargin ( store . state . template . callout . step . innerMargin ) ; if ( lastStep != null && ( callout . height - margin - lastStep . height - margin < 10 ) ) { lastStep = null ; } } const p1 = store . get . point ( arrow . points [ 0 ] ) ; p1 . relativeTo = { type : 'callout' , id : callout . id } ; if ( isOnSide ) { p1 . x = ( calloutPos === 'left' ) ? callout . borderOffset . x + callout . width : 0 ; p1 . y = ( lastStep == null ) ? callout . borderOffset . y + callout . height / 2 : lastStep . y + ( lastStep . height / 2 ) ; } else { p1 . x = ( lastStep == null ) ? callout . borderOffset . x + callout . width / 2 : lastStep . x + ( lastStep . width / 2 ) ; p1 . y = ( calloutPos === 'top' ) ? callout . borderOffset . y + callout . height : 0 ; } const p2 = store . get . point ( arrow . points [ 1 ] ) ; const step = store . get . step ( callout . parent . id ) ; const csi = ( step ?. csiID == null ) ? null : store . get . csi ( step . csiID ) ; if ( csi != null ) { const arrowSize = _ . geom . arrow ( ) . head . length ; p2 . relativeTo = { type : 'csi' , id : csi . id } ; if ( isOnSide ) { p2 . x = ( calloutPos === 'left' ) ? - arrowSize : csi . width + arrowSize ; p2 . y = csi . height / 2 ; } else { p2 . x = csi . width / 2 ; p2 . y = ( calloutPos === 'top' ) ? - arrowSize : csi . height + arrowSize ; } } } , subSteps ( step , stepBox ) { const stepCount = step . steps . length ; let cols = Math . ceil ( Math . sqrt ( stepCount ) ) ; let rows = Math . ceil ( stepCount / cols ) ; const layout = step . subStepLayout ; if ( layout === 'vertical' ) { [ cols , rows ] = [ rows , cols ] ; } const colSize = Math . floor ( stepBox . width / cols ) ; const rowSize = Math . floor ( stepBox . height / rows ) ; const box = { x : stepBox . x , y : stepBox . y , width : colSize , height : rowSize } ; for ( let i = 0 ; i < stepCount ; i ++ ) { if ( layout === 'vertical' ) { box . x = stepBox . x + ( colSize * Math . floor ( i / rows ) ) ; box . y = stepBox . y + ( rowSize * ( i % rows ) ) ; } else { box . x = stepBox . x + ( colSize * ( i % cols ) ) ; box . y = stepBox . y + ( rowSize * Math . floor ( i / cols ) ) ; } const innerStep = store . get . step ( step . steps [ i ] ) ; Layout . step ( innerStep , box ) ; } Layout . dividers ( step , step . subStepLayout , rows , cols , stepBox ) ; } , templatePageDividers ( page , box ) { const template = store . state . template . page ; const margin = getMargin ( template . innerMargin ) ; const step = store . get . step ( page . steps [ 0 ] ) ; if ( step . csiID == null ) { return ; } const csi = store . get . csi ( step . csiID ) ; const x = margin + csi . x + csi . width + 30 ; store . mutations . divider . add ( { parent : page , p1 : { x , y : margin } , p2 : { x , y : box . height - margin } , } ) ; } , dividers ( target , layoutDirection , rows , cols , box ) { store . mutations . item . deleteChildList ( { item : target , listType : 'divider' } ) ; if ( target . type === 'page' && target . subtype === 'templatePage' ) { const page = store . get . page ( target ) ; Layout . templatePageDividers ( page , box ) ; return ; } const x = box . x || 0 ; const y = box . y || 0 ; const template = store . state . template ; const margin = getMargin ( template [ target . type ] . innerMargin ) ; const colSize = Math . floor ( box . width / cols ) ; const rowSize = Math . floor ( box . height / rows ) ; if ( layoutDirection === 'horizontal' ) { for ( let i = 1 ; i < rows ; i ++ ) { store . mutations . divider . add ( { parent : target , p1 : { x : x + margin , y : y + ( rowSize * i ) } , p2 : { x : x + box . width - margin , y : y + ( rowSize * i ) } , } ) ; } } else { for ( let i = 1 ; i < cols ; i ++ ) { store . mutations . divider . add ( { parent : target , p1 : { x : x + ( colSize * i ) , y : y + margin } , p2 : { x : x + ( colSize * i ) , y : y + box . height - margin } , } ) ; } } } , label ( label ) { const labelSize = _ . measureLabel ( label . font , label . text ) ; label . width = labelSize . width ; label . height = labelSize . height ; } , quantityLabel ( label , font , text ) { const labelSize = _ . measureLabel ( font , text ) ; label . width = labelSize . width ; label . height = labelSize . height ; } , async mergeSteps ( stepsToMerge , progressCallback ) { async function mergeOneStep ( ) { return new Promise ( resolve => window . setTimeout ( ( ) => { const step = stepsToMerge [ 0 ] ; const originalPage = store . get . pageForItem ( step ) ; const prevPage = store . get . prevBasicPage ( originalPage ) ; if ( prevPage != null ) { store . mutations . step . moveToPreviousPage ( { step } ) ; const stepsOverlap = prevPage . steps . some ( isStepTooSmall ) ; if ( stepsOverlap ) { store . mutations . step . moveToNextPage ( { step } ) ; } else { store . mutations . page . delete ( { page : originalPage } ) ; } } progressCallback ( tr ( 'glossary.step_count_@c' , stepsToMerge [ 0 ] . number ) ) ; _ . pullAt ( stepsToMerge , 0 ) ; resolve ( ) ; } , 100 ) ) ; } stepsToMerge = stepsToMerge . slice ( 1 ) ; while ( stepsToMerge . length ) { await mergeOneStep ( ) ; } } , adjustBoundingBox : { item ( item , boxes , template ) { const borderWidth = template . border ? template . border . width : 0 ; const margin = getMargin ( template . innerMargin ) ; const bbox = _ . geom . bbox ( boxes ) ; item . borderOffset = item . borderOffset || { x : 0 , y : 0 } ; item . borderOffset . x = bbox . x - item . x - margin ; item . borderOffset . y = bbox . y - item . y - margin ; item . width = borderWidth + margin + bbox . width + margin + borderWidth ; item . height = borderWidth + margin + bbox . height + margin + borderWidth ; } , stepUnused ( item ) { const step = store . get . step ( item ) ; const children = store . get . stepChildren ( step ) ; const boxes = [ ] ; children . forEach ( child => { if ( isBox ( child ) ) { boxes . push ( child ) ; } } ) ; if ( boxes . length === 1 ) { const margin = getMargin ( store . state . template . step . innerMargin ) ; boxes [ 0 ] = { x : boxes [ 0 ] . x - ( margin / 2 ) , y : boxes [ 0 ] . y - ( margin / 2 ) , width : boxes [ 0 ] . width + margin , height : boxes [ 0 ] . height + margin , } ; } const bbox = _ . geom . bbox ( boxes ) ; step . x += bbox . x ; step . y += bbox . y ; step . width = bbox . width ; step . height = bbox . height ; if ( step . parent . type === 'callout' ) { const callout = store . get . callout ( step . parent ) ; Layout . adjustBoundingBox . callout ( callout ) ; } } , pli ( item ) { const boxes = [ ] ; const pli = store . get . pli ( item ) ; pli . pliItems . forEach ( itemID => { const pliItem = store . get . pliItem ( itemID ) ; boxes . push ( { x : pli . x + pliItem . x , y : pli . y + pliItem . y , width : pliItem . width , height : pliItem . height , } ) ; if ( pliItem . quantityLabelID != null ) { const qtyLabel = store . get . quantityLabel ( pliItem . quantityLabelID ) ; boxes . push ( { x : pli . x + pliItem . x + qtyLabel . x , y : pli . y + pliItem . y + qtyLabel . y , width : qtyLabel . width , height : qtyLabel . height , } ) ; } } ) ; Layout . adjustBoundingBox . item ( pli , boxes , store . state . template . pli ) ; } , pliItem ( item ) { const pliItem = store . get . pliItem ( item ) ; if ( pliItem . parent . type === 'pli' ) { Layout . adjustBoundingBox . pli ( pliItem . parent ) ; } } , callout ( item ) { const callout = store . get . callout ( item ) ; const boxes = [ ] ; callout . steps . forEach ( itemID => { const step = store . get . step ( itemID ) ; if ( step != null ) { boxes . push ( { x : callout . x + step . x , y : callout . y + step . y , width : step . width , height : step . height , } ) ; } } ) ; Layout . adjustBoundingBox . item ( callout , boxes , store . state . template . callout ) ; Layout . calloutArrow ( callout ) ; } , quantityLabel ( item ) { const labelItem = store . get . quantityLabel ( item ) ; const parent = store . get . parent ( labelItem ) ; if ( parent != null ) { Layout . adjustBoundingBox . pliItem ( parent ) ; } } , } , } ; function measureStep ( step ) { const box = { width : 0 , height : 0 } ; const template = store . state . template ; const margin = getMargin ( template . step . innerMargin ) ; if ( step . numberLabelID != null ) { const lblSize = _ . measureLabel ( template . callout . step . numberLabel . font , step . number . toString ( ) ) ; box . width += lblSize . width ; box . height += lblSize . height ; } if ( step . csiID != null ) { const csi = store . get . csi ( step . csiID ) ; const localModel = LDParse . model . get . abstractPart ( step . model . filename ) ; const csiSize = store . render . csi ( localModel , step , csi ) ; if ( csiSize != null ) { box . width += ( box . width === 0 ) ? csiSize . width : csiSize . width + margin ; box . height += ( box . height === 0 ) ? csiSize . height : csiSize . height + margin ; } } if ( step . rotateIconID != null ) { const size = template . rotateIcon . size ; box . width += ( box . width === 0 ) ? size : size + margin ; box . height += size ; } const children = store . get . stepChildren ( step ) ; if ( children . length === 1 ) { box . width += margin ; box . height += margin ; } if ( box . width < 1 && box . height < 1 ) { const emptyStepSize = emptyCalloutSize - margin ; return { width : emptyStepSize , height : emptyStepSize } ; } return box ; } function getMargin ( margin ) { const pageSize = store . state . template . page ; return margin * Math . max ( pageSize . width , pageSize . height ) ; } function alignStepContent ( page ) { const margin = getMargin ( store . state . template . step . innerMargin ) ; const steps = page . steps . map ( stepID => store . get . step ( stepID ) ) ; if ( steps . length < 2 || typeof page . actualLayout !== 'object' || page . actualLayout . direction !== 'horizontal' || page . actualLayout . cols === 'auto' ) { return ; } const stepsByRow = _ . chunk ( steps , page . actualLayout . cols ) ; stepsByRow . forEach ( stepList => { stepList = stepList . filter ( el => ! el . submodelImages . length ) ; const tallestPLIHeight = Math . max ( ... stepList . map ( step => { if ( step . pliID != null ) { return ( store . get . pli ( step . pliID ) ) . height ; } return 0 ; } ) ) ; stepList . forEach ( step => { if ( step . csiID != null ) { const csi = store . get . csi ( step . csiID ) ; csi . y = ( step . height + tallestPLIHeight - csi . height ) / 2 ; if ( step . numberLabelID != null ) { const lbl = store . get . numberLabel ( step . numberLabelID ) ; lbl . y = tallestPLIHeight ? tallestPLIHeight + margin : 0 ; } } } ) ; } ) ; } function isStepTooSmall ( stepID ) { const step = store . get . step ( stepID ) ; if ( step . csiID == null ) { return false ; } const csi = store . get . csi ( step . csiID ) ; const pli = ( store . state . plisVisible && step . pliID != null ) ? store . get . pli ( step . pliID ) : null ; const pliHeight = pli ? pli . height : 0 ; const submodelSpace = { width : 0 , height : 0 } ; step . submodelImages . forEach ( submodelImageID => { const submodelImage = store . get . submodelImage ( submodelImageID ) ; submodelSpace . width = Math . max ( submodelImage . width * 1.05 , submodelSpace . width ) ; submodelSpace . height += submodelImage . height ; } ) ; if ( step . width < csi . width * 1.1 ) { return true ; } else if ( pli && step . width < pli . width * 1.05 ) { return true ; } else if ( step . width < submodelSpace . width ) { return true ; } else if ( step . height < ( submodelSpace . height + pliHeight + csi . height ) * 1.2 ) { return true ; } return false ; } export default Layout ;