Class: DesignToolFixture

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/design_tool_fixture.rb

Overview

Catalog entry for a single fixture (toilet, vanity, tub, etc.) that the
online Floor-Heating Design Tool can drop into a room layout.

Each row stores the SketchUp/SU-friendly mesh, screen-unit measurements,
and a JSON visual representation. Class methods load fixtures from the
legacy XML import, export them back to CSV for tooling, and convert
between the in-browser screen units and real-world feet/meters.

Constant Summary collapse

FUDGE_FACTOR =

Martinez-Rueda polygon clipping algorithm cannot handle lines that overlap EXACTLY

0.0005
SCREEN_UNITS_IN_A_METER =

Screen units in a meter.

0.005
FEET_IN_A_METER =

Feet in a meter.

3.2808
SCREEN_UNITS_IN_A_FOOT =

0.016404

SCREEN_UNITS_IN_A_METER * FEET_IN_A_METER

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #creator, #should_not_save_version, #stamp_record, #updater

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.build_transform(key, x, y, rot, width, height, scaleX, scaleY) ⇒ void

This method returns an undefined value.

Parameters:

  • key (Object)
  • x (Object)
  • y (Object)
  • rot (Object)
  • width (Object)
  • height (Object)
  • scaleX (Object)
  • scaleY (Object)


554
555
556
557
558
559
560
561
# File 'app/models/design_tool_fixture.rb', line 554

def self.build_transform(key, x, y, rot, width, height, scaleX, scaleY)
  scaled_half_width = width * scaleX / 2
  scaled_half_height = height * scaleY / 2
  final_rot = key == 'thermostat' ? 0 : rot

  # `translate(${(x - scaledHalfWidth)}, ${(y - scaledHalfHeight)}) rotate(${finalRot}, ${scaledHalfWidth}, ${scaledHalfHeight}) scale(${scaleX}, ${scaleY})`
  "translate(#{x.to_f - scaled_half_width}, #{y.to_f - scaled_half_height}) rotate(#{final_rot}, #{scaled_half_width}, #{scaled_half_height}) scale(#{scaleX}, #{scaleY})"
end

.export_room_types_join_table_to_csvvoid

This method returns an undefined value.



461
462
463
464
465
466
467
468
469
470
# File 'app/models/design_tool_fixture.rb', line 461

def self.export_room_types_join_table_to_csv
  attributes = %w[design_tool_fixture_id room_type_id]
  CSV.generate(headers: true) do |csv|
    csv << attributes
    res = ActiveRecord::Base.lease_connection.execute("SELECT * from design_tool_fixtures_room_types")
    res.to_a.each do |dtfrth|
      csv << attributes.map { |k| dtfrth[k] }
    end
  end
end

.export_to_csvvoid

This method returns an undefined value.



450
451
452
453
454
455
456
457
458
# File 'app/models/design_tool_fixture.rb', line 450

def self.export_to_csv
  attributes = DesignToolFixture.first.attributes.keys
  CSV.generate(headers: true) do |csv|
    csv << attributes
    DesignToolFixture.all.to_a.each do |dtf|
      csv << attributes.map { |k| dtf.send(k) }
    end
  end
end

.fix_snap_distance(options = {}, snap_distance = 0) ⇒ void

This method returns an undefined value.

These are for offline fixes to these design tool fixtures not to be used other than by me: Ramie

Parameters:

  • options (Hash) (defaults to: {})

    fixture filter options, see get_dtfs_from_options

  • snap_distance (Integer) (defaults to: 0)

    the snap distance to set

Options Hash (options):

  • updated_after_days_ago (Integer, nil)

    only fixtures updated after this many days ago

  • updated_before_days_ago (Integer, nil)

    only fixtures updated before this many days ago

  • snap_distance_gt (Integer, nil)

    only fixtures with snap_distance greater than this

  • snap_distance_lt (Integer, nil)

    only fixtures with snap_distance less than this



481
482
483
484
485
# File 'app/models/design_tool_fixture.rb', line 481

def self.fix_snap_distance(options = {}, snap_distance = 0)
  dtfs = DesignToolFixture.get_dtfs_from_options(options)
  logger.debug "fix_snap_distance about to: dtfs.update_all(snap_distance: #{snap_distance}), dtfs.count: #{dtfs.count}"
  dtfs.update_all(snap_distance: snap_distance)
end

.get_dtfs_from_options(options) ⇒ ActiveRecord::Relation

