Merge pull request #9717 from opf/feature/38981-merge-changes-of-114

[#38981] Merge PR for integrating xeokit bim viewer v2.3.6
pull/9724/head
Oliver Günther 3 years ago committed by GitHub
commit 837a09d2d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 43
      app/services/attachments/base_service.rb
  2. 2
      app/services/attachments/build_service.rb
  3. 18
      app/services/attachments/create_service.rb
  4. 14
      config/locales/js-en.yml
  5. 2
      frontend/package.json
  6. 38
      frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.component.html
  7. 72
      frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.component.sass
  8. 18
      frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.component.ts
  9. 9
      frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts
  10. 57
      frontend/src/app/features/bim/ifc_models/pages/viewer/styles/tabs.sass
  11. 12
      frontend/src/app/features/bim/ifc_models/pages/viewer/styles/toolbar.sass
  12. 2
      modules/bim/app/models/bim/bcf/viewpoint.rb
  13. 2
      modules/bim/app/seeders/bim/demo_data/bcf_xml_seeder.rb
  14. 2
      modules/bim/app/services/bim/ifc_models/set_attributes_service.rb
  15. 38
      modules/bim/lib/open_project/bim/bcf_xml/importer.rb
  16. 4
      modules/bim/lib/open_project/bim/bcf_xml/issue_reader.rb
  17. 18
      modules/bim/spec/bcf/bcf_xml/importer_spec.rb
  18. 4
      modules/bim/spec/contracts/ifc_models/shared_contract_examples.rb
  19. BIN
      modules/bim/spec/fixtures/files/issue-with-viewpoint.bcf
  20. 4
      modules/bim/spec/support/pages/ifc_models/show_default.rb

@ -0,0 +1,43 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module Attachments
class BaseService < ::BaseServices::Create
##
# Create an attachment service bypassing the user-provided whitelist
# for internal purposes such as exporting data.
#
# @param user The user to call the service with
# @param whitelist A custom whitelist to validate with, or empty to disable validation
#
# Warning: When passing an empty whitelist, this results in no validations on the content type taking place.
def self.bypass_whitelist(user:, whitelist: [])
new(user: user, contract_options: { whitelist: whitelist.map(&:to_s) })
end
end
end

@ -27,7 +27,7 @@
#++
module Attachments
class BuildService < ::BaseServices::Create
class BuildService < BaseService
private
def persist(service_result)

@ -26,23 +26,12 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class Attachments::CreateService < ::BaseServices::Create
include Attachments::TouchContainer
module Attachments
class CreateService < BaseService
include TouchContainer
around_call :error_wrapped_call
##
# Create an attachment service bypassing the user-provided whitelist
# for internal purposes such as exporting data.
#
# @param user The user to call the service with
# @param whitelist A custom whitelist to validate with, or empty to disable validation
#
# Warning: When passing an empty whitelist, this results in no validations on the content type taking place.
def self.bypass_whitelist(user:, whitelist: [])
new(user: user, contract_options: { whitelist: whitelist.map(&:to_s) })
end
def persist(call)
attachment = call.result
if attachment.container
@ -96,4 +85,5 @@ class Attachments::CreateService < ::BaseServices::Create
OpenProject.logger.error message
end
end
end

@ -333,15 +333,11 @@ en:
current_new_feature_html: >
The release contains various new features and improvements: <br>
<ul class="%{list_styling_class}">
<li>BIM: Improved performance of the IFC model viewer.</li>
<li>BIM: The IFC viewer now supports double precision models, which is great for large construction sites such as bridges, roads etc.</li>
<li>BIM: Toggle button for switching between perspective and orthogonal mode.</li>
<li>BIM: A menu for editing, deleting or flipping section planes.</li>
<li>New button in header navigation to create projects, users and work packages.</li>
<li>A new invite modal for users, groups and placeholder users allows to easily add or invite new users and assign them to work packages.</li>
<li>API v3 extensions for groups and more, i.e. create, read, update and delete groups and group members through the API.</li>
<li>Multi-selection for project custom fields of type list.</li>
<li>Creation of backup from the web interface.</li>
<li>Take a look at our <a href="https://github.com/opf/openproject-revit-add-in">Revit Add-in</a> which lets you work on BCF work packages directly in Revit.</li>
<li>IFC properties got a dedicated pane. Copying GUIDs has never been easier.</li>
<li>Reverse clipping plane direction in BCF viewpoint and become compliant with future BCF 3.0 (Now same direction as Solibri).
<li>Fixed viewing IFC models on mobile.</li>
<li>Fixed export of work package views with BCF snapshot column.</li>
</ul>
label_activate: "Activate"

@ -88,7 +88,7 @@
"@uirouter/core": "^6.0.7",
"@uirouter/rx": "^0.6.5",
"@w11k/ngx-componentdestroyed": "^5.0.2",
"@xeokit/xeokit-bim-viewer": "2.3.0-alpha.4",
"@xeokit/xeokit-bim-viewer": "2.3.6",
"autoprefixer": "^9.6.1",
"byte-base64": "^1.1.0",
"cdk-drag-scroll": "^0.0.6",

@ -15,26 +15,44 @@
</div>
</div>
<div class="op-ifc-viewer--container xeokit-busy-modal-backdrop"
<div *ngIf="modelCount"
class="op-ifc-viewer--container xeokit-busy-modal-backdrop"
data-qa-selector="op-ifc-viewer--container">
<div *ngIf="modelCount && !keyboardEnabled"
class="op-ifc-viewer--focus-warning">
<a class="op-ifc-viewer--keyboard-disabled-icon icon-no-color icon-input-disabled"
(mousedown)="enableFromIcon($event)"
[attr.title]="text.keyboard_input_disabled">
</a>
</div>
<div class="op-ifc-viewer--toolbar op-ifc-viewer--model-canvas-overlay"
data-qa-selector="op-ifc-viewer--toolbar">
<div class="op-ifc-viewer--toolbar-container"
data-qa-selector="op-ifc-viewer--toolbar-container">
</div>
<div class="op-ifc-viewer--toolbar-info-button">
<button class="icon-info1 xeokit-btn"
[class.active]="inspectorVisible$ | async"
(click)="toggleInspector()">
</button>
</div>
</div>
<canvas #modelCanvas
class="op-ifc-viewer--model-canvas"
data-qa-selector="op-ifc-viewer--model-canvas"
tabindex="0">
</canvas>
<div *ngIf="!keyboardEnabled"
class="op-ifc-viewer--focus-warning op-ifc-viewer--model-canvas-overlay">
<a class="op-ifc-viewer--keyboard-disabled-icon icon-no-color icon-input-disabled"
(mousedown)="enableFromIcon($event)"
[attr.title]="text.keyboard_input_disabled">
</a>
</div>
<canvas class="op-ifc-viewer--nav-cube-canvas op-ifc-viewer--model-canvas-overlay"
data-qa-selector="op-ifc-viewer--nav-cube-canvas"></canvas>
</div>
<canvas class="op-ifc-viewer--nav-cube-canvas"
data-qa-selector="op-ifc-viewer--nav-cube-canvas" ></canvas>
<div class="op-ifc-viewer--inspector-container"
[class.op-ifc-viewer--inspector-container-hidden]="!(inspectorVisible$ | async)"
data-qa-selector="op-ifc-viewer--inspector-container">
</div>
</div>

@ -1,35 +1,18 @@
@import "src/assets/sass/helpers"
.op-ifc-viewer
display: flex
justify-content: stretch
height: 100%
.notification-box
position: absolute
right: 10px
&--nav-cube-canvas
position: absolute
width: 200px
height: 200px
bottom: 0
right: 0
z-index: 10
&--section-planes-overview-canvas
position: absolute
width: 250px
height: 250px
top: 50px
right: 50px
z-index: 2000
&--container
position: relative
width: 100%
height: 100%
padding-bottom: 10px
flex-grow: 1
padding-bottom: 24px
overflow: hidden
min-width: 420px
&--model-canvas
width: 100%
@ -38,19 +21,48 @@
&:focus
outline: none
&--focus-warning
@include without-link-styling
&-overlay
position: absolute
top: 6px
right: 395px
z-index: 2
&--keyboard-disabled-icon
font-size: 20px
// toolbar overlays model canvas
&--nav-cube-canvas
bottom: 0
right: 0
width: 200px
height: 200px
// toolbar overlays model canvas
&--toolbar
top: 0
right: 0
display: flex
justify-content: flex-end
margin: 0.5em
&-info-button
margin-left: 0.5em
&--inspector-container
width: 380px
border-left: 2px solid #eee
border-top: 2px solid #eee
&--toolbar-container,
@media only screen and (max-width: 679px)
width: 300px
border-bottom: 2px solid #eee
&-hidden
display: none
// focus warning icon overlays model canvas
&--focus-warning
margin-top: 10px
margin-right: 5px
@include without-link-styling
left: 68px
bottom: 68px
&--keyboard-disabled-icon
font-size: 42px
&--nav-cube-canvas,
&--focus-warning

@ -40,6 +40,7 @@ import { IfcModelsDataService } from 'core-app/features/bim/ifc_models/pages/vie
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { CurrentUserService } from 'core-app/core/current-user/current-user.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'ifc-viewer',
@ -62,6 +63,8 @@ export class IFCViewerComponent implements OnInit, OnDestroy {
keyboardEnabled = false;
public inspectorVisible$:BehaviorSubject<boolean>;
@ViewChild('outerContainer') outerContainer:ElementRef;
@ViewChild('modelCanvas') modelCanvas:ElementRef;
@ -72,6 +75,7 @@ export class IFCViewerComponent implements OnInit, OnDestroy {
private ifcViewer:IFCViewerService,
private currentUserService:CurrentUserService,
private currentProjectService:CurrentProjectService) {
this.inspectorVisible$ = this.ifcViewer.inspectorVisible$;
}
ngOnInit():void {
@ -91,12 +95,14 @@ export class IFCViewerComponent implements OnInit, OnDestroy {
'ifc_models/destroy',
],
this.currentProjectService.id as string,
).subscribe((manageIfcModelsAllowed) => {
)
.subscribe((manageIfcModelsAllowed) => {
this.ifcViewer.newViewer(
{
canvasElement: element.find('[data-qa-selector="op-ifc-viewer--model-canvas"]')[0], // WebGL canvas
explorerElement: jQuery('[data-qa-selector="op-ifc-viewer--tree-panel"]')[0], // Left panel
toolbarElement: element.find('[data-qa-selector="op-ifc-viewer--toolbar-container"]')[0], // Toolbar
inspectorElement: element.find('[data-qa-selector="op-ifc-viewer--inspector-container"]')[0], // Toolbar
navCubeCanvasElement: element.find('[data-qa-selector="op-ifc-viewer--nav-cube-canvas"]')[0],
busyModelBackdropElement: element.find('.xeokit-busy-modal-backdrop')[0],
enableEditModels: manageIfcModelsAllowed,
@ -110,8 +116,12 @@ export class IFCViewerComponent implements OnInit, OnDestroy {
this.ifcViewer.destroy();
}
toggleInspector():void {
this.ifcViewer.inspectorVisible$.next(!this.inspectorVisible$.getValue());
}
@HostListener('mousedown')
enableKeyBoard() {
enableKeyBoard():void {
if (this.modelCount) {
this.keyboardEnabled = true;
this.ifcViewer.setKeyboardEnabled(true);
@ -119,14 +129,14 @@ export class IFCViewerComponent implements OnInit, OnDestroy {
}
@HostListener('window:mousedown', ['$event.target'])
disableKeyboard(target:Element) {
disableKeyboard(target:Element):void {
if (this.modelCount && !this.outerContainer.nativeElement.contains(target)) {
this.keyboardEnabled = false;
this.ifcViewer.setKeyboardEnabled(false);
}
}
enableFromIcon(event:MouseEvent) {
enableFromIcon(event:MouseEvent):boolean {
this.enableKeyBoard();
// Focus on the canvas

@ -16,6 +16,7 @@ export interface XeokitElements {
canvasElement:HTMLElement;
explorerElement:HTMLElement;
toolbarElement:HTMLElement;
inspectorElement:HTMLElement;
navCubeCanvasElement:HTMLElement;
busyModelBackdropElement:HTMLElement;
enableEditModels?:boolean;
@ -51,6 +52,8 @@ export class IFCViewerService extends ViewerBridgeService {
public viewerVisible$ = new BehaviorSubject<boolean>(false);
public inspectorVisible$ = new BehaviorSubject<boolean>(false);
private _viewer:any;
@InjectField() pathHelper:PathHelperService;
@ -68,7 +71,7 @@ export class IFCViewerService extends ViewerBridgeService {
}
public newViewer(elements:XeokitElements, projects:any[]):void {
import('@xeokit/xeokit-bim-viewer/dist/xeokit-bim-viewer.es').then((XeokitViewerModule:any) => {
void import('@xeokit/xeokit-bim-viewer/dist/xeokit-bim-viewer.es').then((XeokitViewerModule:any) => {
const server = new XeokitServer(this.pathHelper);
const viewerUI = new XeokitViewerModule.BIMViewer(server, elements);
@ -84,6 +87,10 @@ export class IFCViewerService extends ViewerBridgeService {
window.location.href = this.pathHelper.ifcModelsNewPath(this.currentProjectService.identifier as string);
});
viewerUI.on('openInspector', () => {
this.inspectorVisible$.next(true);
});
viewerUI.on('editModel', (event:{ modelId:number|string }) => { // "Edit" selected in Models tab's context menu
window.location.href = this.pathHelper.ifcModelsEditPath(this.currentProjectService.identifier as string, event.modelId);
});

@ -38,7 +38,8 @@
.xeokit-models,
.xeokit-objects,
.xeokit-classes,
.xeokit-storeys
.xeokit-storeys,
.xeokit-properties
overflow: hidden !important
.xeokit-btn-group
@ -62,6 +63,56 @@
&:focus
outline: none
.xeokit-propertiesTab
padding: 0 15px
a.xeokit-tab-btn
text-decoration: none
color: var(--body-font-color)
line-height: var(--main-menu-item-height)
height: var(--main-menu-item-height)
font-family: var(--body-font-family)
font-size: var(--main-menu-font-size)
font-style: normal
&.active a.xeokit-tab-btn
color: var(--content-link-color)
border-bottom-color: var(--content-link-color)
.xeokit-tab-content
position: relative
padding: 1.5em 0
color: var(--body-font-color)
p
font-size: 0.9rem
.xeokit-no-prop-set-warning
visibility: hidden
.xeokit-table
width: 100%
tr:first-child
border-top: 1px solid var(--table-row-border-color)
tr
border-bottom: 1px solid var(--table-row-border-color)
td:first-child
font-weight: bold
td:last-child
overflow-wrap: anywhere
td
text-align: left
font-size: 0.9rem
line-height: 1.6
padding-top: 0.5rem
padding-bottom: 0.5rem
vertical-align: top
// Hightlighted list items shall look like a "pill", similarly to the selected tree item in the wiki menu.
$pill-padding-left: 8px
@ -70,9 +121,11 @@ $pill-padding-left: 8px
list-style: none
padding-left: 30px
margin-left: -1 * $pill-padding-left
li
line-height: 30px
padding-left: $pill-padding-left
a
display: inline-block
line-height: 30px
@ -95,6 +148,7 @@ $pill-padding-left: 8px
&.plus:before
@include icon-mixin-arrow-right2
&.minus:before
@include icon-mixin-arrow-down1
@ -110,6 +164,7 @@ $pill-padding-left: 8px
span
cursor: pointer
vertical-align: middle
&:hover
vertical-align: middle

@ -1,10 +1,6 @@
// -------------------------- GENERIC --------------------------
.op-ifc-viewer--toolbar-container
position: absolute
width: 100%
padding: 0px 6px
pointer-events: none
z-index: 2
button
pointer-events: all
@ -89,9 +85,15 @@
// -------------------------- MOBILE specific --------------------------
@media only screen and (max-width: 679px)
.op-ifc-viewer--toolbar-container
.op-ifc-viewer--toolbar
padding-right: 15px
.xeokit-toolbar
flex-wrap: nowrap
.xeokit-btn-group
white-space: nowrap
.op-ifc-viewer--focus-warning,
.op-ifc-viewer--toolbar-container
.fa-male,

@ -46,7 +46,7 @@ module Bim::Bcf
def build_snapshot(file, user: User.current)
::Attachments::BuildService
.new(user: user)
.bypass_whitelist(user: user)
.call(file: file, container: self, filename: file.original_filename, description: 'snapshot')
.result
end

@ -41,7 +41,7 @@ module Bim
filename = project_data_for(key, 'bcf_xml_file')
return unless filename.present?
user = User.admin.first
user = User.admin.active.first
print_status ' ↳ Import BCF XML file'

@ -77,7 +77,7 @@ module Bim
model.attachments << ifc_attachment
else
::Attachments::BuildService
.new(user: user)
.bypass_whitelist(user: user)
.call(file: ifc_attachment, container: model, filename: ifc_attachment.original_filename, description: 'ifc')
end
end

@ -39,11 +39,32 @@ module OpenProject::Bim::BcfXml
end
end
def import!(options = {})
User.execute_as(current_user) do
perform_import(options)
end
end
def aggregations
@aggregations ||= Aggregations.new(extractor_list, @project)
end
def import!(options = {})
def bcf_version_valid?
Zip::File.open(@file) do |zip|
zip_entry = zip.find { |entry| entry.name.end_with?('bcf.version') }
markup = zip_entry.get_input_stream.read
doc = Nokogiri::XML(markup, nil, 'UTF-8')
bcf_version = doc.xpath('/Version').first['VersionId']
return Gem::Version.new(bcf_version) >= Gem::Version.new(MINIMUM_BCF_VERSION)
end
rescue StandardError => _e
# The uploaded file could be anything.
false
end
private
def perform_import(options)
options = DEFAULT_IMPORT_OPTIONS.merge(options)
Zip::File.open(@file) do |zip|
create_or_add_missing_members(options)
@ -61,21 +82,6 @@ module OpenProject::Bim::BcfXml
raise
end
def bcf_version_valid?
Zip::File.open(@file) do |zip|
zip_entry = zip.find { |entry| entry.name.end_with?('bcf.version') }
markup = zip_entry.get_input_stream.read
doc = Nokogiri::XML(markup, nil, 'UTF-8')
bcf_version = doc.xpath('/Version').first['VersionId']
return Gem::Version.new(bcf_version) >= Gem::Version.new(MINIMUM_BCF_VERSION)
end
rescue StandardError => e
# The uploaded file could be anything.
false
end
private
def create_or_add_missing_members(options)
treat_invalid_people(options)
treat_unknown_mails(options)

@ -45,7 +45,7 @@ module OpenProject::Bim::BcfXml
# If there are already errors during the BCF issue creation, don't create or update the WP.
return if issue.errors.any?
self.is_update = issue.work_package.present?
self.is_update = issue.work_package.persisted?
self.wp_last_updated_at = issue.work_package&.updated_at
call =
@ -218,7 +218,7 @@ module OpenProject::Bim::BcfXml
end
def initialize_issue
::Bim::Bcf::Issue.new(uuid: topic_uuid)
::Bim::Bcf::Issue.new(uuid: topic_uuid, project: project)
end
##

@ -40,6 +40,7 @@ describe ::OpenProject::Bim::BcfXml::Importer do
let(:project) do
FactoryBot.create(:project,
identifier: 'bim_project',
enabled_module_names: %w[bim work_package_tracking],
types: [type])
end
let(:member_role) do
@ -72,7 +73,6 @@ describe ::OpenProject::Bim::BcfXml::Importer do
workflow
priority
bcf_manager_member
login_as(bcf_manager)
end
describe '#to_listing' do
@ -99,4 +99,20 @@ describe ::OpenProject::Bim::BcfXml::Importer do
expect(WorkPackage.count).to be_eql 2
end
end
context 'with a viewpoint and snapshot' do
let(:filename) { 'issue-with-viewpoint.bcf' }
it 'imports that viewpoint successfully' do
expect(subject.import!).to be_present
expect(::Bim::Bcf::Issue.count).to eq 1
issue = ::Bim::Bcf::Issue.last
expect(issue.viewpoints.count).to eq 1
viewpoint = issue.viewpoints.first
expect(viewpoint.attachments.count).to eq 1
expect(viewpoint.snapshot).to be_present
end
end
end

@ -106,7 +106,7 @@ shared_examples_for 'ifc model contract' do
let(:ifc_file) { FileHelpers.mock_uploaded_file name: "model.ifc", content_type: 'application/binary', binary: true }
let(:ifc_attachment) do
::Attachments::BuildService
.new(user: current_user)
.bypass_whitelist(user: current_user)
.call(file: ifc_file, filename: 'model.ifc')
.result
end
@ -122,7 +122,7 @@ shared_examples_for 'ifc model contract' do
end
let(:ifc_attachment) do
::Attachments::BuildService
.new(user: current_user)
.bypass_whitelist(user: current_user)
.call(file: ifc_file, filename: 'model.ifc')
.result
end

@ -62,12 +62,12 @@ module Pages
selector = '.xeokit-btn'
if visible
within ('[data-qa-selector="op-ifc-viewer--toolbar-container"]') do
within ('[data-qa-selector="op-ifc-viewer--toolbar"]') do
expect(page).to have_selector(selector, count: 9)
end
else
expect(page).to have_no_selector(selector)
expect(page).to have_no_selector('[data-qa-selector="op-ifc-viewer--toolbar-container"]')
expect(page).to have_no_selector('[data-qa-selector="op-ifc-viewer--toolbar"]')
end
end

Loading…
Cancel
Save