feat(sketch): 👷 add fcinfo generation pre-commit hook
This commit is contained in:
parent
7b0c842513
commit
efe65da3ad
|
@ -5,7 +5,10 @@
|
|||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/base:alpine-3.18",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-community/features/deno:1": {}
|
||||
"ghcr.io/devcontainers-community/features/deno:1": {},
|
||||
"ghcr.io/devcontainers/features/python": {
|
||||
"version": "3"
|
||||
}
|
||||
}
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"$schema": "https://deno.land/x/denoh@v3.2.0/schema.json", //githooks schema
|
||||
"tasks": {
|
||||
//Config
|
||||
"hooks:install": "deno run https://deno.land/x/denoh@v3.2.0/denoh.ts",
|
||||
"hooks:install": "deno run --allow-read=. --allow-write=./.git/hooks https://deno.land/x/denoh@v3.2.0/denoh.ts",
|
||||
//Global
|
||||
"check": "deno task web:check",
|
||||
"dev:deploy": "deno task dev:deploy:reset && deno task dev:deploy:send && deno task dev:deploy:start",
|
||||
|
@ -11,7 +11,9 @@
|
|||
"dev:deploy:start": "ssh julien@gowest.local \"deno task -c gowest/deno.jsonc dev:deploy:serve\"",
|
||||
"dev:deploy:serve": "sudo -- sh -c 'PORT=80 deno task -c /home/julien/gowest/deno.jsonc web:start | tee /var/log/gowest/web/$(date -u +\"%Y-%m-%dT%H:%M:%SZ\").log'",
|
||||
//Web
|
||||
"web:start": "deno task -c ./web/deno.json start"
|
||||
"web:start": "deno task -c ./web/deno.json start",
|
||||
//Sketch
|
||||
"sketch:fcinfo": "deno run --allow-read=. --allow-write=./sketch --allow-run=python3 ./scripts/fcinfo.ts"
|
||||
},
|
||||
"importMap": "./import_map.json",
|
||||
"workspaces": [
|
||||
|
@ -27,5 +29,8 @@
|
|||
"useTabs": true,
|
||||
"semiColons": false,
|
||||
"indentWidth": 4
|
||||
},
|
||||
"githooks": {
|
||||
"pre-commit": ["sketch:fcinfo"]
|
||||
}
|
||||
}
|
||||
|
|
26
scripts/fcinfo.ts
Normal file
26
scripts/fcinfo.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { expandGlob } from 'https://deno.land/std@0.219.1/fs/expand_glob.ts'
|
||||
|
||||
for await (const file of expandGlob('./sketch/*.FCStd')) {
|
||||
const fcinfo = new Deno.Command('python3', {
|
||||
args: ['./utils/fcinfo.py', file.path],
|
||||
})
|
||||
|
||||
const { success, stderr, stdout } = await fcinfo.output()
|
||||
|
||||
if (success) {
|
||||
console.log(
|
||||
`%c[scripts:fcinfo]%c ${file.name}`,
|
||||
'color: green; font-weight: bold',
|
||||
'',
|
||||
)
|
||||
Deno.writeFile(file.path.replace('.FCStd', '.fcinfo'), stdout)
|
||||
} else {
|
||||
console.error(
|
||||
`%c[scripts:fcinfo]%c ${file.name}\n${
|
||||
new TextDecoder().decode(stderr)
|
||||
}`,
|
||||
'color: red; font-weight: bold',
|
||||
'',
|
||||
)
|
||||
}
|
||||
}
|
293
utils/fcinfo.py
Normal file
293
utils/fcinfo.py
Normal file
|
@ -0,0 +1,293 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# -*- coding: utf8 -*-
|
||||
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * 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 Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
__title__ = "FreeCAD File info utility"
|
||||
__author__ = "Yorik van Havre"
|
||||
__url__ = ["https://www.freecad.org"]
|
||||
__doc__ = """
|
||||
This utility prints information about a given FreeCAD file (*.FCStd)
|
||||
on screen, including document properties, number of included objects,
|
||||
object sizes and properties and values. Its main use is to compare
|
||||
two files and be able to see the differences in a text-based form.
|
||||
|
||||
If no option is used, fcinfo prints the document properties and a list
|
||||
of properties of each object found in the given file.
|
||||
|
||||
Usage:
|
||||
|
||||
fcinfo [options] myfile.FCStd
|
||||
|
||||
Options:
|
||||
|
||||
-h, --help: Prints this help text
|
||||
-s, --short: Do not print object properties. Only one line
|
||||
per object is printed, including its size and SHA1.
|
||||
This is sufficient to see that an object has
|
||||
changed, but not what exactly has changed.
|
||||
-vs --veryshort: Only prints the document info, not objects info.
|
||||
This is sufficient to see if a file has changed, as
|
||||
its SHA1 code and timestamp will show it. But won't
|
||||
show details of what has changed.
|
||||
-g --gui: Adds visual properties too (if not using -s or -vs)
|
||||
|
||||
Git usage:
|
||||
|
||||
This script can be used as a textconv tool for git diff by
|
||||
configuring your git folder as follows:
|
||||
|
||||
1) add to .gitattributes (or ~/.gitattributes for user-wide):
|
||||
|
||||
*.fcstd diff=fcinfo
|
||||
|
||||
2) add to .git/config (or ~/.gitconfig for user-wide):
|
||||
|
||||
[diff "fcinfo"]
|
||||
textconv = /path/to/fcinfo
|
||||
|
||||
With this, when committing a .FCStd file with Git,
|
||||
'git diff' will show you the difference between the two
|
||||
texts obtained by fcinfo
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
import zipfile
|
||||
import xml.sax
|
||||
import os
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
|
||||
class FreeCADFileHandler(xml.sax.ContentHandler):
|
||||
def __init__(self, zfile, short=0): # short: 0=normal, 1=short, 2=veryshort
|
||||
|
||||
xml.sax.ContentHandler.__init__(self)
|
||||
self.zfile = zfile
|
||||
self.obj = None
|
||||
self.prop = None
|
||||
self.count = "0"
|
||||
self.contents = {}
|
||||
self.short = short
|
||||
|
||||
def startElement(self, tag, attributes):
|
||||
|
||||
if tag == "Document":
|
||||
self.obj = tag
|
||||
self.contents = {}
|
||||
self.contents["ProgramVersion"] = attributes["ProgramVersion"]
|
||||
self.contents["FileVersion"] = attributes["FileVersion"]
|
||||
|
||||
elif tag == "Object":
|
||||
if "name" in attributes:
|
||||
name = self.clean(attributes["name"])
|
||||
self.obj = name
|
||||
if "type" in attributes:
|
||||
self.contents[name] = attributes["type"]
|
||||
|
||||
elif tag == "ViewProvider":
|
||||
if "name" in attributes:
|
||||
self.obj = self.clean(attributes["name"])
|
||||
|
||||
elif tag == "Part":
|
||||
if self.obj:
|
||||
r = self.zfile.read(attributes["file"])
|
||||
s = r.__sizeof__()
|
||||
if s < 1024:
|
||||
s = str(s) + "B"
|
||||
elif s > 1048576:
|
||||
s = str(s / 1048576) + "M"
|
||||
else:
|
||||
s = str(s / 1024) + "K"
|
||||
s += " " + str(hashlib.sha1(r).hexdigest()[:12])
|
||||
self.contents[self.obj] += " (" + s + ")"
|
||||
|
||||
elif tag == "Property":
|
||||
self.prop = None
|
||||
# skip "internal" properties, useless for a diff
|
||||
if attributes["name"] not in [
|
||||
"Symbol",
|
||||
"AttacherType",
|
||||
"MapMode",
|
||||
"MapPathParameter",
|
||||
"MapReversed",
|
||||
"AttachmentOffset",
|
||||
"SelectionStyle",
|
||||
"TightGrid",
|
||||
"GridSize",
|
||||
"GridSnap",
|
||||
"GridStyle",
|
||||
"Lighting",
|
||||
"Deviation",
|
||||
"AngularDeflection",
|
||||
"BoundingBox",
|
||||
"Selectable",
|
||||
"ShowGrid",
|
||||
]:
|
||||
self.prop = attributes["name"]
|
||||
|
||||
elif tag in ["String", "Uuid", "Float", "Integer", "Bool", "Link"]:
|
||||
if self.prop and ("value" in attributes):
|
||||
if self.obj == "Document":
|
||||
self.contents[self.prop] = attributes["value"]
|
||||
elif self.short == 0:
|
||||
if tag == "Float":
|
||||
self.contents[self.obj + "00000000::" + self.prop] = str(
|
||||
float(attributes["value"])
|
||||
)
|
||||
else:
|
||||
self.contents[self.obj + "00000000::" + self.prop] = attributes["value"]
|
||||
|
||||
elif tag in ["PropertyVector"]:
|
||||
if self.prop and self.obj and (self.short == 0):
|
||||
val = (
|
||||
"("
|
||||
+ str(float(attributes["valueX"]))
|
||||
+ ","
|
||||
+ str(float(attributes["valueY"]))
|
||||
+ ","
|
||||
+ str(float(attributes["valueZ"]))
|
||||
+ ")"
|
||||
)
|
||||
self.contents[self.obj + "00000000::" + self.prop] = val
|
||||
|
||||
elif tag in ["PropertyPlacement"]:
|
||||
if self.prop and self.obj and (self.short == 0):
|
||||
val = (
|
||||
"("
|
||||
+ str(float(attributes["Px"]))
|
||||
+ ","
|
||||
+ str(float(attributes["Py"]))
|
||||
+ ","
|
||||
+ str(float(attributes["Pz"]))
|
||||
+ ")"
|
||||
)
|
||||
val += (
|
||||
" ("
|
||||
+ str(round(float(attributes["Q0"]), 4))
|
||||
+ ","
|
||||
+ str(round(float(attributes["Q1"]), 4))
|
||||
+ ","
|
||||
)
|
||||
val += (
|
||||
str(round(float(attributes["Q2"]), 4))
|
||||
+ ","
|
||||
+ str(round(float(attributes["Q3"]), 4))
|
||||
+ ")"
|
||||
)
|
||||
self.contents[self.obj + "00000000::" + self.prop] = val
|
||||
|
||||
elif tag in ["PropertyColor"]:
|
||||
if self.prop and self.obj and (self.short == 0):
|
||||
c = int(attributes["value"])
|
||||
r = float((c >> 24) & 0xFF) / 255.0
|
||||
g = float((c >> 16) & 0xFF) / 255.0
|
||||
b = float((c >> 8) & 0xFF) / 255.0
|
||||
val = str((r, g, b))
|
||||
self.contents[self.obj + "00000000::" + self.prop] = val
|
||||
|
||||
elif tag == "Objects":
|
||||
self.count = attributes["Count"]
|
||||
self.obj = None
|
||||
|
||||
# Print all the contents of the document properties
|
||||
items = self.contents.items()
|
||||
items = sorted(items)
|
||||
for key, value in items:
|
||||
key = self.clean(key)
|
||||
value = self.clean(value)
|
||||
print(" " + key + " : " + value)
|
||||
print(" Objects: (" + self.count + ")")
|
||||
self.contents = {}
|
||||
|
||||
def endElement(self, tag):
|
||||
|
||||
if (tag == "Document") and (self.short != 2):
|
||||
items = self.contents.items()
|
||||
items = sorted(items)
|
||||
for key, value in items:
|
||||
key = self.clean(key)
|
||||
if "00000000::" in key:
|
||||
key = " " + key.split("00000000::")[1]
|
||||
value = self.clean(value)
|
||||
if value:
|
||||
print(" " + key + " : " + value)
|
||||
|
||||
def clean(self, value):
|
||||
|
||||
value = value.strip()
|
||||
return value
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
sys.exit()
|
||||
|
||||
if ("-h" in sys.argv[1:]) or ("--help" in sys.argv[1:]):
|
||||
print(__doc__)
|
||||
sys.exit()
|
||||
|
||||
ext = sys.argv[-1].rsplit(".")[-1].lower()
|
||||
if not ext.startswith("fcstd") and not ext.startswith("fcbak"):
|
||||
print(__doc__)
|
||||
sys.exit()
|
||||
|
||||
if ("-vs" in sys.argv[1:]) or ("--veryshort" in sys.argv[1:]):
|
||||
short = 2
|
||||
elif ("-s" in sys.argv[1:]) or ("--short" in sys.argv[1:]):
|
||||
short = 1
|
||||
else:
|
||||
short = 0
|
||||
|
||||
if ("-g" in sys.argv[1:]) or ("--gui" in sys.argv[1:]):
|
||||
gui = True
|
||||
else:
|
||||
gui = False
|
||||
|
||||
zfile = zipfile.ZipFile(sys.argv[-1])
|
||||
|
||||
if not "Document.xml" in zfile.namelist():
|
||||
sys.exit(1)
|
||||
doc = zfile.read("Document.xml")
|
||||
if gui and "GuiDocument.xml" in zfile.namelist():
|
||||
guidoc = zfile.read("GuiDocument.xml")
|
||||
guidoc = re.sub(b"<\?xml.*?-->", b" ", guidoc, flags=re.MULTILINE | re.DOTALL)
|
||||
# a valid xml doc can have only one root element. So we need to insert
|
||||
# all the contents of the GUiDocument <document> tag into the main one
|
||||
doc = re.sub(b"<\/Document>", b"", doc, flags=re.MULTILINE | re.DOTALL)
|
||||
guidoc = re.sub(b"<Document.*?>", b" ", guidoc, flags=re.MULTILINE | re.DOTALL)
|
||||
doc += guidoc
|
||||
s = os.path.getsize(sys.argv[-1])
|
||||
if s < 1024:
|
||||
s = str(s) + "B"
|
||||
elif s > 1048576:
|
||||
s = str(s / 1048576) + "M"
|
||||
else:
|
||||
s = str(s / 1024) + "K"
|
||||
print("Document: " + sys.argv[-1] + " (" + s + ")")
|
||||
print(" SHA1: " + str(hashlib.sha1(open(sys.argv[-1], "rb").read()).hexdigest()))
|
||||
xml.sax.parseString(doc, FreeCADFileHandler(zfile, short))
|
Loading…
Reference in a new issue