Returns the fixtures matching the given filter options (all fixtures when empty).

Parameters:

  • options (Hash)

    fixture filter options

Options Hash (options):

  • updated_after_days_ago (Integer, nil)

    only fixtures updated after this many days ago

  • updated_before_days_ago (Integer, nil)

    only fixtures updated before this many days ago

  • snap_distance_gt (Integer, nil)

    only fixtures with snap_distance greater than this

  • snap_distance_lt (Integer, nil)

    only fixtures with snap_distance less than this

Returns:

  • (ActiveRecord::Relation)

    the matching fixtures



532
533
534
535
536
537
538
539
540
541
542
543
# File 'app/models/design_tool_fixture.rb', line 532

def self.get_dtfs_from_options(options)
  if options.empty?
    dtfs = DesignToolFixture.all
  else
    dtfs = DesignToolFixture
    dtfs = dtfs.where(["updated_at > ?", options[:updated_after_days_ago].days.ago]) if options[:updated_after_days_ago] && (options[:updated_after_days_ago] >= 0)
    dtfs = dtfs.where(updated_at: ...options[:updated_before_days_ago].days.ago) if options[:updated_before_days_ago] && (options[:updated_before_days_ago] >= 0)
    dtfs = dtfs.where(["snap_distance > ?", options[:snap_distance_gt]]) if options[:snap_distance_gt] && (options[:snap_distance_gt] >= 0)
    dtfs = dtfs.where(snap_distance: ...(options[:snap_distance_lt])) if options[:snap_distance_lt] && (options[:snap_distance_lt] >= 0)
  end
  dtfs
end

.import_from_xml(xml_path = nil) ⇒ void

This method returns an undefined value.

Parameters:

  • xml_path (Object) (defaults to: nil)


352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'app/models/design_tool_fixture.rb', line 352

def self.import_from_xml(xml_path = nil)
  xml_path ||= '/Users/cbillen/Projects/myprojects/public/DT/params.xml'
  require 'nokogiri'
  doc = Nokogiri::XML(File.open(xml_path))
  doc.xpath('//Fixtures/Fixture').each do |i|
    class_key = i['classKey']
    # puts "Processing node #{class_key}"
    dtf = DesignToolFixture.find_by(class_key: class_key)
    dtf ||= DesignToolFixture.new(class_key: class_key)

    dtf.menu_image_file = i['menuImageURL'].split('/')[1]
    dtf.room_image_file = i['roomImageURL'].split('/')[1]
    dtf.name = i.xpath('DisplayName').text
    dtf.symbol_key = i.xpath('SymbolKey').text
    lpa = i.xpath('LPAException').first
    if lpa['type'] == 'none'
      dtf.lpa_type = nil
      dtf.lpa_n = nil
      dtf.lpa_e = nil
      dtf.lpa_s = nil
      dtf.lpa_w = nil
      dtf.lpa_x = nil
      dtf.lpa_y = nil
    else
      dtf.lpa_type = lpa['type']
      dtf.lpa_n = lpa['north']
      dtf.lpa_e = lpa['east']
      dtf.lpa_s = lpa['south']
      dtf.lpa_w = lpa['west']
      dtf.lpa_x = lpa['offsetX']
      dtf.lpa_y = lpa['offsetY']
    end
    spa = i.xpath('SPAException').first
    if spa['type'] == 'none'
      dtf.spa_type = nil
      dtf.spa_n = nil
      dtf.spa_e = nil
      dtf.spa_s = nil
      dtf.spa_w = nil
      dtf.spa_x = nil
      dtf.spa_y = nil
    else
      dtf.spa_type = spa['type']
      dtf.spa_n = spa['north']
      dtf.spa_e = spa['east']
      dtf.spa_s = spa['south']
      dtf.spa_w = spa['west']
      dtf.spa_x = spa['offsetX']
      dtf.spa_y = spa['offsetY']
    end

    hpa = i.xpath('HPAException').first
    if hpa['type'] == 'none'
      dtf.hpa_type = nil
      dtf.hpa_n = nil
      dtf.hpa_e = nil
      dtf.hpa_s = nil
      dtf.hpa_w = nil
      dtf.hpa_x = nil
      dtf.hpa_y = nil
    else
      dtf.hpa_type = hpa['type']
      dtf.hpa_n = hpa['north']
      dtf.hpa_e = hpa['east']
      dtf.hpa_s = hpa['south']
      dtf.hpa_w = hpa['west']
      dtf.hpa_x = hpa['offsetX']
      dtf.hpa_y = hpa['offsetY']
    end

    dtf.size_x_min = i.xpath('SizeBounds').first['xdMin'].to_i
    dtf.size_x_max = i.xpath('SizeBounds').first['xdMax'].to_i
    dtf.size_y_min = i.xpath('SizeBounds').first['ydMin'].to_i
    dtf.size_y_max = i.xpath('SizeBounds').first['ydMax'].to_i

    dtf.size_x = i.xpath('DefaultSize').first['x'].to_i
    dtf.size_y = i.xpath('DefaultSize').first['y'].to_i

    dtf.auto_rotate = i.xpath('SnapParams').first['autoRotateEnabled'] || false
    dtf.snap_enabled = i.xpath('SnapParams').first['snapEnabled'] || false
    dtf.snap_to_center = i.xpath('SnapParams').first['snapToCenter'] || false
    dtf.snap_distance = i.xpath('SnapParams').first['snapDistance'].to_i
    dtf.snap_rotation = i.xpath('SnapParams').first['snapRotation'].to_i

    dtf.category = i.xpath('Category').text
    # Process rooms
    i.xpath('RoomType').each do |rt|
      room_type = RoomType.where("name ILIKE ?", rt.text).first
      dtf.room_types << room_type if room_type
    end

    dtf.save
    # puts "DTF #{dtf.id} saved."
    # puts dtf.to_yaml
  end
