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 Schedulable

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 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) ⇒ Object



511
512
513
514
515
516
517
518
# File 'app/models/design_tool_fixture.rb', line 511

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_csvObject



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

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_csvObject



440
441
442
443
444
445
446
447
448
# File 'app/models/design_tool_fixture.rb', line 440

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) ⇒ Object

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



463
464
465
466
467
# File 'app/models/design_tool_fixture.rb', line 463

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) ⇒ Object



498
499
500
501
502
503
504
505
506
507
508
509
# File 'app/models/design_tool_fixture.rb', line 498

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) ⇒ Object



343
344
345
346
347
348
349
350
351
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
# File 'app/models/design_tool_fixture.rb', line 343

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 = {}) ⇒ Object



469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'app/models/design_tool_fixture.rb', line 469

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_measurementsObject



336
337
338
339
340
341
# File 'app/models/design_tool_fixture.rb', line 336

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

Instance Method Details

#as_json(_options = {}) ⇒ Object



255
256
257
258
259
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
# File 'app/models/design_tool_fixture.rb', line 255

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) ⇒ Object



251
252
253
# File 'app/models/design_tool_fixture.rb', line 251

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

#update_measurementsObject



321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'app/models/design_tool_fixture.rb', line 321

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

#visualObject



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

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