end

.invert_n_and_s(options = {}) ⇒ void

This method returns an undefined value.

Inverts the n and s heating attributes on the selected fixtures.

Parameters:

Options Hash (options):

  • updated_after_days_ago (Integer, nil)

    only fixtures updated after this many days ago

  • updated_before_days_ago (Integer, nil)

    only fixtures updated before this many days ago

  • snap_distance_gt (Integer, nil)

    only fixtures with snap_distance greater than this

  • snap_distance_lt (Integer, nil)

    only fixtures with snap_distance less than this



495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'app/models/design_tool_fixture.rb', line 495

def self.invert_n_and_s(options = {})
  dtfs = DesignToolFixture.get_dtfs_from_options(options)
  logger.debug "invert_n_and_s, dtfs.count: #{dtfs.count}"
  dtfs.each do |dtf|
    before = "dtf.pha_n: #{dtf.pha_n}, dtf.pha_s: #{dtf.pha_s}, dtf.lpa_n: #{dtf.lpa_n}, dtf.lpa_s: #{dtf.lpa_s}, dtf.spa_n: #{dtf.spa_n}, dtf.spa_s: #{dtf.spa_s}, dtf.hpa_n: #{dtf.hpa_n}, dtf.hpa_s: #{dtf.hpa_s}"
    logger.debug "before: #{before}"
    n = dtf.pha_n
    s = dtf.pha_s
    dtf.pha_n = s
    dtf.pha_s = n
    n = dtf.lpa_n
    s = dtf.lpa_s
    dtf.lpa_n = s
    dtf.lpa_s = n
    n = dtf.spa_n
    s = dtf.spa_s
    dtf.spa_n = s
    dtf.spa_s = n
    n = dtf.hpa_n
    s = dtf.hpa_s
    dtf.hpa_n = s
    dtf.hpa_s = n
    after = "dtf.pha_n: #{dtf.pha_n}, dtf.pha_s: #{dtf.pha_s}, dtf.lpa_n: #{dtf.lpa_n}, dtf.lpa_s: #{dtf.lpa_s}, dtf.spa_n: #{dtf.spa_n}, dtf.spa_s: #{dtf.spa_s}, dtf.hpa_n: #{dtf.hpa_n}, dtf.hpa_s: #{dtf.hpa_s}"
    logger.debug "after: #{after}"
    logger.debug "invert_n_and_s about to dtf.save"
    dtf.save
  end
end

.update_measurementsvoid

This method returns an undefined value.



343
344
345
346
347
348
# File 'app/models/design_tool_fixture.rb', line 343

def self.update_measurements
  DesignToolFixture.find_each do |f|
    f.update_measurements
    f.save
  end
end

Instance Method Details

#as_json(_options = {}) ⇒ void

This method returns an undefined value.

Parameters:

  • _options (Object) (defaults to: {})


260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'app/models/design_tool_fixture.rb', line 260

def as_json(_options = {})
  hole_punches = if spa_e.nil?
                   []
                 else
                   [{
                       height: to_su(spa_height + FUDGE_FACTOR),
                       width: to_su(spa_width + FUDGE_FACTOR),
                       offset: {
                         x: -to_su(spa_e),
                         y: -to_su(spa_s)
                       }
                     }]
                 end

  toasty_zones = if pha_e.nil?
                   []
                 else
                   [{
                       height: to_su(pha_height - FUDGE_FACTOR),
                       width: to_su(pha_width - FUDGE_FACTOR),
                       offset: {
                         x: -to_su(pha_e - FUDGE_FACTOR),
                         y: -to_su(pha_s + FUDGE_FACTOR)
                       }
                     }]
                 end

  {
    id: id,
    name: name,
    key: class_key,
    category: category,
    x: nil,
    y: nil,
    height: to_su(size_y),
    width: to_su(size_x),
    rot: 0,
    scaleX: 1,
    scaleXMin: size_x_min.to_f / size_x,
    scaleXMax: size_x_max.to_f / size_x,
    scaleY: 1,
    scaleYMin: size_y_min.to_f / size_y,
    scaleYMax: size_y_max.to_f / size_y,
    # Snap params (used by the JS "magnet" snapping behavior)
    snapEnabled: snap_enabled,
    autoRotate: auto_rotate,
    snapRotation: snap_rotation,
    # snap_distance is stored in inches (from params.xml). Convert to screen units.
    snapDistance: to_su(snap_distance.to_f),
    snapToCenter: snap_to_center,
    # magnet: {
    #   x: 208,
    #   y: 482.9232829714554
    # },
    magnetOffset: {
      x: 0,
      y: (((snap_to_center? ? (size_y / 2.0) : -snap_distance).to_f - 0.1) / 12 / SCREEN_UNITS_IN_A_FOOT)
      # snap_distance cannot be exactly half-height, so we +0.1
    },
    holePunches: hole_punches,
    toastyZones: toasty_zones,
    isInvalid: false,
    visual: visual
  }
end

#room_typesActiveRecord::Relation<RoomType>

convertScreenUnitsToMeters = (su) => su * SCREEN_UNITS_IN_A_METER
convertScreenUnitsToFt = (su) => su * SCREEN_UNITS_IN_A_FOOT
convertScreenUnitsToInches = (su) => su * SCREEN_UNITS_IN_A_FOOT * 12
convertMetersToScreenUnits = (m) => m / SCREEN_UNITS_IN_A_METER
convertFtToScreenUnits = (feet) => feet / SCREEN_UNITS_IN_A_FOOT
convertInchesToScreenUnits = (inches) => inches / 12 / SCREEN_UNITS_IN_A_FOOT

Returns:

See Also:



243
# File 'app/models/design_tool_fixture.rb', line 243

has_and_belongs_to_many :room_types

#to_su(inches) ⇒ void

This method returns an undefined value.

Parameters:

  • inches (Object)


254
255
256
# File 'app/models/design_tool_fixture.rb', line 254

def to_su(inches)
  inches.to_f / 12 / SCREEN_UNITS_IN_A_FOOT
end

#update_measurementsvoid

This method returns an undefined value.



327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'app/models/design_tool_fixture.rb', line 327

def update_measurements
  self.scale_x_min = size_x_min.to_f / size_x
  self.scale_x_max = size_x_max.to_f / size_x
  self.scale_y_min = size_y_min.to_f / size_y
  self.scale_y_max = size_y_max.to_f / size_y
  self.pha_height = pha_n.nil? ? nil : (pha_n + size_y + pha_s)
  self.pha_width = pha_w.nil? ? nil : (pha_w + size_x + pha_e)
  self.lpa_height = lpa_n.nil? ? nil : (lpa_n + size_y + lpa_s)
  self.lpa_width = lpa_w.nil? ? nil : (lpa_w + size_x + lpa_e)
  self.spa_height = spa_n.nil? ? nil : (spa_n + size_y + spa_s)
  self.spa_width = spa_w.nil? ? nil : (spa_w + size_x + spa_e)
  self.hpa_height = hpa_n.nil? ? nil : (hpa_n + size_y + hpa_s)
  self.hpa_width = hpa_w.nil? ? nil : (hpa_w + size_x + hpa_e)
end

#visualvoid

This method returns an undefined value.



248
249
250
# File 'app/models/design_tool_fixture.rb', line 248

def visual
  super&.gsub(/"/m, '\'')&.gsub(/\r\n\s+/m, '')
end